This is page 2 of 60. Use http://codebase.md/hangwin/mcp-chrome?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .gitattributes
├── .github
│ └── workflows
│ └── build-release.yml
├── .gitignore
├── .husky
│ ├── commit-msg
│ └── pre-commit
├── .prettierignore
├── .prettierrc.json
├── .vscode
│ └── extensions.json
├── app
│ ├── chrome-extension
│ │ ├── _locales
│ │ │ ├── de
│ │ │ │ └── messages.json
│ │ │ ├── en
│ │ │ │ └── messages.json
│ │ │ ├── ja
│ │ │ │ └── messages.json
│ │ │ ├── ko
│ │ │ │ └── messages.json
│ │ │ ├── zh_CN
│ │ │ │ └── messages.json
│ │ │ └── zh_TW
│ │ │ └── messages.json
│ │ ├── .env.example
│ │ ├── assets
│ │ │ └── vue.svg
│ │ ├── common
│ │ │ ├── agent-models.ts
│ │ │ ├── constants.ts
│ │ │ ├── element-marker-types.ts
│ │ │ ├── message-types.ts
│ │ │ ├── node-types.ts
│ │ │ ├── rr-v3-keepalive-protocol.ts
│ │ │ ├── step-types.ts
│ │ │ ├── tool-handler.ts
│ │ │ └── web-editor-types.ts
│ │ ├── entrypoints
│ │ │ ├── background
│ │ │ │ ├── element-marker
│ │ │ │ │ ├── element-marker-storage.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── keepalive-manager.ts
│ │ │ │ ├── native-host.ts
│ │ │ │ ├── quick-panel
│ │ │ │ │ ├── agent-handler.ts
│ │ │ │ │ ├── commands.ts
│ │ │ │ │ └── tabs-handler.ts
│ │ │ │ ├── record-replay
│ │ │ │ │ ├── actions
│ │ │ │ │ │ ├── adapter.ts
│ │ │ │ │ │ ├── handlers
│ │ │ │ │ │ │ ├── assert.ts
│ │ │ │ │ │ │ ├── click.ts
│ │ │ │ │ │ │ ├── common.ts
│ │ │ │ │ │ │ ├── control-flow.ts
│ │ │ │ │ │ │ ├── delay.ts
│ │ │ │ │ │ │ ├── dom.ts
│ │ │ │ │ │ │ ├── drag.ts
│ │ │ │ │ │ │ ├── extract.ts
│ │ │ │ │ │ │ ├── fill.ts
│ │ │ │ │ │ │ ├── http.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── key.ts
│ │ │ │ │ │ │ ├── navigate.ts
│ │ │ │ │ │ │ ├── screenshot.ts
│ │ │ │ │ │ │ ├── script.ts
│ │ │ │ │ │ │ ├── scroll.ts
│ │ │ │ │ │ │ ├── tabs.ts
│ │ │ │ │ │ │ └── wait.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── registry.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── engine
│ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ ├── execution-mode.ts
│ │ │ │ │ │ ├── logging
│ │ │ │ │ │ │ └── run-logger.ts
│ │ │ │ │ │ ├── plugins
│ │ │ │ │ │ │ ├── breakpoint.ts
│ │ │ │ │ │ │ ├── manager.ts
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── policies
│ │ │ │ │ │ │ ├── retry.ts
│ │ │ │ │ │ │ └── wait.ts
│ │ │ │ │ │ ├── runners
│ │ │ │ │ │ │ ├── after-script-queue.ts
│ │ │ │ │ │ │ ├── control-flow-runner.ts
│ │ │ │ │ │ │ ├── step-executor.ts
│ │ │ │ │ │ │ ├── step-runner.ts
│ │ │ │ │ │ │ └── subflow-runner.ts
│ │ │ │ │ │ ├── scheduler.ts
│ │ │ │ │ │ ├── state-manager.ts
│ │ │ │ │ │ └── utils
│ │ │ │ │ │ └── expression.ts
│ │ │ │ │ ├── flow-runner.ts
│ │ │ │ │ ├── flow-store.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── legacy-types.ts
│ │ │ │ │ ├── nodes
│ │ │ │ │ │ ├── assert.ts
│ │ │ │ │ │ ├── click.ts
│ │ │ │ │ │ ├── conditional.ts
│ │ │ │ │ │ ├── download-screenshot-attr-event-frame-loop.ts
│ │ │ │ │ │ ├── drag.ts
│ │ │ │ │ │ ├── execute-flow.ts
│ │ │ │ │ │ ├── extract.ts
│ │ │ │ │ │ ├── fill.ts
│ │ │ │ │ │ ├── http.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── key.ts
│ │ │ │ │ │ ├── loops.ts
│ │ │ │ │ │ ├── navigate.ts
│ │ │ │ │ │ ├── script.ts
│ │ │ │ │ │ ├── scroll.ts
│ │ │ │ │ │ ├── tabs.ts
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── wait.ts
│ │ │ │ │ ├── recording
│ │ │ │ │ │ ├── browser-event-listener.ts
│ │ │ │ │ │ ├── content-injection.ts
│ │ │ │ │ │ ├── content-message-handler.ts
│ │ │ │ │ │ ├── flow-builder.ts
│ │ │ │ │ │ ├── recorder-manager.ts
│ │ │ │ │ │ └── session-manager.ts
│ │ │ │ │ ├── rr-utils.ts
│ │ │ │ │ ├── selector-engine.ts
│ │ │ │ │ ├── storage
│ │ │ │ │ │ └── indexeddb-manager.ts
│ │ │ │ │ ├── trigger-store.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── record-replay-v3
│ │ │ │ │ ├── bootstrap.ts
│ │ │ │ │ ├── domain
│ │ │ │ │ │ ├── debug.ts
│ │ │ │ │ │ ├── errors.ts
│ │ │ │ │ │ ├── events.ts
│ │ │ │ │ │ ├── flow.ts
│ │ │ │ │ │ ├── ids.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── json.ts
│ │ │ │ │ │ ├── policy.ts
│ │ │ │ │ │ ├── triggers.ts
│ │ │ │ │ │ └── variables.ts
│ │ │ │ │ ├── engine
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── keepalive
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── offscreen-keepalive.ts
│ │ │ │ │ │ ├── kernel
│ │ │ │ │ │ │ ├── artifacts.ts
│ │ │ │ │ │ │ ├── breakpoints.ts
│ │ │ │ │ │ │ ├── debug-controller.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── kernel.ts
│ │ │ │ │ │ │ ├── recovery-kernel.ts
│ │ │ │ │ │ │ ├── runner.ts
│ │ │ │ │ │ │ └── traversal.ts
│ │ │ │ │ │ ├── plugins
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── register-v2-replay-nodes.ts
│ │ │ │ │ │ │ ├── registry.ts
│ │ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ │ └── v2-action-adapter.ts
│ │ │ │ │ │ ├── queue
│ │ │ │ │ │ │ ├── enqueue-run.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── leasing.ts
│ │ │ │ │ │ │ ├── queue.ts
│ │ │ │ │ │ │ └── scheduler.ts
│ │ │ │ │ │ ├── recovery
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── recovery-coordinator.ts
│ │ │ │ │ │ ├── storage
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── storage-port.ts
│ │ │ │ │ │ ├── transport
│ │ │ │ │ │ │ ├── events-bus.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── rpc-server.ts
│ │ │ │ │ │ │ └── rpc.ts
│ │ │ │ │ │ └── triggers
│ │ │ │ │ │ ├── command-trigger.ts
│ │ │ │ │ │ ├── context-menu-trigger.ts
│ │ │ │ │ │ ├── cron-trigger.ts
│ │ │ │ │ │ ├── dom-trigger.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── interval-trigger.ts
│ │ │ │ │ │ ├── manual-trigger.ts
│ │ │ │ │ │ ├── once-trigger.ts
│ │ │ │ │ │ ├── trigger-handler.ts
│ │ │ │ │ │ ├── trigger-manager.ts
│ │ │ │ │ │ └── url-trigger.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── storage
│ │ │ │ │ ├── db.ts
│ │ │ │ │ ├── events.ts
│ │ │ │ │ ├── flows.ts
│ │ │ │ │ ├── import
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── v2-reader.ts
│ │ │ │ │ │ └── v2-to-v3.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── persistent-vars.ts
│ │ │ │ │ ├── queue.ts
│ │ │ │ │ ├── runs.ts
│ │ │ │ │ └── triggers.ts
│ │ │ │ ├── semantic-similarity.ts
│ │ │ │ ├── storage-manager.ts
│ │ │ │ ├── tools
│ │ │ │ │ ├── base-browser.ts
│ │ │ │ │ ├── browser
│ │ │ │ │ │ ├── bookmark.ts
│ │ │ │ │ │ ├── common.ts
│ │ │ │ │ │ ├── computer.ts
│ │ │ │ │ │ ├── console-buffer.ts
│ │ │ │ │ │ ├── console.ts
│ │ │ │ │ │ ├── dialog.ts
│ │ │ │ │ │ ├── download.ts
│ │ │ │ │ │ ├── element-picker.ts
│ │ │ │ │ │ ├── file-upload.ts
│ │ │ │ │ │ ├── gif-auto-capture.ts
│ │ │ │ │ │ ├── gif-enhanced-renderer.ts
│ │ │ │ │ │ ├── gif-recorder.ts
│ │ │ │ │ │ ├── history.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── inject-script.ts
│ │ │ │ │ │ ├── interaction.ts
│ │ │ │ │ │ ├── javascript.ts
│ │ │ │ │ │ ├── keyboard.ts
│ │ │ │ │ │ ├── network-capture-debugger.ts
│ │ │ │ │ │ ├── network-capture-web-request.ts
│ │ │ │ │ │ ├── network-capture.ts
│ │ │ │ │ │ ├── network-request.ts
│ │ │ │ │ │ ├── performance.ts
│ │ │ │ │ │ ├── read-page.ts
│ │ │ │ │ │ ├── screenshot.ts
│ │ │ │ │ │ ├── userscript.ts
│ │ │ │ │ │ ├── vector-search.ts
│ │ │ │ │ │ ├── web-fetcher.ts
│ │ │ │ │ │ └── window.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── record-replay.ts
│ │ │ │ ├── utils
│ │ │ │ │ └── sidepanel.ts
│ │ │ │ └── web-editor
│ │ │ │ └── index.ts
│ │ │ ├── builder
│ │ │ │ ├── App.vue
│ │ │ │ ├── index.html
│ │ │ │ └── main.ts
│ │ │ ├── content.ts
│ │ │ ├── element-picker.content.ts
│ │ │ ├── offscreen
│ │ │ │ ├── gif-encoder.ts
│ │ │ │ ├── index.html
│ │ │ │ ├── main.ts
│ │ │ │ └── rr-keepalive.ts
│ │ │ ├── options
│ │ │ │ ├── App.vue
│ │ │ │ ├── index.html
│ │ │ │ └── main.ts
│ │ │ ├── popup
│ │ │ │ ├── App.vue
│ │ │ │ ├── components
│ │ │ │ │ ├── builder
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ ├── Canvas.vue
│ │ │ │ │ │ │ ├── EdgePropertyPanel.vue
│ │ │ │ │ │ │ ├── KeyValueEditor.vue
│ │ │ │ │ │ │ ├── nodes
│ │ │ │ │ │ │ │ ├── node-util.ts
│ │ │ │ │ │ │ │ ├── NodeCard.vue
│ │ │ │ │ │ │ │ └── NodeIf.vue
│ │ │ │ │ │ │ ├── properties
│ │ │ │ │ │ │ │ ├── PropertyAssert.vue
│ │ │ │ │ │ │ │ ├── PropertyClick.vue
│ │ │ │ │ │ │ │ ├── PropertyCloseTab.vue
│ │ │ │ │ │ │ │ ├── PropertyDelay.vue
│ │ │ │ │ │ │ │ ├── PropertyDrag.vue
│ │ │ │ │ │ │ │ ├── PropertyExecuteFlow.vue
│ │ │ │ │ │ │ │ ├── PropertyExtract.vue
│ │ │ │ │ │ │ │ ├── PropertyFill.vue
│ │ │ │ │ │ │ │ ├── PropertyForeach.vue
│ │ │ │ │ │ │ │ ├── PropertyFormRenderer.vue
│ │ │ │ │ │ │ │ ├── PropertyFromSpec.vue
│ │ │ │ │ │ │ │ ├── PropertyHandleDownload.vue
│ │ │ │ │ │ │ │ ├── PropertyHttp.vue
│ │ │ │ │ │ │ │ ├── PropertyIf.vue
│ │ │ │ │ │ │ │ ├── PropertyKey.vue
│ │ │ │ │ │ │ │ ├── PropertyLoopElements.vue
│ │ │ │ │ │ │ │ ├── PropertyNavigate.vue
│ │ │ │ │ │ │ │ ├── PropertyOpenTab.vue
│ │ │ │ │ │ │ │ ├── PropertyScreenshot.vue
│ │ │ │ │ │ │ │ ├── PropertyScript.vue
│ │ │ │ │ │ │ │ ├── PropertyScroll.vue
│ │ │ │ │ │ │ │ ├── PropertySetAttribute.vue
│ │ │ │ │ │ │ │ ├── PropertySwitchFrame.vue
│ │ │ │ │ │ │ │ ├── PropertySwitchTab.vue
│ │ │ │ │ │ │ │ ├── PropertyTrigger.vue
│ │ │ │ │ │ │ │ ├── PropertyTriggerEvent.vue
│ │ │ │ │ │ │ │ ├── PropertyWait.vue
│ │ │ │ │ │ │ │ ├── PropertyWhile.vue
│ │ │ │ │ │ │ │ └── SelectorEditor.vue
│ │ │ │ │ │ │ ├── PropertyPanel.vue
│ │ │ │ │ │ │ ├── Sidebar.vue
│ │ │ │ │ │ │ └── TriggerPanel.vue
│ │ │ │ │ │ ├── model
│ │ │ │ │ │ │ ├── form-widget-registry.ts
│ │ │ │ │ │ │ ├── node-spec-registry.ts
│ │ │ │ │ │ │ ├── node-spec.ts
│ │ │ │ │ │ │ ├── node-specs-builtin.ts
│ │ │ │ │ │ │ ├── toast.ts
│ │ │ │ │ │ │ ├── transforms.ts
│ │ │ │ │ │ │ ├── ui-nodes.ts
│ │ │ │ │ │ │ ├── validation.ts
│ │ │ │ │ │ │ └── variables.ts
│ │ │ │ │ │ ├── store
│ │ │ │ │ │ │ └── useBuilderStore.ts
│ │ │ │ │ │ └── widgets
│ │ │ │ │ │ ├── FieldCode.vue
│ │ │ │ │ │ ├── FieldDuration.vue
│ │ │ │ │ │ ├── FieldExpression.vue
│ │ │ │ │ │ ├── FieldKeySequence.vue
│ │ │ │ │ │ ├── FieldSelector.vue
│ │ │ │ │ │ ├── FieldTargetLocator.vue
│ │ │ │ │ │ └── VarInput.vue
│ │ │ │ │ ├── ConfirmDialog.vue
│ │ │ │ │ ├── ElementMarkerManagement.vue
│ │ │ │ │ ├── icons
│ │ │ │ │ │ ├── BoltIcon.vue
│ │ │ │ │ │ ├── CheckIcon.vue
│ │ │ │ │ │ ├── DatabaseIcon.vue
│ │ │ │ │ │ ├── DocumentIcon.vue
│ │ │ │ │ │ ├── EditIcon.vue
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── MarkerIcon.vue
│ │ │ │ │ │ ├── RecordIcon.vue
│ │ │ │ │ │ ├── RefreshIcon.vue
│ │ │ │ │ │ ├── StopIcon.vue
│ │ │ │ │ │ ├── TabIcon.vue
│ │ │ │ │ │ ├── TrashIcon.vue
│ │ │ │ │ │ ├── VectorIcon.vue
│ │ │ │ │ │ └── WorkflowIcon.vue
│ │ │ │ │ ├── LocalModelPage.vue
│ │ │ │ │ ├── ModelCacheManagement.vue
│ │ │ │ │ ├── ProgressIndicator.vue
│ │ │ │ │ └── ScheduleDialog.vue
│ │ │ │ ├── index.html
│ │ │ │ ├── main.ts
│ │ │ │ └── style.css
│ │ │ ├── quick-panel.content.ts
│ │ │ ├── shared
│ │ │ │ ├── composables
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── useRRV3Rpc.ts
│ │ │ │ └── utils
│ │ │ │ ├── index.ts
│ │ │ │ └── rr-flow-convert.ts
│ │ │ ├── sidepanel
│ │ │ │ ├── App.vue
│ │ │ │ ├── components
│ │ │ │ │ ├── agent
│ │ │ │ │ │ ├── AttachmentPreview.vue
│ │ │ │ │ │ ├── ChatInput.vue
│ │ │ │ │ │ ├── CliSettings.vue
│ │ │ │ │ │ ├── ConnectionStatus.vue
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── MessageItem.vue
│ │ │ │ │ │ ├── MessageList.vue
│ │ │ │ │ │ ├── ProjectCreateForm.vue
│ │ │ │ │ │ └── ProjectSelector.vue
│ │ │ │ │ ├── agent-chat
│ │ │ │ │ │ ├── AgentChatShell.vue
│ │ │ │ │ │ ├── AgentComposer.vue
│ │ │ │ │ │ ├── AgentConversation.vue
│ │ │ │ │ │ ├── AgentOpenProjectMenu.vue
│ │ │ │ │ │ ├── AgentProjectMenu.vue
│ │ │ │ │ │ ├── AgentRequestThread.vue
│ │ │ │ │ │ ├── AgentSessionListItem.vue
│ │ │ │ │ │ ├── AgentSessionMenu.vue
│ │ │ │ │ │ ├── AgentSessionSettingsPanel.vue
│ │ │ │ │ │ ├── AgentSessionsView.vue
│ │ │ │ │ │ ├── AgentSettingsMenu.vue
│ │ │ │ │ │ ├── AgentTimeline.vue
│ │ │ │ │ │ ├── AgentTimelineItem.vue
│ │ │ │ │ │ ├── AgentTopBar.vue
│ │ │ │ │ │ ├── ApplyMessageChip.vue
│ │ │ │ │ │ ├── AttachmentCachePanel.vue
│ │ │ │ │ │ ├── ComposerDrawer.vue
│ │ │ │ │ │ ├── ElementChip.vue
│ │ │ │ │ │ ├── FakeCaretOverlay.vue
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── SelectionChip.vue
│ │ │ │ │ │ ├── timeline
│ │ │ │ │ │ │ ├── markstream-thinking.ts
│ │ │ │ │ │ │ ├── ThinkingNode.vue
│ │ │ │ │ │ │ ├── TimelineNarrativeStep.vue
│ │ │ │ │ │ │ ├── TimelineStatusStep.vue
│ │ │ │ │ │ │ ├── TimelineToolCallStep.vue
│ │ │ │ │ │ │ ├── TimelineToolResultCardStep.vue
│ │ │ │ │ │ │ └── TimelineUserPromptStep.vue
│ │ │ │ │ │ └── WebEditorChanges.vue
│ │ │ │ │ ├── AgentChat.vue
│ │ │ │ │ ├── rr-v3
│ │ │ │ │ │ └── DebuggerPanel.vue
│ │ │ │ │ ├── SidepanelNavigator.vue
│ │ │ │ │ └── workflows
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── WorkflowListItem.vue
│ │ │ │ │ └── WorkflowsView.vue
│ │ │ │ ├── composables
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── useAgentChat.ts
│ │ │ │ │ ├── useAgentChatViewRoute.ts
│ │ │ │ │ ├── useAgentInputPreferences.ts
│ │ │ │ │ ├── useAgentProjects.ts
│ │ │ │ │ ├── useAgentServer.ts
│ │ │ │ │ ├── useAgentSessions.ts
│ │ │ │ │ ├── useAgentTheme.ts
│ │ │ │ │ ├── useAgentThreads.ts
│ │ │ │ │ ├── useAttachments.ts
│ │ │ │ │ ├── useFakeCaret.ts
│ │ │ │ │ ├── useFloatingDrag.ts
│ │ │ │ │ ├── useOpenProjectPreference.ts
│ │ │ │ │ ├── useRRV3Debugger.ts
│ │ │ │ │ ├── useRRV3Rpc.ts
│ │ │ │ │ ├── useTextareaAutoResize.ts
│ │ │ │ │ ├── useWebEditorTxState.ts
│ │ │ │ │ └── useWorkflowsV3.ts
│ │ │ │ ├── index.html
│ │ │ │ ├── main.ts
│ │ │ │ ├── styles
│ │ │ │ │ └── agent-chat.css
│ │ │ │ └── utils
│ │ │ │ └── loading-texts.ts
│ │ │ ├── styles
│ │ │ │ └── tailwind.css
│ │ │ ├── web-editor-v2
│ │ │ │ ├── attr-ui-refactor.md
│ │ │ │ ├── constants.ts
│ │ │ │ ├── core
│ │ │ │ │ ├── css-compare.ts
│ │ │ │ │ ├── cssom-styles-collector.ts
│ │ │ │ │ ├── debug-source.ts
│ │ │ │ │ ├── design-tokens
│ │ │ │ │ │ ├── design-tokens-service.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── token-detector.ts
│ │ │ │ │ │ ├── token-resolver.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── editor.ts
│ │ │ │ │ ├── element-key.ts
│ │ │ │ │ ├── event-controller.ts
│ │ │ │ │ ├── execution-tracker.ts
│ │ │ │ │ ├── hmr-consistency.ts
│ │ │ │ │ ├── locator.ts
│ │ │ │ │ ├── message-listener.ts
│ │ │ │ │ ├── payload-builder.ts
│ │ │ │ │ ├── perf-monitor.ts
│ │ │ │ │ ├── position-tracker.ts
│ │ │ │ │ ├── props-bridge.ts
│ │ │ │ │ ├── snap-engine.ts
│ │ │ │ │ ├── transaction-aggregator.ts
│ │ │ │ │ └── transaction-manager.ts
│ │ │ │ ├── drag
│ │ │ │ │ └── drag-reorder-controller.ts
│ │ │ │ ├── overlay
│ │ │ │ │ ├── canvas-overlay.ts
│ │ │ │ │ └── handles-controller.ts
│ │ │ │ ├── selection
│ │ │ │ │ └── selection-engine.ts
│ │ │ │ ├── ui
│ │ │ │ │ ├── breadcrumbs.ts
│ │ │ │ │ ├── floating-drag.ts
│ │ │ │ │ ├── icons.ts
│ │ │ │ │ ├── property-panel
│ │ │ │ │ │ ├── class-editor.ts
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ ├── alignment-grid.ts
│ │ │ │ │ │ │ ├── icon-button-group.ts
│ │ │ │ │ │ │ ├── input-container.ts
│ │ │ │ │ │ │ ├── slider-input.ts
│ │ │ │ │ │ │ └── token-pill.ts
│ │ │ │ │ │ ├── components-tree.ts
│ │ │ │ │ │ ├── controls
│ │ │ │ │ │ │ ├── appearance-control.ts
│ │ │ │ │ │ │ ├── background-control.ts
│ │ │ │ │ │ │ ├── border-control.ts
│ │ │ │ │ │ │ ├── color-field.ts
│ │ │ │ │ │ │ ├── css-helpers.ts
│ │ │ │ │ │ │ ├── effects-control.ts
│ │ │ │ │ │ │ ├── gradient-control.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── layout-control.ts
│ │ │ │ │ │ │ ├── number-stepping.ts
│ │ │ │ │ │ │ ├── position-control.ts
│ │ │ │ │ │ │ ├── size-control.ts
│ │ │ │ │ │ │ ├── spacing-control.ts
│ │ │ │ │ │ │ ├── token-picker.ts
│ │ │ │ │ │ │ └── typography-control.ts
│ │ │ │ │ │ ├── css-defaults.ts
│ │ │ │ │ │ ├── css-panel.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── property-panel.ts
│ │ │ │ │ │ ├── props-panel.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── shadow-host.ts
│ │ │ │ │ └── toolbar.ts
│ │ │ │ └── utils
│ │ │ │ └── disposables.ts
│ │ │ ├── web-editor-v2.ts
│ │ │ └── welcome
│ │ │ ├── App.vue
│ │ │ ├── index.html
│ │ │ └── main.ts
│ │ ├── env.d.ts
│ │ ├── eslint.config.js
│ │ ├── inject-scripts
│ │ │ ├── accessibility-tree-helper.js
│ │ │ ├── click-helper.js
│ │ │ ├── dom-observer.js
│ │ │ ├── element-marker.js
│ │ │ ├── element-picker.js
│ │ │ ├── fill-helper.js
│ │ │ ├── inject-bridge.js
│ │ │ ├── interactive-elements-helper.js
│ │ │ ├── keyboard-helper.js
│ │ │ ├── network-helper.js
│ │ │ ├── props-agent.js
│ │ │ ├── recorder.js
│ │ │ ├── screenshot-helper.js
│ │ │ ├── wait-helper.js
│ │ │ ├── web-editor.js
│ │ │ └── web-fetcher-helper.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── public
│ │ │ ├── icon
│ │ │ │ ├── 128.png
│ │ │ │ ├── 16.png
│ │ │ │ ├── 32.png
│ │ │ │ ├── 48.png
│ │ │ │ └── 96.png
│ │ │ ├── libs
│ │ │ │ └── ort.min.js
│ │ │ └── wxt.svg
│ │ ├── README.md
│ │ ├── shared
│ │ │ ├── element-picker
│ │ │ │ ├── controller.ts
│ │ │ │ └── index.ts
│ │ │ ├── quick-panel
│ │ │ │ ├── core
│ │ │ │ │ ├── agent-bridge.ts
│ │ │ │ │ ├── search-engine.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── providers
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── tabs-provider.ts
│ │ │ │ └── ui
│ │ │ │ ├── ai-chat-panel.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── markdown-renderer.ts
│ │ │ │ ├── message-renderer.ts
│ │ │ │ ├── panel-shell.ts
│ │ │ │ ├── quick-entries.ts
│ │ │ │ ├── search-input.ts
│ │ │ │ ├── shadow-host.ts
│ │ │ │ └── styles.ts
│ │ │ └── selector
│ │ │ ├── dom-path.ts
│ │ │ ├── fingerprint.ts
│ │ │ ├── generator.ts
│ │ │ ├── index.ts
│ │ │ ├── locator.ts
│ │ │ ├── shadow-dom.ts
│ │ │ ├── stability.ts
│ │ │ ├── strategies
│ │ │ │ ├── anchor-relpath.ts
│ │ │ │ ├── aria.ts
│ │ │ │ ├── css-path.ts
│ │ │ │ ├── css-unique.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── testid.ts
│ │ │ │ └── text.ts
│ │ │ └── types.ts
│ │ ├── tailwind.config.ts
│ │ ├── tests
│ │ │ ├── __mocks__
│ │ │ │ └── hnswlib-wasm-static.ts
│ │ │ ├── record-replay
│ │ │ │ ├── _test-helpers.ts
│ │ │ │ ├── adapter-policy.contract.test.ts
│ │ │ │ ├── flow-store-strip-steps.contract.test.ts
│ │ │ │ ├── high-risk-actions.integration.test.ts
│ │ │ │ ├── hybrid-actions.integration.test.ts
│ │ │ │ ├── script-control-flow.integration.test.ts
│ │ │ │ ├── session-dag-sync.contract.test.ts
│ │ │ │ ├── step-executor.contract.test.ts
│ │ │ │ └── tab-cursor.integration.test.ts
│ │ │ ├── record-replay-v3
│ │ │ │ ├── command-trigger.test.ts
│ │ │ │ ├── context-menu-trigger.test.ts
│ │ │ │ ├── cron-trigger.test.ts
│ │ │ │ ├── debugger.contract.test.ts
│ │ │ │ ├── dom-trigger.test.ts
│ │ │ │ ├── e2e.integration.test.ts
│ │ │ │ ├── events.contract.test.ts
│ │ │ │ ├── interval-trigger.test.ts
│ │ │ │ ├── manual-trigger.test.ts
│ │ │ │ ├── once-trigger.test.ts
│ │ │ │ ├── queue.contract.test.ts
│ │ │ │ ├── recovery.test.ts
│ │ │ │ ├── rpc-api.test.ts
│ │ │ │ ├── runner.onError.contract.test.ts
│ │ │ │ ├── scheduler-integration.test.ts
│ │ │ │ ├── scheduler.test.ts
│ │ │ │ ├── spec-smoke.test.ts
│ │ │ │ ├── trigger-manager.test.ts
│ │ │ │ ├── triggers.test.ts
│ │ │ │ ├── url-trigger.test.ts
│ │ │ │ ├── v2-action-adapter.test.ts
│ │ │ │ ├── v2-adapter-integration.test.ts
│ │ │ │ ├── v2-to-v3-conversion.test.ts
│ │ │ │ └── v3-e2e-harness.ts
│ │ │ ├── vitest.setup.ts
│ │ │ └── web-editor-v2
│ │ │ ├── design-tokens.test.ts
│ │ │ ├── drag-reorder-controller.test.ts
│ │ │ ├── event-controller.test.ts
│ │ │ ├── locator.test.ts
│ │ │ ├── property-panel-live-sync.test.ts
│ │ │ ├── selection-engine.test.ts
│ │ │ ├── snap-engine.test.ts
│ │ │ └── test-utils
│ │ │ └── dom.ts
│ │ ├── tsconfig.json
│ │ ├── types
│ │ │ ├── gifenc.d.ts
│ │ │ └── icons.d.ts
│ │ ├── utils
│ │ │ ├── cdp-session-manager.ts
│ │ │ ├── content-indexer.ts
│ │ │ ├── i18n.ts
│ │ │ ├── image-utils.ts
│ │ │ ├── indexeddb-client.ts
│ │ │ ├── lru-cache.ts
│ │ │ ├── model-cache-manager.ts
│ │ │ ├── offscreen-manager.ts
│ │ │ ├── output-sanitizer.ts
│ │ │ ├── screenshot-context.ts
│ │ │ ├── semantic-similarity-engine.ts
│ │ │ ├── simd-math-engine.ts
│ │ │ ├── text-chunker.ts
│ │ │ └── vector-database.ts
│ │ ├── vitest.config.ts
│ │ ├── workers
│ │ │ ├── ort-wasm-simd-threaded.jsep.mjs
│ │ │ ├── ort-wasm-simd-threaded.jsep.wasm
│ │ │ ├── ort-wasm-simd-threaded.mjs
│ │ │ ├── ort-wasm-simd-threaded.wasm
│ │ │ ├── simd_math_bg.wasm
│ │ │ ├── simd_math.js
│ │ │ └── similarity.worker.js
│ │ └── wxt.config.ts
│ └── native-server
│ ├── .npmignore
│ ├── debug.sh
│ ├── install.md
│ ├── jest.config.js
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── agent
│ │ │ ├── attachment-service.ts
│ │ │ ├── ccr-detector.ts
│ │ │ ├── chat-service.ts
│ │ │ ├── db
│ │ │ │ ├── client.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── schema.ts
│ │ │ ├── directory-picker.ts
│ │ │ ├── engines
│ │ │ │ ├── claude.ts
│ │ │ │ ├── codex.ts
│ │ │ │ └── types.ts
│ │ │ ├── message-service.ts
│ │ │ ├── open-project.ts
│ │ │ ├── project-service.ts
│ │ │ ├── project-types.ts
│ │ │ ├── session-service.ts
│ │ │ ├── storage.ts
│ │ │ ├── stream-manager.ts
│ │ │ ├── tool-bridge.ts
│ │ │ └── types.ts
│ │ ├── cli.ts
│ │ ├── constant
│ │ │ └── index.ts
│ │ ├── file-handler.ts
│ │ ├── index.ts
│ │ ├── mcp
│ │ │ ├── mcp-server-stdio.ts
│ │ │ ├── mcp-server.ts
│ │ │ ├── register-tools.ts
│ │ │ └── stdio-config.json
│ │ ├── native-messaging-host.ts
│ │ ├── scripts
│ │ │ ├── browser-config.ts
│ │ │ ├── build.ts
│ │ │ ├── constant.ts
│ │ │ ├── doctor.ts
│ │ │ ├── postinstall.ts
│ │ │ ├── register-dev.ts
│ │ │ ├── register.ts
│ │ │ ├── report.ts
│ │ │ ├── run_host.bat
│ │ │ ├── run_host.sh
│ │ │ └── utils.ts
│ │ ├── server
│ │ │ ├── index.ts
│ │ │ ├── routes
│ │ │ │ ├── agent.ts
│ │ │ │ └── index.ts
│ │ │ └── server.test.ts
│ │ ├── shims
│ │ │ └── devtools.d.ts
│ │ ├── trace-analyzer.ts
│ │ ├── types
│ │ │ └── devtools-frontend.d.ts
│ │ └── util
│ │ └── logger.ts
│ └── tsconfig.json
├── commitlint.config.cjs
├── docs
│ ├── ARCHITECTURE_zh.md
│ ├── ARCHITECTURE.md
│ ├── CHANGELOG.md
│ ├── CONTRIBUTING_zh.md
│ ├── CONTRIBUTING.md
│ ├── ISSUE.md
│ ├── mcp-cli-config.md
│ ├── TOOLS_zh.md
│ ├── TOOLS.md
│ ├── TROUBLESHOOTING_zh.md
│ ├── TROUBLESHOOTING.md
│ ├── VisualEditor_zh.md
│ ├── VisualEditor.md
│ └── WINDOWS_INSTALL_zh.md
├── eslint.config.js
├── LICENSE
├── package.json
├── packages
│ ├── shared
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── agent-types.ts
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── labels.ts
│ │ │ ├── node-spec-registry.ts
│ │ │ ├── node-spec.ts
│ │ │ ├── node-specs-builtin.ts
│ │ │ ├── rr-graph.ts
│ │ │ ├── step-types.ts
│ │ │ ├── tools.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ └── wasm-simd
│ ├── .gitignore
│ ├── BUILD.md
│ ├── Cargo.toml
│ ├── package.json
│ ├── README.md
│ └── src
│ └── lib.rs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── prompt
│ ├── content-analize.md
│ ├── excalidraw-prompt.md
│ └── modify-web.md
├── README_zh.md
├── README.md
└── releases
├── chrome-extension
│ └── latest
│ └── chrome-mcp-server-lastest.zip
└── README.md
```
# Files
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyLoopElements.vue:
--------------------------------------------------------------------------------
```vue
1 | <template>
2 | <div class="form-section">
3 | <div class="form-group">
4 | <label class="form-label">元素选择器</label>
5 | <input class="form-input" v-model="(node as any).config.selector" placeholder="CSS 选择器" />
6 | </div>
7 | <div class="form-group">
8 | <label class="form-label">列表变量名</label>
9 | <input class="form-input" v-model="(node as any).config.saveAs" placeholder="默认 elements" />
10 | </div>
11 | <div class="form-group">
12 | <label class="form-label">循环项变量名</label>
13 | <input class="form-input" v-model="(node as any).config.itemVar" placeholder="默认 item" />
14 | </div>
15 | <div class="form-group">
16 | <label class="form-label">子流 ID</label>
17 | <input
18 | class="form-input"
19 | v-model="(node as any).config.subflowId"
20 | placeholder="选择或新建子流"
21 | />
22 | <button class="btn-sm" style="margin-top: 8px" @click="onCreateSubflow">新建子流</button>
23 | </div>
24 | </div>
25 | </template>
26 |
27 | <script lang="ts" setup>
28 | /* eslint-disable vue/no-mutating-props */
29 | import type { NodeBase } from '@/entrypoints/background/record-replay/types';
30 |
31 | const props = defineProps<{ node: NodeBase }>();
32 | const emit = defineEmits<{ (e: 'create-subflow', id: string): void }>();
33 |
34 | function onCreateSubflow() {
35 | const id = prompt('请输入新子流ID');
36 | if (!id) return;
37 | emit('create-subflow', id);
38 | const n = props.node as any;
39 | if (n && n.config) n.config.subflowId = id;
40 | }
41 | </script>
42 |
43 | <style scoped></style>
44 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/window.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createErrorResponse, ToolResult } from '@/common/tool-handler';
2 | import { BaseBrowserToolExecutor } from '../base-browser';
3 | import { TOOL_NAMES } from 'chrome-mcp-shared';
4 |
5 | class WindowTool extends BaseBrowserToolExecutor {
6 | name = TOOL_NAMES.BROWSER.GET_WINDOWS_AND_TABS;
7 | async execute(): Promise<ToolResult> {
8 | try {
9 | const windows = await chrome.windows.getAll({ populate: true });
10 | let tabCount = 0;
11 |
12 | const structuredWindows = windows.map((window) => {
13 | const tabs =
14 | window.tabs?.map((tab) => {
15 | tabCount++;
16 | return {
17 | tabId: tab.id || 0,
18 | url: tab.url || '',
19 | title: tab.title || '',
20 | active: tab.active || false,
21 | };
22 | }) || [];
23 |
24 | return {
25 | windowId: window.id || 0,
26 | tabs: tabs,
27 | };
28 | });
29 |
30 | const result = {
31 | windowCount: windows.length,
32 | tabCount: tabCount,
33 | windows: structuredWindows,
34 | };
35 |
36 | return {
37 | content: [
38 | {
39 | type: 'text',
40 | text: JSON.stringify(result),
41 | },
42 | ],
43 | isError: false,
44 | };
45 | } catch (error) {
46 | console.error('Error in WindowTool.execute:', error);
47 | return createErrorResponse(
48 | `Error getting windows and tabs information: ${error instanceof Error ? error.message : String(error)}`,
49 | );
50 | }
51 | }
52 | }
53 |
54 | export const windowTool = new WindowTool();
55 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyScript.vue:
--------------------------------------------------------------------------------
```vue
1 | <template>
2 | <div class="form-section">
3 | <div class="form-group">
4 | <label class="form-label">代码</label>
5 | <textarea
6 | class="form-textarea"
7 | v-model="(node as any).config.code"
8 | rows="6"
9 | placeholder="// your JS code"
10 | ></textarea>
11 | </div>
12 | <div class="form-group">
13 | <label class="form-label">执行环境</label>
14 | <select class="form-select" v-model="(node as any).config.world">
15 | <option value="ISOLATED">ISOLATED</option>
16 | <option value="MAIN">MAIN</option>
17 | </select>
18 | </div>
19 | <div class="form-group">
20 | <label class="form-label">执行时机</label>
21 | <select class="form-select" v-model="(node as any).config.when">
22 | <option value="before">before</option>
23 | <option value="after">after</option>
24 | </select>
25 | </div>
26 | <div class="form-group">
27 | <label class="form-label">保存为变量(可选)</label>
28 | <input class="form-input" v-model="(node as any).config.saveAs" placeholder="变量名" />
29 | </div>
30 | <div class="form-group">
31 | <label class="form-label">结果字段映射</label>
32 | <KeyValueEditor v-model="(node as any).config.assign" />
33 | </div>
34 | </div>
35 | </template>
36 |
37 | <script lang="ts" setup>
38 | /* eslint-disable vue/no-mutating-props */
39 | import type { NodeBase } from '@/entrypoints/background/record-replay/types';
40 | import KeyValueEditor from '@/entrypoints/popup/components/builder/components/KeyValueEditor.vue';
41 |
42 | defineProps<{ node: NodeBase }>();
43 | </script>
44 |
45 | <style scoped></style>
46 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/shared/selector/strategies/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Selector Strategies - Strategy exports and default configuration
3 | */
4 |
5 | import type { SelectorStrategy } from '../types';
6 | import { anchorRelpathStrategy } from './anchor-relpath';
7 | import { ariaStrategy } from './aria';
8 | import { cssPathStrategy } from './css-path';
9 | import { cssUniqueStrategy } from './css-unique';
10 | import { testIdStrategy } from './testid';
11 | import { textStrategy } from './text';
12 |
13 | /**
14 | * Default selector strategy list (ordered by priority).
15 | *
16 | * Strategy order:
17 | * 1. testid - Stable test attributes (data-testid, name, title, alt)
18 | * 2. aria - Accessibility attributes (aria-label, role)
19 | * 3. css-unique - Unique CSS selectors (id, class combinations)
20 | * 4. css-path - Structural path selector (nth-of-type)
21 | * 5. anchor-relpath - Anchor + relative path (fallback for elements without unique attrs)
22 | * 6. text - Text content selector (lowest priority)
23 | *
24 | * Note: Final candidate order is determined by stability scoring,
25 | * but strategy order affects which candidates are generated first.
26 | */
27 | export const DEFAULT_SELECTOR_STRATEGIES: ReadonlyArray<SelectorStrategy> = [
28 | testIdStrategy,
29 | ariaStrategy,
30 | cssUniqueStrategy,
31 | cssPathStrategy,
32 | anchorRelpathStrategy,
33 | textStrategy,
34 | ];
35 |
36 | export { anchorRelpathStrategy } from './anchor-relpath';
37 | export { ariaStrategy } from './aria';
38 | export { cssPathStrategy } from './css-path';
39 | export { cssUniqueStrategy } from './css-unique';
40 | export { testIdStrategy } from './testid';
41 | export { textStrategy } from './text';
42 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/loops.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { ExecCtx, ExecResult, NodeRuntime } from './types';
2 | import { ENGINE_CONSTANTS } from '../engine/constants';
3 |
4 | export const foreachNode: NodeRuntime<any> = {
5 | validate: (step) => {
6 | const s = step as any;
7 | const ok =
8 | typeof s.listVar === 'string' && s.listVar && typeof s.subflowId === 'string' && s.subflowId;
9 | return ok ? { ok } : { ok, errors: ['foreach: 需提供 listVar 与 subflowId'] };
10 | },
11 | run: async (_ctx: ExecCtx, step) => {
12 | const s: any = step;
13 | const itemVar = typeof s.itemVar === 'string' && s.itemVar ? s.itemVar : 'item';
14 | return {
15 | control: {
16 | kind: 'foreach',
17 | listVar: String(s.listVar),
18 | itemVar,
19 | subflowId: String(s.subflowId),
20 | concurrency: Math.max(
21 | 1,
22 | Math.min(ENGINE_CONSTANTS.MAX_FOREACH_CONCURRENCY, Number(s.concurrency ?? 1)),
23 | ),
24 | },
25 | } as ExecResult;
26 | },
27 | };
28 |
29 | export const whileNode: NodeRuntime<any> = {
30 | validate: (step) => {
31 | const s = step as any;
32 | const ok = !!s.condition && typeof s.subflowId === 'string' && s.subflowId;
33 | return ok ? { ok } : { ok, errors: ['while: 需提供 condition 与 subflowId'] };
34 | },
35 | run: async (_ctx: ExecCtx, step) => {
36 | const s: any = step;
37 | const max = Math.max(1, Math.min(10000, Number(s.maxIterations ?? 100)));
38 | return {
39 | control: {
40 | kind: 'while',
41 | condition: s.condition,
42 | subflowId: String(s.subflowId),
43 | maxIterations: max,
44 | },
45 | } as ExecResult;
46 | },
47 | };
48 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/record-replay.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createErrorResponse, ToolResult } from '@/common/tool-handler';
2 | import { TOOL_NAMES } from 'chrome-mcp-shared';
3 | import { listPublished } from '../record-replay/flow-store';
4 | import { getFlow } from '../record-replay/flow-store';
5 | import { runFlow } from '../record-replay/flow-runner';
6 |
7 | class FlowRunTool {
8 | name = TOOL_NAMES.RECORD_REPLAY.FLOW_RUN;
9 | async execute(args: any): Promise<ToolResult> {
10 | const {
11 | flowId,
12 | args: vars,
13 | tabTarget,
14 | refresh,
15 | captureNetwork,
16 | returnLogs,
17 | timeoutMs,
18 | startUrl,
19 | } = args || {};
20 | if (!flowId) return createErrorResponse('flowId is required');
21 | const flow = await getFlow(flowId);
22 | if (!flow) return createErrorResponse(`Flow not found: ${flowId}`);
23 | const result = await runFlow(flow, {
24 | tabTarget,
25 | refresh,
26 | captureNetwork,
27 | returnLogs,
28 | timeoutMs,
29 | startUrl,
30 | args: vars,
31 | });
32 | return {
33 | content: [
34 | {
35 | type: 'text',
36 | text: JSON.stringify(result),
37 | },
38 | ],
39 | isError: false,
40 | };
41 | }
42 | }
43 |
44 | class ListPublishedTool {
45 | name = TOOL_NAMES.RECORD_REPLAY.LIST_PUBLISHED;
46 | async execute(): Promise<ToolResult> {
47 | const list = await listPublished();
48 | return {
49 | content: [
50 | {
51 | type: 'text',
52 | text: JSON.stringify({ success: true, published: list }),
53 | },
54 | ],
55 | isError: false,
56 | };
57 | }
58 | }
59 |
60 | export const flowRunTool = new FlowRunTool();
61 | export const listPublishedFlowsTool = new ListPublishedTool();
62 |
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
1 | import globals from 'globals';
2 | import js from '@eslint/js';
3 | import tseslint from 'typescript-eslint';
4 | import eslintConfigPrettier from 'eslint-config-prettier';
5 |
6 | export default tseslint.config(
7 | // Global ignores first - these apply to all configurations
8 | {
9 | ignores: [
10 | 'node_modules/',
11 | 'dist/',
12 | '.output/',
13 | '.wxt/',
14 | 'logs/',
15 | '*.log',
16 | '.cache/',
17 | '.temp/',
18 | '.idea/',
19 | '.DS_Store',
20 | 'Thumbs.db',
21 | '*.zip',
22 | '*.tar.gz',
23 | 'stats.html',
24 | 'stats-*.json',
25 | 'pnpm-lock.yaml',
26 | '**/workers/**',
27 | 'app/**/workers/**',
28 | 'packages/**/workers/**',
29 | 'test-inject-script.js',
30 | ],
31 | },
32 |
33 | js.configs.recommended,
34 | ...tseslint.configs.recommended,
35 | // Global rule adjustments
36 | {
37 | // Allow intentionally empty catch blocks (common in extension code),
38 | // while keeping other empty blocks reported.
39 | rules: {
40 | 'no-empty': ['error', { allowEmptyCatch: true }],
41 | },
42 | },
43 | {
44 | files: ['app/**/*.{js,jsx,ts,tsx}', 'packages/**/*.{js,jsx,ts,tsx}'],
45 | ignores: ['**/workers/**'], // Additional ignores for this specific config
46 | languageOptions: {
47 | ecmaVersion: 2021,
48 | sourceType: 'module',
49 | parser: tseslint.parser,
50 | globals: {
51 | ...globals.node,
52 | ...globals.es2021,
53 | },
54 | },
55 |
56 | rules: {
57 | '@typescript-eslint/no-explicit-any': 'off',
58 | '@typescript-eslint/no-require-imports': 'off',
59 | '@typescript-eslint/no-unused-vars': 'off',
60 | },
61 | },
62 | eslintConfigPrettier,
63 | );
64 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/trigger-store.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { IndexedDbStorage, ensureMigratedFromLocal } from './storage/indexeddb-manager';
2 |
3 | export type TriggerType = 'url' | 'contextMenu' | 'command' | 'dom';
4 |
5 | export interface BaseTrigger {
6 | id: string;
7 | type: TriggerType;
8 | enabled: boolean;
9 | flowId: string;
10 | args?: Record<string, any>;
11 | }
12 |
13 | export interface UrlTrigger extends BaseTrigger {
14 | type: 'url';
15 | match: Array<{ kind: 'url' | 'domain' | 'path'; value: string }>;
16 | }
17 |
18 | export interface ContextMenuTrigger extends BaseTrigger {
19 | type: 'contextMenu';
20 | title: string;
21 | contexts?: chrome.contextMenus.ContextType[];
22 | }
23 |
24 | export interface CommandTrigger extends BaseTrigger {
25 | type: 'command';
26 | commandKey: string; // e.g., run_quick_trigger_1
27 | }
28 |
29 | export interface DomTrigger extends BaseTrigger {
30 | type: 'dom';
31 | selector: string;
32 | appear?: boolean; // default true
33 | once?: boolean; // default true
34 | debounceMs?: number; // default 800
35 | }
36 |
37 | export type FlowTrigger = UrlTrigger | ContextMenuTrigger | CommandTrigger | DomTrigger;
38 |
39 | export async function listTriggers(): Promise<FlowTrigger[]> {
40 | await ensureMigratedFromLocal();
41 | return await IndexedDbStorage.triggers.list();
42 | }
43 |
44 | export async function saveTrigger(t: FlowTrigger): Promise<void> {
45 | await ensureMigratedFromLocal();
46 | await IndexedDbStorage.triggers.save(t);
47 | }
48 |
49 | export async function deleteTrigger(id: string): Promise<void> {
50 | await ensureMigratedFromLocal();
51 | await IndexedDbStorage.triggers.delete(id);
52 | }
53 |
54 | export function toId(prefix = 'trg') {
55 | return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
56 | }
57 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export { navigateTool, closeTabsTool, switchTabTool } from './common';
2 | export { windowTool } from './window';
3 | export { vectorSearchTabsContentTool as searchTabsContentTool } from './vector-search';
4 | export { screenshotTool } from './screenshot';
5 | export { webFetcherTool, getInteractiveElementsTool } from './web-fetcher';
6 | export { clickTool, fillTool } from './interaction';
7 | export { elementPickerTool } from './element-picker';
8 | export { networkRequestTool } from './network-request';
9 | export { networkCaptureTool } from './network-capture';
10 | // Legacy exports (for internal use by networkCaptureTool)
11 | export { networkDebuggerStartTool, networkDebuggerStopTool } from './network-capture-debugger';
12 | export { networkCaptureStartTool, networkCaptureStopTool } from './network-capture-web-request';
13 | export { keyboardTool } from './keyboard';
14 | export { historyTool } from './history';
15 | export { bookmarkSearchTool, bookmarkAddTool, bookmarkDeleteTool } from './bookmark';
16 | export { injectScriptTool, sendCommandToInjectScriptTool } from './inject-script';
17 | export { javascriptTool } from './javascript';
18 | export { consoleTool } from './console';
19 | export { fileUploadTool } from './file-upload';
20 | export { readPageTool } from './read-page';
21 | export { computerTool } from './computer';
22 | export { handleDialogTool } from './dialog';
23 | export { handleDownloadTool } from './download';
24 | export { userscriptTool } from './userscript';
25 | export {
26 | performanceStartTraceTool,
27 | performanceStopTraceTool,
28 | performanceAnalyzeInsightTool,
29 | } from './performance';
30 | export { gifRecorderTool } from './gif-recorder';
31 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/web-editor-v2/core/design-tokens/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Design Tokens Module (Phase 5.4)
3 | *
4 | * Runtime CSS custom property detection, resolution, and application.
5 | *
6 | * Usage:
7 | * ```typescript
8 | * import { createDesignTokensService } from './core/design-tokens';
9 | *
10 | * const service = createDesignTokensService();
11 | *
12 | * // Get available tokens for an element
13 | * const { tokens } = service.getContextTokens(element);
14 | *
15 | * // Apply a token to a style property
16 | * service.applyTokenToStyle(transactionManager, element, 'color', '--color-primary');
17 | *
18 | * // Cleanup
19 | * service.dispose();
20 | * ```
21 | */
22 |
23 | // Main service
24 | export {
25 | createDesignTokensService,
26 | type DesignTokensService,
27 | type DesignTokensServiceOptions,
28 | type GetContextTokensOptions,
29 | type GetRootTokensOptions,
30 | } from './design-tokens-service';
31 |
32 | // Detector
33 | export {
34 | createTokenDetector,
35 | type TokenDetector,
36 | type TokenDetectorOptions,
37 | } from './token-detector';
38 |
39 | // Resolver
40 | export {
41 | createTokenResolver,
42 | type TokenResolver,
43 | type TokenResolverOptions,
44 | type ResolveForPropertyOptions,
45 | } from './token-resolver';
46 |
47 | // Types
48 | export type {
49 | // Core identifiers
50 | CssVarName,
51 | RootCacheKey,
52 | RootType,
53 | // Token classification
54 | TokenKind,
55 | // Declaration source
56 | StyleSheetRef,
57 | TokenDeclarationOrigin,
58 | TokenDeclaration,
59 | // Token model
60 | DesignToken,
61 | // Index and query
62 | TokenIndexStats,
63 | TokenIndex,
64 | ContextToken,
65 | TokenQueryResult,
66 | // Resolution
67 | CssVarReference,
68 | TokenAvailability,
69 | TokenResolutionMethod,
70 | TokenResolution,
71 | TokenResolvedForProperty,
72 | // Cache invalidation
73 | TokenInvalidationReason,
74 | TokenInvalidationEvent,
75 | Unsubscribe,
76 | } from './types';
77 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/ProgressIndicator.vue:
--------------------------------------------------------------------------------
```vue
1 | <template>
2 | <div v-if="visible" class="progress-section">
3 | <div class="progress-indicator">
4 | <div class="spinner" v-if="showSpinner"></div>
5 | <span class="progress-text">{{ text }}</span>
6 | </div>
7 | </div>
8 | </template>
9 |
10 | <script lang="ts" setup>
11 | interface Props {
12 | visible?: boolean;
13 | text: string;
14 | showSpinner?: boolean;
15 | }
16 |
17 | withDefaults(defineProps<Props>(), {
18 | visible: true,
19 | showSpinner: true,
20 | });
21 | </script>
22 |
23 | <style scoped>
24 | .progress-section {
25 | margin-top: 16px;
26 | animation: slideIn 0.3s ease-out;
27 | }
28 |
29 | .progress-indicator {
30 | display: flex;
31 | align-items: center;
32 | gap: 12px;
33 | padding: 16px;
34 | background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
35 | border-radius: 8px;
36 | border-left: 4px solid #667eea;
37 | backdrop-filter: blur(10px);
38 | border: 1px solid rgba(102, 126, 234, 0.2);
39 | }
40 |
41 | .spinner {
42 | width: 20px;
43 | height: 20px;
44 | border: 3px solid rgba(102, 126, 234, 0.2);
45 | border-top: 3px solid #667eea;
46 | border-radius: 50%;
47 | animation: spin 1s linear infinite;
48 | flex-shrink: 0;
49 | }
50 |
51 | @keyframes spin {
52 | 0% {
53 | transform: rotate(0deg);
54 | }
55 | 100% {
56 | transform: rotate(360deg);
57 | }
58 | }
59 |
60 | .progress-text {
61 | font-size: 14px;
62 | color: #4a5568;
63 | font-weight: 500;
64 | line-height: 1.4;
65 | }
66 |
67 | @keyframes slideIn {
68 | from {
69 | opacity: 0;
70 | transform: translateY(10px);
71 | }
72 | to {
73 | opacity: 1;
74 | transform: translateY(0);
75 | }
76 | }
77 |
78 | /* 响应式设计 */
79 | @media (max-width: 420px) {
80 | .progress-indicator {
81 | padding: 12px;
82 | gap: 8px;
83 | }
84 |
85 | .spinner {
86 | width: 16px;
87 | height: 16px;
88 | border-width: 2px;
89 | }
90 |
91 | .progress-text {
92 | font-size: 13px;
93 | }
94 | }
95 | </style>
96 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "chrome-mcp-server",
3 | "description": "a chrome extension to use your own chrome as a mcp server",
4 | "author": "hangye",
5 | "private": true,
6 | "version": "1.0.0",
7 | "type": "module",
8 | "scripts": {
9 | "dev": "wxt",
10 | "dev:firefox": "wxt -b firefox",
11 | "build": "wxt build",
12 | "build:firefox": "wxt build -b firefox",
13 | "zip": "wxt zip",
14 | "zip:firefox": "wxt zip -b firefox",
15 | "compile": "vue-tsc --noEmit",
16 | "postinstall": "wxt prepare",
17 | "lint": "npx eslint .",
18 | "lint:fix": "npx eslint . --fix",
19 | "format": "npx prettier --write .",
20 | "format:check": "npx prettier --check .",
21 | "test": "vitest run",
22 | "test:watch": "vitest"
23 | },
24 | "dependencies": {
25 | "@modelcontextprotocol/sdk": "^1.11.0",
26 | "@vue-flow/background": "^1.3.2",
27 | "@vue-flow/controls": "^1.1.3",
28 | "@vue-flow/core": "^1.47.0",
29 | "@vue-flow/minimap": "^1.5.4",
30 | "@xenova/transformers": "^2.17.2",
31 | "chrome-mcp-shared": "workspace:*",
32 | "date-fns": "^4.1.0",
33 | "elkjs": "^0.11.0",
34 | "gifenc": "^1.0.3",
35 | "hnswlib-wasm-static": "0.8.5",
36 | "markstream-vue": "0.0.3-beta.5",
37 | "vue": "^3.5.13",
38 | "zod": "^3.24.4"
39 | },
40 | "devDependencies": {
41 | "@iconify-json/lucide": "^1.1.0",
42 | "@tailwindcss/vite": "^4.0.0",
43 | "@types/chrome": "^0.0.318",
44 | "@wxt-dev/module-vue": "^1.0.2",
45 | "dotenv": "^16.5.0",
46 | "fake-indexeddb": "^6.2.5",
47 | "jsdom": "^26.0.0",
48 | "tailwindcss": "^4.0.0",
49 | "unplugin-icons": "^0.19.0",
50 | "unplugin-vue-components": "^0.27.5",
51 | "vite-plugin-static-copy": "^3.0.0",
52 | "vitest": "^2.1.8",
53 | "vue-tsc": "^2.2.8",
54 | "wxt": "^0.20.0"
55 | }
56 | }
57 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/plugins/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Plugin system for record-replay engine
2 | // Inspired by webpack-like lifecycle hooks, to avoid touching core for extensibility
3 |
4 | import type { Flow, Step } from '../../types';
5 | import type { ExecResult } from '../../nodes';
6 |
7 | export interface RunContext {
8 | runId: string;
9 | flow: Flow;
10 | vars: Record<string, any>;
11 | }
12 |
13 | export interface StepContext extends RunContext {
14 | step: Step;
15 | }
16 |
17 | export interface StepErrorContext extends StepContext {
18 | error: any;
19 | }
20 |
21 | export interface StepRetryContext extends StepErrorContext {
22 | attempt: number;
23 | }
24 |
25 | export interface StepAfterContext extends StepContext {
26 | result?: ExecResult;
27 | }
28 |
29 | export interface SubflowContext extends RunContext {
30 | subflowId: string;
31 | }
32 |
33 | export interface RunEndContext extends RunContext {
34 | success: boolean;
35 | failed: number;
36 | }
37 |
38 | export interface HookControl {
39 | pause?: boolean; // request scheduler to pause run (e.g., breakpoint)
40 | nextLabel?: string; // override next edge label
41 | }
42 |
43 | export interface RunPlugin {
44 | name: string;
45 | onRunStart?(ctx: RunContext): Promise<void> | void;
46 | onBeforeStep?(ctx: StepContext): Promise<HookControl | void> | HookControl | void;
47 | onAfterStep?(ctx: StepAfterContext): Promise<void> | void;
48 | onStepError?(ctx: StepErrorContext): Promise<HookControl | void> | HookControl | void;
49 | onRetry?(ctx: StepRetryContext): Promise<void> | void;
50 | onChooseNextLabel?(
51 | ctx: StepContext & { suggested?: string },
52 | ): Promise<HookControl | void> | HookControl | void;
53 | onSubflowStart?(ctx: SubflowContext): Promise<void> | void;
54 | onSubflowEnd?(ctx: SubflowContext): Promise<void> | void;
55 | onRunEnd?(ctx: RunEndContext): Promise<void> | void;
56 | }
57 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/web-editor-v2.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Web Editor V2 - Inject Script Entry Point
3 | *
4 | * This is the main entry point for the visual editor, injected into web pages
5 | * via chrome.scripting.executeScript from the background script.
6 | *
7 | * Architecture:
8 | * - Uses WXT's defineUnlistedScript for TypeScript compilation
9 | * - Exposes API on window.__MCP_WEB_EDITOR_V2__
10 | * - Communicates with background via chrome.runtime.onMessage
11 | *
12 | * Module structure:
13 | * - web-editor-v2/constants.ts - Configuration values
14 | * - web-editor-v2/utils/disposables.ts - Resource cleanup
15 | * - web-editor-v2/ui/shadow-host.ts - Shadow DOM isolation
16 | * - web-editor-v2/core/editor.ts - Main orchestrator
17 | * - web-editor-v2/core/message-listener.ts - Background communication
18 | *
19 | * Build output: .output/chrome-mv3/web-editor-v2.js
20 | */
21 |
22 | import { WEB_EDITOR_V2_LOG_PREFIX } from './web-editor-v2/constants';
23 | import { createWebEditorV2 } from './web-editor-v2/core/editor';
24 | import { installMessageListener } from './web-editor-v2/core/message-listener';
25 |
26 | export default defineUnlistedScript(() => {
27 | // Phase 1: Only support top frame
28 | // Phase 4 will add iframe support via content injection
29 | if (window !== window.top) {
30 | return;
31 | }
32 |
33 | // Singleton guard: prevent multiple instances
34 | if (window.__MCP_WEB_EDITOR_V2__) {
35 | console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Already installed, skipping initialization`);
36 | return;
37 | }
38 |
39 | // Create and expose the API
40 | const api = createWebEditorV2();
41 | window.__MCP_WEB_EDITOR_V2__ = api;
42 |
43 | // Install message listener for background communication
44 | installMessageListener(api);
45 |
46 | console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Installed successfully`);
47 | });
48 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/extract.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { StepExtract } from '../types';
2 | import { expandTemplatesDeep } from '../rr-utils';
3 | import type { ExecCtx, ExecResult, NodeRuntime } from './types';
4 |
5 | export const extractNode: NodeRuntime<StepExtract> = {
6 | run: async (ctx: ExecCtx, step: StepExtract) => {
7 | const s: any = expandTemplatesDeep(step as any, ctx.vars);
8 | const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
9 | const tabId = tabs?.[0]?.id;
10 | if (typeof tabId !== 'number') throw new Error('Active tab not found');
11 | let value: any = null;
12 | if (s.js && String(s.js).trim()) {
13 | const [{ result }] = await chrome.scripting.executeScript({
14 | target: { tabId },
15 | func: (code: string) => {
16 | try {
17 | return (0, eval)(code);
18 | } catch (e) {
19 | return null;
20 | }
21 | },
22 | args: [String(s.js)],
23 | } as any);
24 | value = result;
25 | } else if (s.selector) {
26 | const attr = String(s.attr || 'text');
27 | const sel = String(s.selector);
28 | const [{ result }] = await chrome.scripting.executeScript({
29 | target: { tabId },
30 | func: (selector: string, attr: string) => {
31 | try {
32 | const el = document.querySelector(selector) as any;
33 | if (!el) return null;
34 | if (attr === 'text' || attr === 'textContent') return (el.textContent || '').trim();
35 | return el.getAttribute ? el.getAttribute(attr) : null;
36 | } catch {
37 | return null;
38 | }
39 | },
40 | args: [sel, attr],
41 | } as any);
42 | value = result;
43 | }
44 | if (s.saveAs) ctx.vars[s.saveAs] = value;
45 | return {} as ExecResult;
46 | },
47 | };
48 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/utils/screenshot-context.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Simple in-memory screenshot context manager per tab
2 | // Used to scale coordinates from screenshot space to viewport space
3 |
4 | export interface ScreenshotContext {
5 | // Final screenshot dimensions (in CSS pixels after any scaling)
6 | screenshotWidth: number;
7 | screenshotHeight: number;
8 | // Viewport dimensions (CSS pixels)
9 | viewportWidth: number;
10 | viewportHeight: number;
11 | // Device pixel ratio at capture time (optional, for reference)
12 | devicePixelRatio?: number;
13 | // Hostname of the page when the screenshot was taken (used for domain safety checks)
14 | hostname?: string;
15 | // Timestamp
16 | timestamp: number;
17 | }
18 |
19 | const TTL_MS = 5 * 60 * 1000; // 5 minutes
20 |
21 | const contexts = new Map<number, ScreenshotContext>();
22 |
23 | export const screenshotContextManager = {
24 | setContext(tabId: number, ctx: Omit<ScreenshotContext, 'timestamp'>) {
25 | contexts.set(tabId, { ...ctx, timestamp: Date.now() });
26 | },
27 | getContext(tabId: number): ScreenshotContext | undefined {
28 | const ctx = contexts.get(tabId);
29 | if (!ctx) return undefined;
30 | if (Date.now() - ctx.timestamp > TTL_MS) {
31 | contexts.delete(tabId);
32 | return undefined;
33 | }
34 | return ctx;
35 | },
36 | clear(tabId: number) {
37 | contexts.delete(tabId);
38 | },
39 | };
40 |
41 | // Scale screenshot-space coordinates (x,y) to viewport CSS pixels
42 | export function scaleCoordinates(
43 | x: number,
44 | y: number,
45 | ctx: ScreenshotContext,
46 | ): { x: number; y: number } {
47 | if (!ctx.screenshotWidth || !ctx.screenshotHeight || !ctx.viewportWidth || !ctx.viewportHeight) {
48 | return { x, y };
49 | }
50 | const sx = (x / ctx.screenshotWidth) * ctx.viewportWidth;
51 | const sy = (y / ctx.screenshotHeight) * ctx.viewportHeight;
52 | return { x: Math.round(sx), y: Math.round(sy) };
53 | }
54 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/types/gifenc.d.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Type declarations for gifenc library
3 | * @see https://github.com/mattdesl/gifenc
4 | */
5 |
6 | declare module 'gifenc' {
7 | export interface GIFEncoderOptions {
8 | auto?: boolean;
9 | }
10 |
11 | export interface WriteFrameOptions {
12 | palette: number[];
13 | delay?: number;
14 | transparent?: boolean;
15 | transparentIndex?: number;
16 | dispose?: number;
17 | }
18 |
19 | export interface GIFEncoder {
20 | writeFrame(
21 | index: Uint8Array | Uint8ClampedArray,
22 | width: number,
23 | height: number,
24 | options: WriteFrameOptions,
25 | ): void;
26 | finish(): void;
27 | bytes(): Uint8Array;
28 | bytesView(): Uint8Array;
29 | reset(): void;
30 | }
31 |
32 | export function GIFEncoder(options?: GIFEncoderOptions): GIFEncoder;
33 |
34 | export interface QuantizeOptions {
35 | format?: 'rgb565' | 'rgba4444' | 'rgb444';
36 | oneBitAlpha?: boolean | number;
37 | clearAlpha?: boolean;
38 | clearAlphaColor?: number;
39 | clearAlphaThreshold?: number;
40 | }
41 |
42 | export function quantize(
43 | rgba: Uint8Array | Uint8ClampedArray,
44 | maxColors: number,
45 | options?: QuantizeOptions,
46 | ): number[];
47 |
48 | export function applyPalette(
49 | rgba: Uint8Array | Uint8ClampedArray,
50 | palette: number[],
51 | format?: 'rgb565' | 'rgba4444' | 'rgb444',
52 | ): Uint8Array;
53 |
54 | export function nearestColorIndex(palette: number[], pixel: number[]): number;
55 |
56 | export function nearestColorIndexWithDistance(
57 | palette: number[],
58 | pixel: number[],
59 | ): [number, number];
60 |
61 | export function snapColorsToPalette(
62 | palette: number[],
63 | knownColors: number[][],
64 | threshold?: number,
65 | ): void;
66 |
67 | export function prequantize(
68 | rgba: Uint8Array | Uint8ClampedArray,
69 | options?: { roundRGB?: number; roundAlpha?: number; oneBitAlpha?: boolean | number },
70 | ): void;
71 | }
72 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyExecuteFlow.vue:
--------------------------------------------------------------------------------
```vue
1 | <template>
2 | <div class="form-section">
3 | <div class="form-group">
4 | <label class="form-label">目标工作流</label>
5 | <select class="form-select" v-model="(node as any).config.flowId">
6 | <option value="">请选择</option>
7 | <option v-for="f in flows" :key="f.id" :value="f.id">{{ f.name || f.id }}</option>
8 | </select>
9 | </div>
10 | <div class="form-group checkbox-group">
11 | <label class="checkbox-label"
12 | ><input type="checkbox" v-model="(node as any).config.inline" />
13 | 内联执行(共享上下文变量)</label
14 | >
15 | </div>
16 | <div class="form-group">
17 | <label class="form-label">传参 (JSON)</label>
18 | <textarea
19 | class="form-textarea"
20 | v-model="execArgsJson"
21 | rows="3"
22 | placeholder='{"k": "v"}'
23 | ></textarea>
24 | </div>
25 | </div>
26 | </template>
27 |
28 | <script lang="ts" setup>
29 | /* eslint-disable vue/no-mutating-props */
30 | import { computed, onMounted, ref } from 'vue';
31 | import type { NodeBase } from '@/entrypoints/background/record-replay/types';
32 | import { BACKGROUND_MESSAGE_TYPES } from '@/common/message-types';
33 |
34 | const props = defineProps<{ node: NodeBase }>();
35 |
36 | type FlowLite = { id: string; name?: string };
37 | const flows = ref<FlowLite[]>([]);
38 | onMounted(async () => {
39 | try {
40 | const res = await chrome.runtime.sendMessage({ type: BACKGROUND_MESSAGE_TYPES.RR_LIST_FLOWS });
41 | if (res && res.success) flows.value = res.flows || [];
42 | } catch {}
43 | });
44 |
45 | const execArgsJson = computed({
46 | get() {
47 | try {
48 | return JSON.stringify((props.node as any).config?.args || {}, null, 2);
49 | } catch {
50 | return '';
51 | }
52 | },
53 | set(v: string) {
54 | try {
55 | (props.node as any).config.args = v ? JSON.parse(v) : {};
56 | } catch {}
57 | },
58 | });
59 | </script>
60 |
61 | <style scoped></style>
62 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/drag.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { TOOL_NAMES } from 'chrome-mcp-shared';
2 | import { handleCallTool } from '@/entrypoints/background/tools';
3 | import type { StepDrag } from '../types';
4 | import { locateElement } from '../selector-engine';
5 | import type { ExecCtx, ExecResult, NodeRuntime } from './types';
6 |
7 | export const dragNode: NodeRuntime<StepDrag> = {
8 | run: async (_ctx, step: StepDrag) => {
9 | const s = step as StepDrag;
10 | const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
11 | const tabId = tabs?.[0]?.id;
12 | let startRef: string | undefined;
13 | let endRef: string | undefined;
14 | try {
15 | if (typeof tabId === 'number') {
16 | const locatedStart = await locateElement(tabId, (s as any).start);
17 | const locatedEnd = await locateElement(tabId, (s as any).end);
18 | startRef = (locatedStart as any)?.ref || (s as any).start.ref;
19 | endRef = (locatedEnd as any)?.ref || (s as any).end.ref;
20 | }
21 | } catch {}
22 | let startCoordinates: { x: number; y: number } | undefined;
23 | let endCoordinates: { x: number; y: number } | undefined;
24 | if ((!startRef || !endRef) && Array.isArray((s as any).path) && (s as any).path.length >= 2) {
25 | startCoordinates = { x: Number((s as any).path[0].x), y: Number((s as any).path[0].y) };
26 | const last = (s as any).path[(s as any).path.length - 1];
27 | endCoordinates = { x: Number(last.x), y: Number(last.y) };
28 | }
29 | const res = await handleCallTool({
30 | name: TOOL_NAMES.BROWSER.COMPUTER,
31 | args: {
32 | action: 'left_click_drag',
33 | startRef,
34 | ref: endRef,
35 | startCoordinates,
36 | coordinates: endCoordinates,
37 | },
38 | });
39 | if ((res as any).isError) throw new Error('drag failed');
40 | return {} as ExecResult;
41 | },
42 | };
43 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/components/agent-chat/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * AgentChat Components
3 | * Export all components for the redesigned AgentChat UI.
4 | */
5 | export { default as AgentChatShell } from './AgentChatShell.vue';
6 | export { default as AgentTopBar } from './AgentTopBar.vue';
7 | export { default as AgentComposer } from './AgentComposer.vue';
8 | export { default as WebEditorChanges } from './WebEditorChanges.vue';
9 | export { default as ElementChip } from './ElementChip.vue';
10 | export { default as SelectionChip } from './SelectionChip.vue';
11 | export { default as AgentConversation } from './AgentConversation.vue';
12 | export { default as AgentRequestThread } from './AgentRequestThread.vue';
13 | export { default as AgentTimeline } from './AgentTimeline.vue';
14 | export { default as AgentTimelineItem } from './AgentTimelineItem.vue';
15 | export { default as AgentSettingsMenu } from './AgentSettingsMenu.vue';
16 | export { default as AgentProjectMenu } from './AgentProjectMenu.vue';
17 | export { default as AgentSessionMenu } from './AgentSessionMenu.vue';
18 | export { default as AgentSessionSettingsPanel } from './AgentSessionSettingsPanel.vue';
19 | export { default as AgentSessionsView } from './AgentSessionsView.vue';
20 | export { default as AgentSessionListItem } from './AgentSessionListItem.vue';
21 | export { default as AgentOpenProjectMenu } from './AgentOpenProjectMenu.vue';
22 | export { default as FakeCaretOverlay } from './FakeCaretOverlay.vue';
23 |
24 | // Timeline step components
25 | export { default as TimelineNarrativeStep } from './timeline/TimelineNarrativeStep.vue';
26 | export { default as TimelineToolCallStep } from './timeline/TimelineToolCallStep.vue';
27 | export { default as TimelineToolResultCardStep } from './timeline/TimelineToolResultCardStep.vue';
28 | export { default as TimelineStatusStep } from './timeline/TimelineStatusStep.vue';
29 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/shared/quick-panel/ui/markdown-renderer.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Quick Panel Markdown Renderer
3 | *
4 | * Simple markdown renderer for Quick Panel.
5 | * Currently uses plain text rendering - markdown support to be added later
6 | * when proper Vue/content-script integration is resolved.
7 | */
8 |
9 | // ============================================================
10 | // Types
11 | // ============================================================
12 |
13 | export interface MarkdownRendererInstance {
14 | /** Update the markdown content */
15 | setContent: (content: string, isStreaming?: boolean) => void;
16 | /** Get current content */
17 | getContent: () => string;
18 | /** Dispose resources */
19 | dispose: () => void;
20 | }
21 |
22 | // ============================================================
23 | // Main Factory
24 | // ============================================================
25 |
26 | /**
27 | * Create a markdown renderer instance that mounts to a container element.
28 | * Currently renders as plain text - markdown support pending.
29 | *
30 | * @param container - The DOM element to render content into
31 | * @returns Markdown renderer instance with setContent and dispose methods
32 | */
33 | export function createMarkdownRenderer(container: HTMLElement): MarkdownRendererInstance {
34 | let currentContent = '';
35 |
36 | // Create a wrapper div for content
37 | const contentEl = document.createElement('div');
38 | contentEl.className = 'qp-markdown-content';
39 | container.appendChild(contentEl);
40 |
41 | return {
42 | setContent(newContent: string, _streaming = false) {
43 | currentContent = newContent;
44 | // For now, render as plain text with basic whitespace preservation
45 | contentEl.textContent = newContent;
46 | },
47 |
48 | getContent() {
49 | return currentContent;
50 | },
51 |
52 | dispose() {
53 | try {
54 | contentEl.remove();
55 | } catch {
56 | // Best-effort cleanup
57 | }
58 | },
59 | };
60 | }
61 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyWhile.vue:
--------------------------------------------------------------------------------
```vue
1 | <template>
2 | <div class="form-section">
3 | <div class="form-group">
4 | <label class="form-label">条件 (JSON)</label>
5 | <textarea
6 | class="form-textarea"
7 | v-model="whileJson"
8 | rows="3"
9 | placeholder='{"expression":"workflow.count < 3"}'
10 | ></textarea>
11 | </div>
12 | <div class="form-group">
13 | <label class="form-label">子流 ID</label>
14 | <input
15 | class="form-input"
16 | v-model="(node as any).config.subflowId"
17 | placeholder="选择或新建子流"
18 | />
19 | <button class="btn-sm" style="margin-top: 8px" @click="onCreateSubflow">新建子流</button>
20 | </div>
21 | <div class="form-group">
22 | <label class="form-label">最大迭代次数(可选)</label>
23 | <input
24 | class="form-input"
25 | type="number"
26 | min="0"
27 | v-model.number="(node as any).config.maxIterations"
28 | />
29 | </div>
30 | </div>
31 | </template>
32 |
33 | <script lang="ts" setup>
34 | /* eslint-disable vue/no-mutating-props */
35 | import { computed } from 'vue';
36 | import type { NodeBase } from '@/entrypoints/background/record-replay/types';
37 |
38 | const props = defineProps<{ node: NodeBase }>();
39 | const emit = defineEmits<{ (e: 'create-subflow', id: string): void }>();
40 |
41 | const whileJson = computed({
42 | get() {
43 | try {
44 | return JSON.stringify((props.node as any).config?.condition || {}, null, 2);
45 | } catch {
46 | return '';
47 | }
48 | },
49 | set(v: string) {
50 | try {
51 | (props.node as any).config = {
52 | ...((props.node as any).config || {}),
53 | condition: JSON.parse(v || '{}'),
54 | };
55 | } catch {}
56 | },
57 | });
58 |
59 | function onCreateSubflow() {
60 | const id = prompt('请输入新子流ID');
61 | if (!id) return;
62 | emit('create-subflow', id);
63 | const n = props.node as any;
64 | if (n && n.config) n.config.subflowId = id;
65 | }
66 | </script>
67 |
68 | <style scoped></style>
69 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/utils/sidepanel.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Sidepanel Utilities
3 | *
4 | * Shared helpers for opening and managing the Chrome sidepanel from background modules.
5 | * Used by web-editor, quick-panel, and other modules that need to trigger sidepanel navigation.
6 | */
7 |
8 | /**
9 | * Best-effort open the sidepanel with AgentChat tab selected.
10 | *
11 | * @param tabId - Tab ID to associate with sidepanel
12 | * @param windowId - Optional window ID for fallback when tab-level open fails
13 | * @param sessionId - Optional session ID to navigate directly to chat view (deep-link)
14 | *
15 | * @remarks
16 | * This function is intentionally resilient - it will not throw on failures.
17 | * Sidepanel availability varies across Chrome versions and contexts.
18 | */
19 | export async function openAgentChatSidepanel(
20 | tabId: number,
21 | windowId?: number,
22 | sessionId?: string,
23 | ): Promise<void> {
24 | try {
25 | // Build deep-link path with optional session navigation
26 | let path = 'sidepanel.html?tab=agent-chat';
27 | if (sessionId) {
28 | path += `&view=chat&sessionId=${encodeURIComponent(sessionId)}`;
29 | }
30 |
31 | // Configure sidepanel options for this tab
32 |
33 | const sidePanel = chrome.sidePanel as any;
34 |
35 | if (sidePanel?.setOptions) {
36 | await sidePanel.setOptions({
37 | tabId,
38 | path,
39 | enabled: true,
40 | });
41 | }
42 |
43 | // Attempt to open the sidepanel
44 | if (sidePanel?.open) {
45 | try {
46 | await sidePanel.open({ tabId });
47 | } catch {
48 | // Fallback to window-level open if tab-level fails
49 | // This handles cases where the tab is in a special state
50 | if (typeof windowId === 'number') {
51 | await sidePanel.open({ windowId });
52 | }
53 | }
54 | }
55 | } catch {
56 | // Best-effort: side panel may be unavailable in some Chrome versions/environments
57 | // Intentionally suppress errors to avoid breaking calling code
58 | }
59 | }
60 |
```
--------------------------------------------------------------------------------
/app/native-server/src/agent/storage.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Storage path helpers for agent-related state.
3 | *
4 | * Provides unified path resolution for:
5 | * - SQLite database file
6 | * - Data directory
7 | * - Default workspace directory
8 | *
9 | * All paths can be overridden via environment variables.
10 | */
11 | import os from 'node:os';
12 | import path from 'node:path';
13 |
14 | const DEFAULT_DATA_DIR = path.join(os.homedir(), '.chrome-mcp-agent');
15 |
16 | /**
17 | * Resolve base data directory for agent state.
18 | *
19 | * Environment:
20 | * - CHROME_MCP_AGENT_DATA_DIR: overrides the default base directory.
21 | */
22 | export function getAgentDataDir(): string {
23 | const raw = process.env.CHROME_MCP_AGENT_DATA_DIR;
24 | if (raw && raw.trim()) {
25 | return path.resolve(raw.trim());
26 | }
27 | return DEFAULT_DATA_DIR;
28 | }
29 |
30 | /**
31 | * Resolve database file path.
32 | *
33 | * Environment:
34 | * - CHROME_MCP_AGENT_DB_FILE: overrides the default database path.
35 | */
36 | export function getDatabasePath(): string {
37 | const raw = process.env.CHROME_MCP_AGENT_DB_FILE;
38 | if (raw && raw.trim()) {
39 | return path.resolve(raw.trim());
40 | }
41 | return path.join(getAgentDataDir(), 'agent.db');
42 | }
43 |
44 | /**
45 | * Get the default workspace directory for agent projects.
46 | * This is a subdirectory under the agent data directory.
47 | *
48 | * Cross-platform compatible:
49 | * - Mac/Linux: ~/.chrome-mcp-agent/workspaces
50 | * - Windows: %USERPROFILE%\.chrome-mcp-agent\workspaces
51 | */
52 | export function getDefaultWorkspaceDir(): string {
53 | return path.join(getAgentDataDir(), 'workspaces');
54 | }
55 |
56 | /**
57 | * Generate a default project root path for a given project name.
58 | */
59 | export function getDefaultProjectRoot(projectName: string): string {
60 | // Sanitize project name for use as directory name
61 | const safeName = projectName
62 | .trim()
63 | .toLowerCase()
64 | .replace(/[^a-z0-9_-]/g, '-')
65 | .replace(/-+/g, '-')
66 | .replace(/^-|-$/g, '');
67 | return path.join(getDefaultWorkspaceDir(), safeName || 'default-project');
68 | }
69 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/dialog.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createErrorResponse, ToolResult } from '@/common/tool-handler';
2 | import { BaseBrowserToolExecutor } from '../base-browser';
3 | import { TOOL_NAMES } from 'chrome-mcp-shared';
4 | import { cdpSessionManager } from '@/utils/cdp-session-manager';
5 |
6 | interface HandleDialogParams {
7 | action: 'accept' | 'dismiss';
8 | promptText?: string;
9 | }
10 |
11 | /**
12 | * Handle JavaScript dialogs (alert/confirm/prompt) via CDP Page.handleJavaScriptDialog
13 | */
14 | class HandleDialogTool extends BaseBrowserToolExecutor {
15 | name = TOOL_NAMES.BROWSER.HANDLE_DIALOG;
16 |
17 | async execute(args: HandleDialogParams): Promise<ToolResult> {
18 | const { action, promptText } = args || ({} as HandleDialogParams);
19 | if (!action || (action !== 'accept' && action !== 'dismiss')) {
20 | return createErrorResponse('action must be "accept" or "dismiss"');
21 | }
22 |
23 | try {
24 | const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
25 | if (!activeTab?.id) return createErrorResponse('No active tab found');
26 | const tabId = activeTab.id!;
27 |
28 | // Use shared CDP session manager for safe attach/detach with refcount
29 | await cdpSessionManager.withSession(tabId, 'dialog', async () => {
30 | await cdpSessionManager.sendCommand(tabId, 'Page.enable');
31 | await cdpSessionManager.sendCommand(tabId, 'Page.handleJavaScriptDialog', {
32 | accept: action === 'accept',
33 | promptText: action === 'accept' ? promptText : undefined,
34 | });
35 | });
36 |
37 | return {
38 | content: [
39 | {
40 | type: 'text',
41 | text: JSON.stringify({ success: true, action, promptText: promptText || null }),
42 | },
43 | ],
44 | isError: false,
45 | };
46 | } catch (error) {
47 | return createErrorResponse(
48 | `Failed to handle dialog: ${error instanceof Error ? error.message : String(error)}`,
49 | );
50 | }
51 | }
52 | }
53 |
54 | export const handleDialogTool = new HandleDialogTool();
55 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/shared/quick-panel/ui/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Quick Panel UI Module Index
3 | *
4 | * Exports all UI components for the Quick Panel feature.
5 | */
6 |
7 | // ============================================================
8 | // Shell (unified container for search + chat views)
9 | // ============================================================
10 |
11 | export {
12 | mountQuickPanelShell,
13 | type QuickPanelShellElements,
14 | type QuickPanelShellManager,
15 | type QuickPanelShellOptions,
16 | } from './panel-shell';
17 |
18 | // ============================================================
19 | // Shadow DOM host
20 | // ============================================================
21 |
22 | export {
23 | mountQuickPanelShadowHost,
24 | type QuickPanelShadowHostElements,
25 | type QuickPanelShadowHostManager,
26 | type QuickPanelShadowHostOptions,
27 | } from './shadow-host';
28 |
29 | // ============================================================
30 | // Search UI Components
31 | // ============================================================
32 |
33 | export {
34 | createSearchInput,
35 | type SearchInputManager,
36 | type SearchInputOptions,
37 | type SearchInputState,
38 | } from './search-input';
39 |
40 | export {
41 | createQuickEntries,
42 | type QuickEntriesManager,
43 | type QuickEntriesOptions,
44 | } from './quick-entries';
45 |
46 | // ============================================================
47 | // AI Chat Components
48 | // ============================================================
49 |
50 | export {
51 | createQuickPanelMessageRenderer,
52 | type QuickPanelMessageRenderer,
53 | type QuickPanelMessageRendererOptions,
54 | } from './message-renderer';
55 |
56 | export { createMarkdownRenderer, type MarkdownRendererInstance } from './markdown-renderer';
57 |
58 | export {
59 | mountQuickPanelAiChatPanel,
60 | type QuickPanelAiChatPanelManager,
61 | type QuickPanelAiChatPanelOptions,
62 | type QuickPanelAiChatPanelState,
63 | } from './ai-chat-panel';
64 |
65 | // ============================================================
66 | // Styles
67 | // ============================================================
68 |
69 | export { QUICK_PANEL_STYLES } from './styles';
70 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay-v3/engine/triggers/manual-trigger.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Manual Trigger Handler (P4-08)
3 | * @description
4 | * Manual triggers are the simplest trigger type - they don't auto-fire.
5 | * They're only triggered programmatically via RPC or UI.
6 | *
7 | * This handler just tracks installed triggers but doesn't set up any listeners.
8 | * Manual triggers are fired by calling TriggerManager's fire method directly.
9 | */
10 |
11 | import type { TriggerId } from '../../domain/ids';
12 | import type { TriggerSpecByKind } from '../../domain/triggers';
13 | import type { TriggerFireCallback, TriggerHandler, TriggerHandlerFactory } from './trigger-handler';
14 |
15 | // ==================== Types ====================
16 |
17 | export interface ManualTriggerHandlerDeps {
18 | logger?: Pick<Console, 'debug' | 'info' | 'warn' | 'error'>;
19 | }
20 |
21 | type ManualTriggerSpec = TriggerSpecByKind<'manual'>;
22 |
23 | // ==================== Handler Implementation ====================
24 |
25 | /**
26 | * Create manual trigger handler factory
27 | */
28 | export function createManualTriggerHandlerFactory(
29 | deps?: ManualTriggerHandlerDeps,
30 | ): TriggerHandlerFactory<'manual'> {
31 | return (fireCallback) => createManualTriggerHandler(fireCallback, deps);
32 | }
33 |
34 | /**
35 | * Create manual trigger handler
36 | *
37 | * Manual triggers don't auto-fire - they're only triggered via RPC.
38 | * This handler just tracks which manual triggers are installed.
39 | */
40 | export function createManualTriggerHandler(
41 | _fireCallback: TriggerFireCallback,
42 | _deps?: ManualTriggerHandlerDeps,
43 | ): TriggerHandler<'manual'> {
44 | const installed = new Map<TriggerId, ManualTriggerSpec>();
45 |
46 | return {
47 | kind: 'manual',
48 |
49 | async install(trigger: ManualTriggerSpec): Promise<void> {
50 | installed.set(trigger.id, trigger);
51 | },
52 |
53 | async uninstall(triggerId: string): Promise<void> {
54 | installed.delete(triggerId as TriggerId);
55 | },
56 |
57 | async uninstallAll(): Promise<void> {
58 | installed.clear();
59 | },
60 |
61 | getInstalledIds(): string[] {
62 | return Array.from(installed.keys());
63 | },
64 | };
65 | }
66 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-chrome-bridge-monorepo",
3 | "version": "1.0.0",
4 | "private": true,
5 | "author": "hangye",
6 | "type": "module",
7 | "scripts": {
8 | "build:shared": "pnpm --filter chrome-mcp-shared build",
9 | "build:native": "pnpm --filter mcp-chrome-bridge build",
10 | "build:extension": "pnpm --filter chrome-mcp-server build",
11 | "build:wasm": "pnpm --filter @chrome-mcp/wasm-simd build && pnpm run copy:wasm",
12 | "build": "pnpm -r --filter='!@chrome-mcp/wasm-simd' build",
13 | "copy:wasm": "cp ./packages/wasm-simd/pkg/simd_math.js ./packages/wasm-simd/pkg/simd_math_bg.wasm ./app/chrome-extension/workers/",
14 | "dev:shared": "pnpm --filter chrome-mcp-shared dev",
15 | "dev:native": "pnpm --filter mcp-chrome-bridge dev",
16 | "dev:extension": "pnpm --filter chrome-mcp-server dev",
17 | "dev": "pnpm --filter chrome-mcp-shared build && pnpm -r --parallel dev",
18 | "lint": "pnpm -r lint",
19 | "lint:fix": "pnpm -r lint:fix",
20 | "format": "pnpm -r format",
21 | "clean:dist": "pnpm -r exec rm -rf dist .turbo",
22 | "clean:modules": "pnpm -r exec rm -rf node_modules && rm -rf node_modules",
23 | "clean": "npm run clean:dist && npm run clean:modules",
24 | "typecheck": "pnpm -r exec tsc --noEmit",
25 | "prepare": "husky"
26 | },
27 | "devDependencies": {
28 | "@commitlint/cli": "^19.8.1",
29 | "@commitlint/config-conventional": "^19.8.1",
30 | "@eslint/js": "^9.25.1",
31 | "@typescript-eslint/eslint-plugin": "^8.32.0",
32 | "@typescript-eslint/parser": "^8.32.0",
33 | "eslint": "^9.26.0",
34 | "eslint-config-prettier": "^10.1.5",
35 | "eslint-plugin-vue": "^10.0.0",
36 | "globals": "^16.1.0",
37 | "husky": "^9.1.7",
38 | "lint-staged": "^15.5.1",
39 | "prettier": "^3.5.3",
40 | "typescript": "^5.8.3",
41 | "typescript-eslint": "^8.32.0",
42 | "vue-eslint-parser": "^10.1.3"
43 | },
44 | "lint-staged": {
45 | "**/*.{js,jsx,ts,tsx,vue}": [
46 | "eslint --fix",
47 | "prettier --write"
48 | ],
49 | "**/*.{json,md,yaml,html,css}": [
50 | "prettier --write"
51 | ]
52 | }
53 | }
54 |
```
--------------------------------------------------------------------------------
/packages/shared/src/node-spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | // node-spec.ts — shared NodeSpec types for UI-driven forms
2 |
3 | export type FieldType = 'string' | 'number' | 'boolean' | 'select' | 'object' | 'array' | 'json';
4 |
5 | export interface FieldSpecBase {
6 | key: string;
7 | label: string;
8 | type: FieldType;
9 | required?: boolean;
10 | placeholder?: string;
11 | help?: string;
12 | // widget name used by UI; runtime ignores it
13 | widget?: string;
14 | uiProps?: Record<string, any>;
15 | }
16 |
17 | export interface FieldString extends FieldSpecBase {
18 | type: 'string';
19 | default?: string;
20 | }
21 | export interface FieldNumber extends FieldSpecBase {
22 | type: 'number';
23 | min?: number;
24 | max?: number;
25 | step?: number;
26 | default?: number;
27 | }
28 | export interface FieldBoolean extends FieldSpecBase {
29 | type: 'boolean';
30 | default?: boolean;
31 | }
32 | export interface FieldSelect extends FieldSpecBase {
33 | type: 'select';
34 | options: Array<{ label: string; value: string | number | boolean }>;
35 | default?: string | number | boolean;
36 | }
37 | export interface FieldObject extends FieldSpecBase {
38 | type: 'object';
39 | fields: FieldSpec[];
40 | default?: Record<string, any>;
41 | }
42 | export interface FieldArray extends FieldSpecBase {
43 | type: 'array';
44 | item: FieldString | FieldNumber | FieldBoolean | FieldSelect | FieldObject | FieldJson;
45 | default?: any[];
46 | }
47 | export interface FieldJson extends FieldSpecBase {
48 | type: 'json';
49 | default?: any;
50 | }
51 |
52 | export type FieldSpec =
53 | | FieldString
54 | | FieldNumber
55 | | FieldBoolean
56 | | FieldSelect
57 | | FieldObject
58 | | FieldArray
59 | | FieldJson;
60 |
61 | export type NodeCategory = 'Flow' | 'Actions' | 'Logic' | 'Tools' | 'Tabs' | 'Page';
62 |
63 | export interface NodeSpecDisplay {
64 | label: string;
65 | iconClass: string;
66 | category: NodeCategory;
67 | docUrl?: string;
68 | }
69 |
70 | export interface NodeSpec {
71 | type: string; // Aligns with NodeType/StepType
72 | version: number;
73 | display: NodeSpecDisplay;
74 | ports: { inputs: number | 'any'; outputs: Array<{ label?: string }> | 'any' };
75 | schema: FieldSpec[];
76 | defaults: Record<string, any>;
77 | validate?: (config: any) => string[];
78 | }
79 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/tabs.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { TOOL_NAMES } from 'chrome-mcp-shared';
2 | import { handleCallTool } from '@/entrypoints/background/tools';
3 | import type { StepOpenTab, StepSwitchTab, StepCloseTab } from '../types';
4 | import { expandTemplatesDeep } from '../rr-utils';
5 | import type { ExecCtx, ExecResult, NodeRuntime } from './types';
6 |
7 | export const openTabNode: NodeRuntime<StepOpenTab> = {
8 | run: async (ctx, step) => {
9 | const s: any = expandTemplatesDeep(step as any, ctx.vars);
10 | if (s.newWindow) await chrome.windows.create({ url: s.url || undefined, focused: true });
11 | else await chrome.tabs.create({ url: s.url || undefined, active: true });
12 | return {} as ExecResult;
13 | },
14 | };
15 |
16 | export const switchTabNode: NodeRuntime<StepSwitchTab> = {
17 | run: async (ctx, step) => {
18 | const s: any = expandTemplatesDeep(step as any, ctx.vars);
19 | let targetTabId: number | undefined = s.tabId;
20 | if (!targetTabId) {
21 | const tabs = await chrome.tabs.query({});
22 | const hit = tabs.find(
23 | (t) =>
24 | (s.urlContains && (t.url || '').includes(String(s.urlContains))) ||
25 | (s.titleContains && (t.title || '').includes(String(s.titleContains))),
26 | );
27 | targetTabId = (hit && hit.id) as number | undefined;
28 | }
29 | if (!targetTabId) throw new Error('switchTab: no matching tab');
30 | const res = await handleCallTool({
31 | name: TOOL_NAMES.BROWSER.SWITCH_TAB,
32 | args: { tabId: targetTabId },
33 | });
34 | if ((res as any).isError) throw new Error('switchTab failed');
35 | return {} as ExecResult;
36 | },
37 | };
38 |
39 | export const closeTabNode: NodeRuntime<StepCloseTab> = {
40 | run: async (ctx, step) => {
41 | const s: any = expandTemplatesDeep(step as any, ctx.vars);
42 | const args: any = {};
43 | if (Array.isArray(s.tabIds) && s.tabIds.length) args.tabIds = s.tabIds;
44 | if (s.url) args.url = s.url;
45 | const res = await handleCallTool({ name: TOOL_NAMES.BROWSER.CLOSE_TABS, args });
46 | if ((res as any).isError) throw new Error('closeTab failed');
47 | return {} as ExecResult;
48 | },
49 | };
50 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/conditional.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { Step } from '../types';
2 | import type { ExecCtx, ExecResult, NodeRuntime } from './types';
3 |
4 | export const ifNode: NodeRuntime<any> = {
5 | validate: (step) => {
6 | const s = step as any;
7 | const hasBranches = Array.isArray(s.branches) && s.branches.length > 0;
8 | const ok = hasBranches || !!s.condition;
9 | return ok ? { ok } : { ok, errors: ['缺少条件或分支'] };
10 | },
11 | run: async (ctx: ExecCtx, step: Step) => {
12 | const s: any = step;
13 | if (Array.isArray(s.branches) && s.branches.length > 0) {
14 | const evalExpr = (expr: string): boolean => {
15 | const code = String(expr || '').trim();
16 | if (!code) return false;
17 | try {
18 | const fn = new Function(
19 | 'vars',
20 | 'workflow',
21 | `try { return !!(${code}); } catch (e) { return false; }`,
22 | );
23 | return !!fn(ctx.vars, ctx.vars);
24 | } catch {
25 | return false;
26 | }
27 | };
28 | for (const br of s.branches) {
29 | if (br?.expr && evalExpr(String(br.expr)))
30 | return { nextLabel: String(br.label || `case:${br.id || 'match'}`) } as ExecResult;
31 | }
32 | if ('else' in s) return { nextLabel: String(s.else || 'default') } as ExecResult;
33 | return { nextLabel: 'default' } as ExecResult;
34 | }
35 | // legacy condition: { var/equals | expression }
36 | try {
37 | let result = false;
38 | const cond = s.condition;
39 | if (cond && typeof cond.expression === 'string' && cond.expression.trim()) {
40 | const fn = new Function(
41 | 'vars',
42 | `try { return !!(${cond.expression}); } catch (e) { return false; }`,
43 | );
44 | result = !!fn(ctx.vars);
45 | } else if (cond && typeof cond.var === 'string') {
46 | const v = ctx.vars[cond.var];
47 | if ('equals' in cond) result = String(v) === String(cond.equals);
48 | else result = !!v;
49 | }
50 | return { nextLabel: result ? 'true' : 'false' } as ExecResult;
51 | } catch {
52 | return { nextLabel: 'false' } as ExecResult;
53 | }
54 | },
55 | };
56 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/logging/run-logger.ts:
--------------------------------------------------------------------------------
```typescript
1 | // engine/logging/run-logger.ts — run logs, overlay and persistence
2 | import type { RunLogEntry, RunRecord, Flow } from '../../types';
3 | import { appendRun } from '../../flow-store';
4 | import { TOOL_NAMES } from 'chrome-mcp-shared';
5 | import { handleCallTool } from '@/entrypoints/background/tools';
6 |
7 | export class RunLogger {
8 | private logs: RunLogEntry[] = [];
9 | constructor(private runId: string) {}
10 |
11 | push(e: RunLogEntry) {
12 | this.logs.push(e);
13 | }
14 |
15 | getLogs() {
16 | return this.logs;
17 | }
18 |
19 | async overlayInit() {
20 | try {
21 | const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
22 | if (tabs[0]?.id)
23 | await chrome.tabs.sendMessage(tabs[0].id, { action: 'rr_overlay', cmd: 'init' } as any);
24 | } catch {}
25 | }
26 |
27 | async overlayAppend(text: string) {
28 | try {
29 | const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
30 | if (tabs[0]?.id)
31 | await chrome.tabs.sendMessage(tabs[0].id, {
32 | action: 'rr_overlay',
33 | cmd: 'append',
34 | text,
35 | } as any);
36 | } catch {}
37 | }
38 |
39 | async overlayDone() {
40 | try {
41 | const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
42 | if (tabs[0]?.id)
43 | await chrome.tabs.sendMessage(tabs[0].id, { action: 'rr_overlay', cmd: 'done' } as any);
44 | } catch {}
45 | }
46 |
47 | async screenshotOnFailure() {
48 | try {
49 | const shot = await handleCallTool({
50 | name: TOOL_NAMES.BROWSER.COMPUTER,
51 | args: { action: 'screenshot' },
52 | });
53 | const img = (shot?.content?.find((c: any) => c.type === 'image') as any)?.data as string;
54 | if (img) this.logs[this.logs.length - 1].screenshotBase64 = img;
55 | } catch {}
56 | }
57 |
58 | async persist(flow: Flow, startedAt: number, success: boolean) {
59 | const record: RunRecord = {
60 | id: this.runId,
61 | flowId: flow.id,
62 | startedAt: new Date(startedAt).toISOString(),
63 | finishedAt: new Date().toISOString(),
64 | success,
65 | entries: this.logs,
66 | };
67 | await appendRun(record);
68 | }
69 | }
70 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/tests/record-replay/_test-helpers.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Test helpers for record-replay contract tests.
3 | *
4 | * Provides minimal factories and mocks for testing the execution pipeline
5 | * without requiring real browser or tool dependencies.
6 | */
7 |
8 | import { vi } from 'vitest';
9 | import type { ExecCtx } from '@/entrypoints/background/record-replay/nodes/types';
10 | import type { ActionExecutionContext } from '@/entrypoints/background/record-replay/actions/types';
11 |
12 | /**
13 | * Create a minimal ExecCtx for testing
14 | */
15 | export function createMockExecCtx(overrides: Partial<ExecCtx> = {}): ExecCtx {
16 | return {
17 | vars: {},
18 | logger: vi.fn(),
19 | ...overrides,
20 | };
21 | }
22 |
23 | /**
24 | * Create a minimal ActionExecutionContext for testing
25 | */
26 | export function createMockActionCtx(
27 | overrides: Partial<ActionExecutionContext> = {},
28 | ): ActionExecutionContext {
29 | return {
30 | vars: {},
31 | tabId: 1,
32 | log: vi.fn(),
33 | ...overrides,
34 | };
35 | }
36 |
37 | /**
38 | * Create a minimal Step for testing
39 | */
40 | export function createMockStep(type: string, overrides: Record<string, unknown> = {}): any {
41 | return {
42 | id: `step_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
43 | type,
44 | ...overrides,
45 | };
46 | }
47 |
48 | /**
49 | * Create a minimal Flow for testing (with nodes/edges for scheduler)
50 | */
51 | export function createMockFlow(overrides: Record<string, unknown> = {}): any {
52 | const id = `flow_${Date.now()}`;
53 | return {
54 | id,
55 | name: 'Test Flow',
56 | version: 1,
57 | steps: [],
58 | nodes: [],
59 | edges: [],
60 | variables: [],
61 | meta: {
62 | createdAt: new Date().toISOString(),
63 | updatedAt: new Date().toISOString(),
64 | },
65 | ...overrides,
66 | };
67 | }
68 |
69 | /**
70 | * Create a mock ActionRegistry for testing
71 | */
72 | export function createMockRegistry(handlers: Map<string, any> = new Map()) {
73 | const executeFn = vi.fn(async () => ({ status: 'success' as const }));
74 |
75 | return {
76 | get: vi.fn((type: string) => handlers.get(type) || { type }),
77 | execute: executeFn,
78 | register: vi.fn(),
79 | has: vi.fn((type: string) => handlers.has(type)),
80 | _executeFn: executeFn, // Expose for assertions
81 | };
82 | }
83 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/KeyValueEditor.vue:
--------------------------------------------------------------------------------
```vue
1 | <template>
2 | <div class="kve">
3 | <div v-for="(item, i) in rows" :key="i" class="kve-row">
4 | <input class="kve-key" v-model="item.k" placeholder="变量名" />
5 | <input class="kve-val" v-model="item.v" placeholder="结果路径(如 data.items[0].id)" />
6 | <button class="mini" @click="move(i, -1)" :disabled="i === 0">↑</button>
7 | <button class="mini" @click="move(i, 1)" :disabled="i === rows.length - 1">↓</button>
8 | <button class="mini danger" @click="remove(i)">删</button>
9 | </div>
10 | <button class="mini" @click="add">添加映射</button>
11 | </div>
12 | </template>
13 |
14 | <script lang="ts" setup>
15 | import { watch, reactive } from 'vue';
16 |
17 | const props = defineProps<{ modelValue: Record<string, string> | undefined }>();
18 | const emit = defineEmits(['update:modelValue']);
19 |
20 | const rows = reactive<Array<{ k: string; v: string }>>([]);
21 |
22 | function syncFromModel() {
23 | rows.splice(0, rows.length);
24 | const obj = props.modelValue || {};
25 | for (const [k, v] of Object.entries(obj)) rows.push({ k, v: String(v) });
26 | }
27 | function syncToModel() {
28 | const out: Record<string, string> = {};
29 | for (const r of rows) if (r.k) out[r.k] = r.v || '';
30 | emit('update:modelValue', out);
31 | }
32 | watch(() => props.modelValue, syncFromModel, { immediate: true, deep: true });
33 | watch(rows, syncToModel, { deep: true });
34 |
35 | function add() {
36 | rows.push({ k: '', v: '' });
37 | }
38 | function remove(i: number) {
39 | rows.splice(i, 1);
40 | }
41 | function move(i: number, d: number) {
42 | const j = i + d;
43 | if (j < 0 || j >= rows.length) return;
44 | const t = rows[i];
45 | rows[i] = rows[j];
46 | rows[j] = t;
47 | }
48 | </script>
49 |
50 | <style scoped>
51 | .kve {
52 | display: flex;
53 | flex-direction: column;
54 | gap: 6px;
55 | }
56 | .kve-row {
57 | display: grid;
58 | grid-template-columns: 160px 1fr auto auto auto;
59 | gap: 6px;
60 | align-items: center;
61 | }
62 | .kve-key,
63 | .kve-val {
64 | border: 1px solid #d1d5db;
65 | border-radius: 6px;
66 | padding: 6px;
67 | }
68 | .mini {
69 | font-size: 12px;
70 | padding: 4px 8px;
71 | border: 1px solid #d1d5db;
72 | background: #fff;
73 | border-radius: 6px;
74 | cursor: pointer;
75 | }
76 | .mini.danger {
77 | background: #fee2e2;
78 | border-color: #fecaca;
79 | }
80 | </style>
81 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/shared/selector/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Selector Engine - Unified selector generation and element location
3 | *
4 | * Modules:
5 | * - types: Type definitions
6 | * - stability: Stability scoring
7 | * - strategies: Selector generation strategies
8 | * - generator: Selector target generation
9 | * - locator: Element location
10 | * - fingerprint: Element fingerprinting (Phase 1.2)
11 | * - dom-path: DOM path computation (Phase 1.2)
12 | * - shadow-dom: Shadow DOM utilities (Phase 1.2)
13 | */
14 |
15 | // Type exports
16 | export * from './types';
17 |
18 | // Stability scoring
19 | export { computeSelectorStability, withStability, compareSelectorCandidates } from './stability';
20 |
21 | // Selector strategies
22 | export { DEFAULT_SELECTOR_STRATEGIES } from './strategies';
23 | export { anchorRelpathStrategy } from './strategies/anchor-relpath';
24 | export { ariaStrategy } from './strategies/aria';
25 | export { cssPathStrategy } from './strategies/css-path';
26 | export { cssUniqueStrategy } from './strategies/css-unique';
27 | export { testIdStrategy } from './strategies/testid';
28 | export { textStrategy } from './strategies/text';
29 |
30 | // Selector generation
31 | export {
32 | generateSelectorTarget,
33 | generateExtendedSelectorTarget,
34 | normalizeSelectorGenerationOptions,
35 | cssEscape,
36 | type GenerateSelectorTargetOptions,
37 | } from './generator';
38 |
39 | // Element location
40 | export {
41 | SelectorLocator,
42 | createChromeSelectorLocator,
43 | createChromeSelectorLocatorTransport,
44 | type SelectorLocatorTransport,
45 | } from './locator';
46 |
47 | // Fingerprint utilities (Phase 1.2)
48 | export {
49 | computeFingerprint,
50 | parseFingerprint,
51 | verifyFingerprint,
52 | fingerprintSimilarity,
53 | fingerprintMatches,
54 | type ElementFingerprint,
55 | type FingerprintOptions,
56 | } from './fingerprint';
57 |
58 | // DOM path utilities (Phase 1.2)
59 | export {
60 | computeDomPath,
61 | locateByDomPath,
62 | compareDomPaths,
63 | isAncestorPath,
64 | getRelativePath,
65 | type DomPath,
66 | } from './dom-path';
67 |
68 | // Shadow DOM utilities (Phase 1.2)
69 | export {
70 | traverseShadowDom,
71 | traverseShadowDomWithDetails,
72 | queryInShadowDom,
73 | queryAllInShadowDom,
74 | isUniqueInShadowDom,
75 | type ShadowTraversalResult,
76 | type ShadowTraversalFailureReason,
77 | } from './shadow-dom';
78 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/runners/control-flow-runner.ts:
--------------------------------------------------------------------------------
```typescript
1 | // control-flow-runner.ts — foreach / while orchestration
2 |
3 | import type { ExecCtx } from '../../nodes';
4 | import { RunLogger } from '../logging/run-logger';
5 |
6 | export interface ControlFlowEnv {
7 | vars: Record<string, any>;
8 | logger: RunLogger;
9 | evalCondition: (cond: any) => boolean;
10 | runSubflowById: (subflowId: string, ctx: ExecCtx) => Promise<void>;
11 | isPaused: () => boolean;
12 | }
13 |
14 | export class ControlFlowRunner {
15 | constructor(private env: ControlFlowEnv) {}
16 |
17 | async run(control: any, ctx: ExecCtx): Promise<'ok' | 'paused'> {
18 | if (control?.kind === 'foreach') {
19 | const list = Array.isArray(this.env.vars[control.listVar])
20 | ? (this.env.vars[control.listVar] as any[])
21 | : [];
22 | const concurrency = Math.max(1, Math.min(16, Number(control.concurrency ?? 1)));
23 | if (concurrency <= 1) {
24 | for (const it of list) {
25 | this.env.vars[control.itemVar] = it;
26 | await this.env.runSubflowById(control.subflowId, ctx);
27 | if (this.env.isPaused()) return 'paused';
28 | }
29 | return this.env.isPaused() ? 'paused' : 'ok';
30 | }
31 | // Parallel with shallow-cloned vars per task (no automatic merge)
32 | let idx = 0;
33 | const runOne = async () => {
34 | while (idx < list.length) {
35 | const cur = idx++;
36 | const it = list[cur];
37 | const childCtx: ExecCtx = { ...ctx, vars: { ...this.env.vars } };
38 | childCtx.vars[control.itemVar] = it;
39 | await this.env.runSubflowById(control.subflowId, childCtx);
40 | if (this.env.isPaused()) return;
41 | }
42 | };
43 | const workers = Array.from({ length: Math.min(concurrency, list.length) }, () => runOne());
44 | await Promise.all(workers);
45 | return this.env.isPaused() ? 'paused' : 'ok';
46 | }
47 | if (control?.kind === 'while') {
48 | let i = 0;
49 | while (i < control.maxIterations && this.env.evalCondition(control.condition)) {
50 | await this.env.runSubflowById(control.subflowId, ctx);
51 | if (this.env.isPaused()) return 'paused';
52 | i++;
53 | }
54 | return this.env.isPaused() ? 'paused' : 'ok';
55 | }
56 | // Unknown control type → no-op
57 | return 'ok';
58 | }
59 | }
60 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/plugins/manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type {
2 | RunPlugin,
3 | HookControl,
4 | RunContext,
5 | StepContext,
6 | StepAfterContext,
7 | StepErrorContext,
8 | StepRetryContext,
9 | RunEndContext,
10 | SubflowContext,
11 | } from './types';
12 |
13 | export class PluginManager {
14 | constructor(private plugins: RunPlugin[]) {}
15 |
16 | async runStart(ctx: RunContext) {
17 | for (const p of this.plugins) await safeCall(p, 'onRunStart', ctx);
18 | }
19 |
20 | async beforeStep(ctx: StepContext): Promise<HookControl | undefined> {
21 | for (const p of this.plugins) {
22 | const out = await safeCall(p, 'onBeforeStep', ctx);
23 | if (out && (out.pause || out.nextLabel)) return out;
24 | }
25 | return undefined;
26 | }
27 |
28 | async afterStep(ctx: StepAfterContext) {
29 | for (const p of this.plugins) await safeCall(p, 'onAfterStep', ctx);
30 | }
31 |
32 | async onError(ctx: StepErrorContext): Promise<HookControl | undefined> {
33 | for (const p of this.plugins) {
34 | const out = await safeCall(p, 'onStepError', ctx);
35 | if (out && (out.pause || out.nextLabel)) return out;
36 | }
37 | return undefined;
38 | }
39 |
40 | async onRetry(ctx: StepRetryContext) {
41 | for (const p of this.plugins) await safeCall(p, 'onRetry', ctx);
42 | }
43 |
44 | async onChooseNextLabel(ctx: StepContext & { suggested?: string }): Promise<string | undefined> {
45 | for (const p of this.plugins) {
46 | const out = await safeCall(p, 'onChooseNextLabel', ctx);
47 | if (out && out.nextLabel) return String(out.nextLabel);
48 | }
49 | return undefined;
50 | }
51 |
52 | async subflowStart(ctx: SubflowContext) {
53 | for (const p of this.plugins) await safeCall(p, 'onSubflowStart', ctx);
54 | }
55 |
56 | async subflowEnd(ctx: SubflowContext) {
57 | for (const p of this.plugins) await safeCall(p, 'onSubflowEnd', ctx);
58 | }
59 |
60 | async runEnd(ctx: RunEndContext) {
61 | for (const p of this.plugins) await safeCall(p, 'onRunEnd', ctx);
62 | }
63 | }
64 |
65 | async function safeCall<T extends keyof RunPlugin>(plugin: RunPlugin, key: T, arg: any) {
66 | try {
67 | const fn = plugin[key] as any;
68 | if (typeof fn === 'function') return await fn.call(plugin, arg);
69 | } catch (e) {
70 | // swallow plugin errors to keep core stable
71 | // console.warn(`[plugin:${plugin.name}] ${String(key)} error:`, e);
72 | }
73 | return undefined;
74 | }
75 |
```
--------------------------------------------------------------------------------
/.github/workflows/build-release.yml:
--------------------------------------------------------------------------------
```yaml
1 | # name: Build and Release Chrome Extension
2 |
3 | # on:
4 | # push:
5 | # branches: [ master, develop ]
6 | # paths:
7 | # - 'app/chrome-extension/**'
8 | # pull_request:
9 | # branches: [ master ]
10 | # paths:
11 | # - 'app/chrome-extension/**'
12 | # workflow_dispatch:
13 |
14 | # jobs:
15 | # build-extension:
16 | # runs-on: ubuntu-latest
17 |
18 | # steps:
19 | # - name: Checkout code
20 | # uses: actions/checkout@v4
21 |
22 | # - name: Setup Node.js
23 | # uses: actions/setup-node@v4
24 | # with:
25 | # node-version: '18'
26 | # cache: 'npm'
27 | # cache-dependency-path: 'app/chrome-extension/package-lock.json'
28 |
29 | # - name: Install dependencies
30 | # run: |
31 | # cd app/chrome-extension
32 | # npm ci
33 |
34 | # - name: Build extension
35 | # run: |
36 | # cd app/chrome-extension
37 | # npm run build
38 |
39 | # - name: Create zip package
40 | # run: |
41 | # cd app/chrome-extension
42 | # npm run zip
43 |
44 | # - name: Prepare release directory
45 | # run: |
46 | # mkdir -p releases/chrome-extension/latest
47 | # mkdir -p releases/chrome-extension/$(date +%Y%m%d-%H%M%S)
48 |
49 | # - name: Copy release files
50 | # run: |
51 | # # Copy to latest
52 | # cp app/chrome-extension/.output/chrome-mv3-prod.zip releases/chrome-extension/latest/chrome-mcp-server-latest.zip
53 |
54 | # # Copy to timestamped version
55 | # TIMESTAMP=$(date +%Y%m%d-%H%M%S)
56 | # cp app/chrome-extension/.output/chrome-mv3-prod.zip releases/chrome-extension/$TIMESTAMP/chrome-mcp-server-$TIMESTAMP.zip
57 |
58 | # - name: Upload build artifacts
59 | # uses: actions/upload-artifact@v4
60 | # with:
61 | # name: chrome-extension-build
62 | # path: releases/chrome-extension/
63 | # retention-days: 30
64 |
65 | # - name: Commit and push releases (if on main branch)
66 | # if: github.ref == 'refs/heads/main'
67 | # run: |
68 | # git config --local user.email "[email protected]"
69 | # git config --local user.name "GitHub Action"
70 | # git add releases/
71 | # git diff --staged --quiet || git commit -m "Auto-build: Update Chrome extension release [skip ci]"
72 | # git push
73 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay-v3/storage/triggers.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview 触发器存储
3 | * @description 实现触发器的 CRUD 操作(Phase 4 完整实现)
4 | */
5 |
6 | import type { TriggerId } from '../domain/ids';
7 | import type { TriggerSpec } from '../domain/triggers';
8 | import type { TriggersStore } from '../engine/storage/storage-port';
9 | import { RR_V3_STORES, withTransaction } from './db';
10 |
11 | /**
12 | * 创建 TriggersStore 实现
13 | */
14 | export function createTriggersStore(): TriggersStore {
15 | return {
16 | async list(): Promise<TriggerSpec[]> {
17 | return withTransaction(RR_V3_STORES.TRIGGERS, 'readonly', async (stores) => {
18 | const store = stores[RR_V3_STORES.TRIGGERS];
19 | return new Promise<TriggerSpec[]>((resolve, reject) => {
20 | const request = store.getAll();
21 | request.onsuccess = () => resolve(request.result as TriggerSpec[]);
22 | request.onerror = () => reject(request.error);
23 | });
24 | });
25 | },
26 |
27 | async get(id: TriggerId): Promise<TriggerSpec | null> {
28 | return withTransaction(RR_V3_STORES.TRIGGERS, 'readonly', async (stores) => {
29 | const store = stores[RR_V3_STORES.TRIGGERS];
30 | return new Promise<TriggerSpec | null>((resolve, reject) => {
31 | const request = store.get(id);
32 | request.onsuccess = () => resolve((request.result as TriggerSpec) ?? null);
33 | request.onerror = () => reject(request.error);
34 | });
35 | });
36 | },
37 |
38 | async save(spec: TriggerSpec): Promise<void> {
39 | return withTransaction(RR_V3_STORES.TRIGGERS, 'readwrite', async (stores) => {
40 | const store = stores[RR_V3_STORES.TRIGGERS];
41 | return new Promise<void>((resolve, reject) => {
42 | const request = store.put(spec);
43 | request.onsuccess = () => resolve();
44 | request.onerror = () => reject(request.error);
45 | });
46 | });
47 | },
48 |
49 | async delete(id: TriggerId): Promise<void> {
50 | return withTransaction(RR_V3_STORES.TRIGGERS, 'readwrite', async (stores) => {
51 | const store = stores[RR_V3_STORES.TRIGGERS];
52 | return new Promise<void>((resolve, reject) => {
53 | const request = store.delete(id);
54 | request.onsuccess = () => resolve();
55 | request.onerror = () => reject(request.error);
56 | });
57 | });
58 | },
59 | };
60 | }
61 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/shared/selector/strategies/aria.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * ARIA Strategy - 基于无障碍属性的选择器策略
3 | * 使用 aria-label, role 等属性生成选择器
4 | */
5 |
6 | import type { SelectorCandidate, SelectorStrategy } from '../types';
7 |
8 | function guessRoleByTag(tag: string): string | undefined {
9 | if (tag === 'input' || tag === 'textarea') return 'textbox';
10 | if (tag === 'button') return 'button';
11 | if (tag === 'a') return 'link';
12 | return undefined;
13 | }
14 |
15 | function uniqStrings(items: ReadonlyArray<string>): string[] {
16 | const seen = new Set<string>();
17 | const out: string[] = [];
18 | for (const s of items) {
19 | const v = s.trim();
20 | if (!v) continue;
21 | if (seen.has(v)) continue;
22 | seen.add(v);
23 | out.push(v);
24 | }
25 | return out;
26 | }
27 |
28 | export const ariaStrategy: SelectorStrategy = {
29 | id: 'aria',
30 | generate(ctx) {
31 | if (!ctx.options.includeAria) return [];
32 |
33 | const { element, helpers } = ctx;
34 | const out: SelectorCandidate[] = [];
35 |
36 | const name = element.getAttribute('aria-label')?.trim();
37 | if (!name) return out;
38 |
39 | const tag = element.tagName?.toLowerCase?.() ?? '';
40 | const role = element.getAttribute('role')?.trim() || guessRoleByTag(tag);
41 |
42 | const qName = JSON.stringify(name);
43 | const selectors: string[] = [];
44 |
45 | if (role) selectors.push(`[role=${JSON.stringify(role)}][aria-label=${qName}]`);
46 | selectors.push(`[aria-label=${qName}]`);
47 |
48 | if (role === 'textbox') {
49 | selectors.unshift(
50 | `input[aria-label=${qName}]`,
51 | `textarea[aria-label=${qName}]`,
52 | `[role="textbox"][aria-label=${qName}]`,
53 | );
54 | } else if (role === 'button') {
55 | selectors.unshift(`button[aria-label=${qName}]`, `[role="button"][aria-label=${qName}]`);
56 | } else if (role === 'link') {
57 | selectors.unshift(`a[aria-label=${qName}]`, `[role="link"][aria-label=${qName}]`);
58 | }
59 |
60 | for (const sel of uniqStrings(selectors)) {
61 | if (helpers.isUnique(sel)) {
62 | out.push({ type: 'attr', value: sel, source: 'generated', strategy: 'aria' });
63 | }
64 | }
65 |
66 | // Structured aria candidate for UI/debugging (locator can translate it too).
67 | out.push({
68 | type: 'aria',
69 | value: `${role ?? 'element'}[name=${JSON.stringify(name)}]`,
70 | role,
71 | name,
72 | source: 'generated',
73 | strategy: 'aria',
74 | });
75 |
76 | return out;
77 | },
78 | };
79 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/state-manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | // engine/state-manager.ts — lightweight run state store with events and persistence
2 |
3 | type Listener<T> = (payload: T) => void;
4 |
5 | export interface RunState {
6 | id: string;
7 | flowId: string;
8 | name?: string;
9 | status: 'running' | 'completed' | 'failed' | 'stopped';
10 | startedAt: number;
11 | updatedAt: number;
12 | }
13 |
14 | export class StateManager<T extends { id: string }> {
15 | private key: string;
16 | private states = new Map<string, T>();
17 | private listeners: Record<string, Listener<any>[]> = Object.create(null);
18 |
19 | constructor(storageKey: string) {
20 | this.key = storageKey;
21 | }
22 |
23 | on<E = any>(name: string, listener: Listener<E>) {
24 | (this.listeners[name] = this.listeners[name] || []).push(listener);
25 | }
26 |
27 | off<E = any>(name: string, listener: Listener<E>) {
28 | const arr = this.listeners[name];
29 | if (!arr) return;
30 | const i = arr.indexOf(listener as any);
31 | if (i >= 0) arr.splice(i, 1);
32 | }
33 |
34 | private emit<E = any>(name: string, payload: E) {
35 | const arr = this.listeners[name] || [];
36 | for (const fn of arr)
37 | try {
38 | fn(payload);
39 | } catch {}
40 | }
41 |
42 | getAll(): Map<string, T> {
43 | return this.states;
44 | }
45 |
46 | get(id: string): T | undefined {
47 | return this.states.get(id);
48 | }
49 |
50 | async add(id: string, data: T): Promise<void> {
51 | this.states.set(id, data);
52 | this.emit('add', { id, data });
53 | await this.persist();
54 | }
55 |
56 | async update(id: string, patch: Partial<T>): Promise<void> {
57 | const cur = this.states.get(id);
58 | if (!cur) return;
59 | const next = Object.assign({}, cur, patch);
60 | this.states.set(id, next);
61 | this.emit('update', { id, data: next });
62 | await this.persist();
63 | }
64 |
65 | async delete(id: string): Promise<void> {
66 | this.states.delete(id);
67 | this.emit('delete', { id });
68 | await this.persist();
69 | }
70 |
71 | private async persist(): Promise<void> {
72 | try {
73 | const obj = Object.fromEntries(this.states.entries());
74 | await chrome.storage.local.set({ [this.key]: obj });
75 | } catch {}
76 | }
77 |
78 | async restore(): Promise<void> {
79 | try {
80 | const res = await chrome.storage.local.get(this.key);
81 | const obj = (res && res[this.key]) || {};
82 | this.states = new Map(Object.entries(obj) as any);
83 | } catch {}
84 | }
85 | }
86 |
87 | export const runState = new StateManager<RunState>('rr_run_states');
88 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/dom-observer.js:
--------------------------------------------------------------------------------
```javascript
1 | /* eslint-disable */
2 | // dom-observer.js - observe DOM for triggers and notify background
3 | (function () {
4 | if (window.__RR_DOM_OBSERVER__) return;
5 | window.__RR_DOM_OBSERVER__ = true;
6 |
7 | const active = { triggers: [], hits: new Map() };
8 |
9 | function now() {
10 | return Date.now();
11 | }
12 |
13 | function applyTriggers(list) {
14 | try {
15 | active.triggers = Array.isArray(list) ? list.slice() : [];
16 | active.hits.clear();
17 | checkAll();
18 | } catch (e) {}
19 | }
20 |
21 | function checkAll() {
22 | try {
23 | for (const t of active.triggers) {
24 | maybeFire(t);
25 | }
26 | } catch (e) {}
27 | }
28 |
29 | function maybeFire(t) {
30 | try {
31 | const appear = t.appear !== false; // default true
32 | const sel = String(t.selector || '').trim();
33 | if (!sel) return;
34 | const exists = !!document.querySelector(sel);
35 | const key = t.id;
36 | const last = active.hits.get(key) || 0;
37 | const debounce = Math.max(0, Number(t.debounceMs ?? 800));
38 | if (now() - last < debounce) return;
39 | const should = appear ? exists : !exists;
40 | if (should) {
41 | active.hits.set(key, now());
42 | chrome.runtime.sendMessage({
43 | action: 'dom_trigger_fired',
44 | triggerId: t.id,
45 | url: location.href,
46 | });
47 | if (t.once !== false) removeTrigger(t.id);
48 | }
49 | } catch (e) {}
50 | }
51 |
52 | function removeTrigger(id) {
53 | try {
54 | active.triggers = active.triggers.filter((x) => x.id !== id);
55 | } catch (e) {}
56 | }
57 |
58 | const mo = new MutationObserver(() => {
59 | checkAll();
60 | });
61 | try {
62 | mo.observe(document.documentElement || document, {
63 | childList: true,
64 | subtree: true,
65 | attributes: false,
66 | characterData: false,
67 | });
68 | } catch (e) {}
69 |
70 | chrome.runtime.onMessage.addListener((req, _sender, sendResponse) => {
71 | try {
72 | if (req && req.action === 'dom_observer_ping') {
73 | sendResponse({ status: 'pong' });
74 | return false;
75 | }
76 | if (req && req.action === 'set_dom_triggers') {
77 | applyTriggers(req.triggers || []);
78 | sendResponse({ success: true, count: active.triggers.length });
79 | return true;
80 | }
81 | } catch (e) {
82 | sendResponse({ success: false, error: String(e && e.message ? e.message : e) });
83 | return true;
84 | }
85 | return false;
86 | });
87 | })();
88 |
```
--------------------------------------------------------------------------------
/app/native-server/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-chrome-bridge",
3 | "version": "1.0.29",
4 | "description": "Chrome Native-Messaging host (Node)",
5 | "main": "dist/index.js",
6 | "bin": {
7 | "mcp-chrome-bridge": "./dist/cli.js",
8 | "chrome-mcp-bridge": "./dist/cli.js",
9 | "mcp-chrome-stdio": "./dist/mcp/mcp-server-stdio.js"
10 | },
11 | "scripts": {
12 | "dev": "nodemon --watch src --ext ts,js,json --ignore dist/ --exec \"npm run build && npm run register:dev\"",
13 | "build": "ts-node src/scripts/build.ts",
14 | "test": "jest",
15 | "test:watch": "jest --watch",
16 | "lint": "eslint 'src/**/*.{js,ts}'",
17 | "lint:fix": "eslint 'src/**/*.{js,ts}' --fix",
18 | "format": "prettier --write 'src/**/*.{js,ts,json}'",
19 | "register:dev": "node dist/scripts/register-dev.js",
20 | "postinstall": "node dist/scripts/postinstall.js"
21 | },
22 | "files": [
23 | "dist",
24 | "!dist/node_path.txt"
25 | ],
26 | "engines": {
27 | "node": ">=20.0.0"
28 | },
29 | "preferGlobal": true,
30 | "keywords": [
31 | "mcp",
32 | "chrome",
33 | "browser"
34 | ],
35 | "author": "hangye",
36 | "license": "MIT",
37 | "dependencies": {
38 | "@anthropic-ai/claude-agent-sdk": "^0.1.69",
39 | "@fastify/cors": "^11.0.1",
40 | "@modelcontextprotocol/sdk": "^1.11.0",
41 | "@types/node-fetch": "2",
42 | "better-sqlite3": "^11.6.0",
43 | "chalk": "^5.4.1",
44 | "chrome-devtools-frontend": "^1.0.1299282",
45 | "chrome-mcp-shared": "workspace:*",
46 | "commander": "^13.1.0",
47 | "drizzle-orm": "^0.38.2",
48 | "fastify": "^5.3.2",
49 | "is-admin": "^4.0.0",
50 | "node-fetch": "2",
51 | "pino": "^9.6.0",
52 | "uuid": "^11.1.0"
53 | },
54 | "devDependencies": {
55 | "@jest/globals": "^29.7.0",
56 | "@types/better-sqlite3": "^7.6.12",
57 | "@types/chrome": "^0.0.318",
58 | "@types/jest": "^29.5.14",
59 | "@types/node": "^22.15.3",
60 | "@types/supertest": "^6.0.3",
61 | "@typescript-eslint/parser": "^8.31.1",
62 | "cross-env": "^7.0.3",
63 | "husky": "^9.1.7",
64 | "jest": "^29.7.0",
65 | "lint-staged": "^15.5.1",
66 | "nodemon": "^3.1.10",
67 | "pino-pretty": "^13.0.0",
68 | "rimraf": "^6.0.1",
69 | "supertest": "^7.1.0",
70 | "ts-jest": "^29.3.2",
71 | "ts-node": "^10.9.2"
72 | },
73 | "husky": {
74 | "hooks": {
75 | "pre-commit": "lint-staged"
76 | }
77 | },
78 | "lint-staged": {
79 | "*.{js,ts}": [
80 | "eslint --fix",
81 | "prettier --write"
82 | ],
83 | "*.{json,md}": [
84 | "prettier --write"
85 | ]
86 | }
87 | }
88 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/scroll.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { TOOL_NAMES } from 'chrome-mcp-shared';
2 | import { handleCallTool } from '@/entrypoints/background/tools';
3 | import type { StepScroll } from '../types';
4 | import { expandTemplatesDeep } from '../rr-utils';
5 | import type { ExecCtx, ExecResult, NodeRuntime } from './types';
6 |
7 | export const scrollNode: NodeRuntime<StepScroll> = {
8 | run: async (ctx, step: StepScroll) => {
9 | const s = expandTemplatesDeep(step as StepScroll, ctx.vars);
10 | const top = s.offset?.y ?? undefined;
11 | const left = s.offset?.x ?? undefined;
12 | const selectorFromTarget = (s as any).target?.candidates?.find(
13 | (c: any) => c.type === 'css' || c.type === 'attr',
14 | )?.value;
15 | let code = '';
16 | if (s.mode === 'offset' && !(s as any).target) {
17 | const t = top != null ? Number(top) : 'undefined';
18 | const l = left != null ? Number(left) : 'undefined';
19 | code = `try { window.scrollTo({ top: ${t}, left: ${l}, behavior: 'instant' }); } catch (e) {}`;
20 | } else if (s.mode === 'element' && selectorFromTarget) {
21 | code = `(() => { try { const el = document.querySelector(${JSON.stringify(selectorFromTarget)}); if (el) el.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'nearest' }); } catch (e) {} })();`;
22 | } else if (s.mode === 'container' && selectorFromTarget) {
23 | const t = top != null ? Number(top) : 'undefined';
24 | const l = left != null ? Number(left) : 'undefined';
25 | code = `(() => { try { const el = document.querySelector(${JSON.stringify(selectorFromTarget)}); if (el && typeof el.scrollTo === 'function') el.scrollTo({ top: ${t}, left: ${l}, behavior: 'instant' }); } catch (e) {} })();`;
26 | } else {
27 | const direction = top != null && Number(top) < 0 ? 'up' : 'down';
28 | const amount = 3;
29 | const res = await handleCallTool({
30 | name: TOOL_NAMES.BROWSER.COMPUTER,
31 | args: { action: 'scroll', scrollDirection: direction, scrollAmount: amount },
32 | });
33 | if ((res as any).isError) throw new Error('scroll failed');
34 | return {} as ExecResult;
35 | }
36 | if (code) {
37 | const res = await handleCallTool({
38 | name: TOOL_NAMES.BROWSER.INJECT_SCRIPT,
39 | args: { type: 'MAIN', jsScript: code },
40 | });
41 | if ((res as any).isError) throw new Error('scroll failed');
42 | }
43 | return {} as ExecResult;
44 | },
45 | };
46 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/components/agent/ProjectCreateForm.vue:
--------------------------------------------------------------------------------
```vue
1 | <template>
2 | <div class="flex flex-col gap-2">
3 | <!-- Project name input -->
4 | <div class="flex items-center gap-2">
5 | <span class="whitespace-nowrap w-12">Name</span>
6 | <input
7 | :value="name"
8 | class="flex-1 border border-slate-200 rounded px-2 py-1 text-xs bg-white focus:outline-none focus:ring-1 focus:ring-slate-400"
9 | placeholder="Project name"
10 | @input="handleNameInput"
11 | />
12 | </div>
13 |
14 | <!-- Root path selection -->
15 | <div class="flex items-center gap-2">
16 | <span class="whitespace-nowrap w-12">Root</span>
17 | <input
18 | :value="rootPath"
19 | readonly
20 | class="flex-1 border border-slate-200 rounded px-2 py-1 text-xs bg-slate-50 text-slate-600 focus:outline-none cursor-default"
21 | :placeholder="isLoadingDefault ? 'Loading...' : 'Select a directory'"
22 | />
23 | <button
24 | class="btn-secondary !px-2 !py-1 text-[11px] whitespace-nowrap"
25 | type="button"
26 | :disabled="isPicking"
27 | title="Use default directory (~/.chrome-mcp-agent/workspaces/...)"
28 | @click="$emit('use-default')"
29 | >
30 | Default
31 | </button>
32 | <button
33 | class="btn-secondary !px-2 !py-1 text-[11px] whitespace-nowrap"
34 | type="button"
35 | :disabled="isPicking"
36 | title="Open system directory picker"
37 | @click="$emit('pick-directory')"
38 | >
39 | {{ isPicking ? '...' : 'Browse' }}
40 | </button>
41 | <button
42 | class="btn-primary !px-2 !py-1 text-[11px]"
43 | type="button"
44 | :disabled="isCreating || !canCreate"
45 | @click="$emit('create')"
46 | >
47 | {{ isCreating ? 'Creating...' : 'Create' }}
48 | </button>
49 | </div>
50 |
51 | <!-- Error message -->
52 | <div v-if="error" class="text-[11px] text-red-600">
53 | {{ error }}
54 | </div>
55 | </div>
56 | </template>
57 |
58 | <script lang="ts" setup>
59 | defineProps<{
60 | name: string;
61 | rootPath: string;
62 | isCreating: boolean;
63 | isPicking: boolean;
64 | isLoadingDefault: boolean;
65 | canCreate: boolean;
66 | error: string | null;
67 | }>();
68 |
69 | const emit = defineEmits<{
70 | 'update:name': [value: string];
71 | 'update:root-path': [value: string];
72 | 'use-default': [];
73 | 'pick-directory': [];
74 | create: [];
75 | }>();
76 |
77 | function handleNameInput(event: Event): void {
78 | const value = (event.target as HTMLInputElement).value;
79 | emit('update:name', value);
80 | }
81 | </script>
82 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/nodes/NodeCard.vue:
--------------------------------------------------------------------------------
```vue
1 | <template>
2 | <div
3 | :class="['workflow-node', selected ? 'selected' : '', `type-${data.node.type}`]"
4 | @click="onSelect()"
5 | >
6 | <div v-if="hasErrors" class="node-error" :title="errorsTitle">
7 | <ILucideShieldX />
8 | <div class="tooltip">
9 | <div class="item" v-for="e in errList" :key="e">• {{ e }}</div>
10 | </div>
11 | </div>
12 | <div class="node-container">
13 | <div :class="['node-icon', `icon-${data.node.type}`]">
14 | <component :is="iconComp(data.node.type)" />
15 | </div>
16 | <div class="node-body">
17 | <div class="node-name">{{ data.node.name || getTypeLabel(data.node.type) }}</div>
18 | <div class="node-subtitle">{{ subtitle }}</div>
19 | </div>
20 | </div>
21 |
22 | <!-- Hide left target handle for trigger (no inputs allowed) -->
23 | <Handle
24 | v-if="data.node.type !== 'trigger'"
25 | type="target"
26 | :position="Position.Left"
27 | :class="['node-handle', hasIncoming ? 'connected' : 'unconnected']"
28 | />
29 | <Handle
30 | v-if="data.node.type !== 'if'"
31 | type="source"
32 | :position="Position.Right"
33 | :class="['node-handle', hasOutgoing ? 'connected' : 'unconnected']"
34 | />
35 | </div>
36 | </template>
37 |
38 | <script lang="ts" setup>
39 | // Reusable card-like node for most operation nodes
40 | import { computed } from 'vue';
41 | import type { NodeBase, Edge as EdgeV2 } from '@/entrypoints/background/record-replay/types';
42 | import { Handle, Position } from '@vue-flow/core';
43 | import { iconComp, getTypeLabel, nodeSubtitle } from './node-util';
44 | import ILucideShieldX from '~icons/lucide/shield-x';
45 |
46 | const props = defineProps<{
47 | id: string;
48 | data: { node: NodeBase; edges: EdgeV2[]; onSelect: (id: string) => void; errors?: string[] };
49 | selected?: boolean;
50 | }>();
51 |
52 | const subtitle = computed(() => nodeSubtitle(props.data.node));
53 | const hasIncoming = computed(
54 | () => props.data.edges?.some?.((e) => e && e.to === props.data.node.id) || false,
55 | );
56 | const hasOutgoing = computed(
57 | () => props.data.edges?.some?.((e) => e && e.from === props.data.node.id) || false,
58 | );
59 | const errList = computed(() => (props.data.errors || []) as string[]);
60 | const hasErrors = computed(() => errList.value.length > 0);
61 | const errorsTitle = computed(() => errList.value.join('\n'));
62 |
63 | function onSelect() {
64 | // keep event as function to avoid emitting through VueFlow slots
65 | try {
66 | props.data.onSelect(props.id);
67 | } catch {}
68 | }
69 | </script>
70 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/inject-bridge.js:
--------------------------------------------------------------------------------
```javascript
1 | /* eslint-disable */
2 |
3 | (() => {
4 | // Prevent duplicate injection of the bridge itself.
5 | if (window.__INJECT_SCRIPT_TOOL_UNIVERSAL_BRIDGE_LOADED__) return;
6 | window.__INJECT_SCRIPT_TOOL_UNIVERSAL_BRIDGE_LOADED__ = true;
7 | const EVENT_NAME = {
8 | RESPONSE: 'chrome-mcp:response',
9 | CLEANUP: 'chrome-mcp:cleanup',
10 | EXECUTE: 'chrome-mcp:execute',
11 | };
12 | const pendingRequests = new Map();
13 |
14 | const messageHandler = (request, _sender, sendResponse) => {
15 | // --- Lifecycle Command ---
16 | if (request.type === EVENT_NAME.CLEANUP) {
17 | window.dispatchEvent(new CustomEvent(EVENT_NAME.CLEANUP));
18 | // Acknowledge cleanup signal received, but don't hold the connection.
19 | sendResponse({ success: true });
20 | return true;
21 | }
22 |
23 | // --- Execution Command for MAIN world ---
24 | if (request.targetWorld === 'MAIN') {
25 | const requestId = `req-${Date.now()}-${Math.random()}`;
26 | pendingRequests.set(requestId, sendResponse);
27 |
28 | window.dispatchEvent(
29 | new CustomEvent(EVENT_NAME.EXECUTE, {
30 | detail: {
31 | action: request.action,
32 | payload: request.payload,
33 | requestId: requestId,
34 | },
35 | }),
36 | );
37 | return true; // Async response is expected.
38 | }
39 | // Note: Requests for ISOLATED world are handled by the user's isolatedWorldCode script directly.
40 | // This listener won't process them unless it's the only script in ISOLATED world.
41 | };
42 |
43 | chrome.runtime.onMessage.addListener(messageHandler);
44 |
45 | // Listen for responses coming back from the MAIN world.
46 | const responseHandler = (event) => {
47 | const { requestId, data, error } = event.detail;
48 | if (pendingRequests.has(requestId)) {
49 | const sendResponse = pendingRequests.get(requestId);
50 | sendResponse({ data, error });
51 | pendingRequests.delete(requestId);
52 | }
53 | };
54 | window.addEventListener(EVENT_NAME.RESPONSE, responseHandler);
55 |
56 | // --- Self Cleanup ---
57 | // When the cleanup signal arrives, this bridge must also clean itself up.
58 | const cleanupHandler = () => {
59 | chrome.runtime.onMessage.removeListener(messageHandler);
60 | window.removeEventListener(EVENT_NAME.RESPONSE, responseHandler);
61 | window.removeEventListener(EVENT_NAME.CLEANUP, cleanupHandler);
62 | delete window.__INJECT_SCRIPT_TOOL_UNIVERSAL_BRIDGE_LOADED__;
63 | };
64 | window.addEventListener(EVENT_NAME.CLEANUP, cleanupHandler);
65 | })();
66 |
```
--------------------------------------------------------------------------------
/docs/VisualEditor.md:
--------------------------------------------------------------------------------
```markdown
1 | # A Visual Editor for Claude Code & Codex
2 |
3 | **How to enable:**
4 | `Right Click > Chrome MCP Server > Toggle Web Editing Mode`
5 | **Shortcut:** `Cmd/Ctrl` + `Shift` + `O`
6 |
7 | ### Interactive Sizing & Layout Adjustment
8 |
9 | Directly drag element edges on the canvas to adjust width, height, and font sizes. All visual manipulations are automatically converted into code suggestions and applied to your source code by the Agent, bridging the gap between design and development in real-time.
10 |
11 | <div align="center">
12 | <a href="https://youtu.be/76_DsUU7aHs">
13 | <img src="https://img.youtube.com/vi/76_DsUU7aHs/maxresdefault.jpg" alt="Interactive Sizing & Layout Adjustment" style="width:100%; max-width:600px;">
14 | </a>
15 | </div>
16 |
17 | ### Visual Property Controls
18 |
19 | Manage CSS properties directly through a visual inspector panel. Effortlessly tweak Flex/Grid layouts, margins, padding, and styling details with a single click. Perfect for rapid prototyping or UI fine-tuning, significantly reducing the time spent writing raw CSS.
20 |
21 | <div align="center">
22 | <a href="https://youtu.be/ADOzT7El2mI">
23 | <img src="https://img.youtube.com/vi/76_DsUU7aHs/maxresdefault.jpg" alt="Interactive Sizing & Layout Adjustment" style="width:100%; max-width:600px;">
24 | </a>
25 | </div>
26 |
27 | ### Live Component State Debugging (Vue/React)
28 |
29 | Inspect and modify React and Vue component props in real-time. Test how your components render under different data states without ever leaving your current view or writing temporary console logs.
30 |
31 | <div align="center">
32 | <a href="https://youtu.be/PaIxdpGcEEk">
33 | <img src="https://img.youtube.com/vi/76_DsUU7aHs/maxresdefault.jpg" alt="Interactive Sizing & Layout Adjustment" style="width:100%; max-width:600px;">
34 | </a>
35 | </div>
36 |
37 | ### Point, Click & Prompt
38 |
39 | Select any element on the page and send instructions directly to Claude Code or Codex. The tool automatically captures the component's structure and context, enabling the AI to provide modifications with far greater precision and lower latency than global chat contexts.
40 |
41 | Simply click an element and say, _"Make this bigger"_ or _"Change the background to red"_, and watch Claude Code implement the exact changes in seconds.
42 |
43 | <div align="center">
44 | <a href="https://youtu.be/dSkt5HaTU_s">
45 | <img src="https://img.youtube.com/vi/76_DsUU7aHs/maxresdefault.jpg" alt="Interactive Sizing & Layout Adjustment" style="width:100%; max-width:600px;">
46 | </a>
47 | </div>
48 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/keepalive-manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Keepalive Manager
3 | * @description Global singleton service for managing Service Worker keepalive.
4 | *
5 | * This module provides a unified interface for acquiring and releasing keepalive
6 | * references. Multiple modules can acquire keepalive independently using tags,
7 | * and the underlying keepalive mechanism will remain active as long as at least
8 | * one reference is held.
9 | */
10 |
11 | import {
12 | createOffscreenKeepaliveController,
13 | type KeepaliveController,
14 | } from './record-replay-v3/engine/keepalive/offscreen-keepalive';
15 |
16 | const LOG_PREFIX = '[KeepaliveManager]';
17 |
18 | /**
19 | * Singleton keepalive controller instance.
20 | * Created lazily to avoid initialization issues during module loading.
21 | */
22 | let controller: KeepaliveController | null = null;
23 |
24 | /**
25 | * Get or create the singleton keepalive controller.
26 | */
27 | function getController(): KeepaliveController {
28 | if (!controller) {
29 | controller = createOffscreenKeepaliveController({ logger: console });
30 | console.debug(`${LOG_PREFIX} Controller initialized`);
31 | }
32 | return controller;
33 | }
34 |
35 | /**
36 | * Acquire a keepalive reference with a tag.
37 | *
38 | * @param tag - Identifier for the reference (e.g., 'native-host', 'rr-engine')
39 | * @returns A release function to call when keepalive is no longer needed
40 | *
41 | * @example
42 | * ```typescript
43 | * const release = acquireKeepalive('native-host');
44 | * // ... do work that needs SW to stay alive ...
45 | * release(); // Release when done
46 | * ```
47 | */
48 | export function acquireKeepalive(tag: string): () => void {
49 | try {
50 | const release = getController().acquire(tag);
51 | console.debug(`${LOG_PREFIX} Acquired keepalive for tag: ${tag}`);
52 | return () => {
53 | try {
54 | release();
55 | console.debug(`${LOG_PREFIX} Released keepalive for tag: ${tag}`);
56 | } catch (error) {
57 | console.warn(`${LOG_PREFIX} Failed to release keepalive for ${tag}:`, error);
58 | }
59 | };
60 | } catch (error) {
61 | console.warn(`${LOG_PREFIX} Failed to acquire keepalive for ${tag}:`, error);
62 | return () => {};
63 | }
64 | }
65 |
66 | /**
67 | * Check if keepalive is currently active (any references held).
68 | */
69 | export function isKeepaliveActive(): boolean {
70 | try {
71 | return getController().isActive();
72 | } catch {
73 | return false;
74 | }
75 | }
76 |
77 | /**
78 | * Get the current keepalive reference count.
79 | * Useful for debugging.
80 | */
81 | export function getKeepaliveRefCount(): number {
82 | try {
83 | return getController().getRefCount();
84 | } catch {
85 | return 0;
86 | }
87 | }
88 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/nodes/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { Step } from '../types';
2 | import type { ExecCtx, ExecResult, NodeRuntime } from './types';
3 | import { clickNode, dblclickNode } from './click';
4 | import { fillNode } from './fill';
5 | import { httpNode } from './http';
6 | import { extractNode } from './extract';
7 | import { scriptNode } from './script';
8 | import { openTabNode, switchTabNode, closeTabNode } from './tabs';
9 | import { scrollNode } from './scroll';
10 | import { dragNode } from './drag';
11 | import { keyNode } from './key';
12 | import { waitNode } from './wait';
13 | import { assertNode } from './assert';
14 | import { navigateNode } from './navigate';
15 | import { ifNode } from './conditional';
16 | import { STEP_TYPES } from 'chrome-mcp-shared';
17 | import { foreachNode, whileNode } from './loops';
18 | import { executeFlowNode } from './execute-flow';
19 | import {
20 | handleDownloadNode,
21 | screenshotNode,
22 | triggerEventNode,
23 | setAttributeNode,
24 | switchFrameNode,
25 | loopElementsNode,
26 | } from './download-screenshot-attr-event-frame-loop';
27 |
28 | const registry = new Map<string, NodeRuntime<any>>([
29 | [STEP_TYPES.CLICK, clickNode],
30 | [STEP_TYPES.DBLCLICK, dblclickNode],
31 | [STEP_TYPES.FILL, fillNode],
32 | [STEP_TYPES.HTTP, httpNode],
33 | [STEP_TYPES.EXTRACT, extractNode],
34 | [STEP_TYPES.SCRIPT, scriptNode],
35 | [STEP_TYPES.OPEN_TAB, openTabNode],
36 | [STEP_TYPES.SWITCH_TAB, switchTabNode],
37 | [STEP_TYPES.CLOSE_TAB, closeTabNode],
38 | [STEP_TYPES.SCROLL, scrollNode],
39 | [STEP_TYPES.DRAG, dragNode],
40 | [STEP_TYPES.KEY, keyNode],
41 | [STEP_TYPES.WAIT, waitNode],
42 | [STEP_TYPES.ASSERT, assertNode],
43 | [STEP_TYPES.NAVIGATE, navigateNode],
44 | [STEP_TYPES.IF, ifNode],
45 | [STEP_TYPES.FOREACH, foreachNode],
46 | [STEP_TYPES.WHILE, whileNode],
47 | [STEP_TYPES.EXECUTE_FLOW, executeFlowNode],
48 | [STEP_TYPES.HANDLE_DOWNLOAD, handleDownloadNode],
49 | [STEP_TYPES.SCREENSHOT, screenshotNode],
50 | [STEP_TYPES.TRIGGER_EVENT, triggerEventNode],
51 | [STEP_TYPES.SET_ATTRIBUTE, setAttributeNode],
52 | [STEP_TYPES.SWITCH_FRAME, switchFrameNode],
53 | [STEP_TYPES.LOOP_ELEMENTS, loopElementsNode],
54 | ]);
55 |
56 | export async function executeStep(ctx: ExecCtx, step: Step): Promise<ExecResult> {
57 | const rt = registry.get(step.type);
58 | if (!rt) throw new Error(`unsupported step type: ${String(step.type)}`);
59 | const v = rt.validate ? rt.validate(step) : { ok: true };
60 | if (!v.ok) throw new Error((v.errors || []).join(', ') || 'validation failed');
61 | const out = await rt.run(ctx, step);
62 | return out || {};
63 | }
64 |
65 | export type { ExecCtx, ExecResult, NodeRuntime } from './types';
66 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/properties/PropertyScroll.vue:
--------------------------------------------------------------------------------
```vue
1 | <template>
2 | <div>
3 | <div class="form-row">
4 | <label class="form-label">模式</label>
5 | <select v-model="cfg.mode" class="form-select-sm">
6 | <option value="element">滚动到元素</option>
7 | <option value="offset">窗口偏移</option>
8 | <option value="container">容器偏移</option>
9 | </select>
10 | </div>
11 |
12 | <div v-if="cfg.mode === 'element'" class="mt-2">
13 | <SelectorEditor :node="node" :allowPick="true" title="目标元素" targetKey="target" />
14 | </div>
15 |
16 | <div v-if="cfg.mode !== 'element'" class="mt-2">
17 | <div class="form-row">
18 | <label class="form-label">偏移 X</label>
19 | <input type="number" class="form-input-sm" v-model.number="cfg.offset.x" placeholder="0" />
20 | </div>
21 | <div class="form-row">
22 | <label class="form-label">偏移 Y</label>
23 | <input
24 | type="number"
25 | class="form-input-sm"
26 | v-model.number="cfg.offset.y"
27 | placeholder="300"
28 | />
29 | </div>
30 | <div v-if="cfg.mode === 'container'" class="mt-2">
31 | <SelectorEditor :node="node" :allowPick="true" title="容器选择器" targetKey="target" />
32 | <div class="hint"><small>容器需支持 scrollTo(top,left)</small></div>
33 | </div>
34 | </div>
35 | </div>
36 | </template>
37 |
38 | <script lang="ts" setup>
39 | /* eslint-disable vue/no-mutating-props */
40 | import type { NodeBase } from '@/entrypoints/background/record-replay/types';
41 | import SelectorEditor from './SelectorEditor.vue';
42 |
43 | const props = defineProps<{ node: NodeBase }>();
44 |
45 | function ensure() {
46 | const n: any = props.node;
47 | n.config = n.config || {};
48 | if (!n.config.mode) n.config.mode = 'offset';
49 | if (!n.config.offset) n.config.offset = { x: 0, y: 300 };
50 | if (!n.config.target) n.config.target = { candidates: [] };
51 | }
52 |
53 | const cfg = {
54 | get mode() {
55 | ensure();
56 | return (props.node as any).config.mode;
57 | },
58 | set mode(v: any) {
59 | ensure();
60 | (props.node as any).config.mode = v;
61 | },
62 | get offset() {
63 | ensure();
64 | return (props.node as any).config.offset;
65 | },
66 | set offset(v: any) {
67 | ensure();
68 | (props.node as any).config.offset = v;
69 | },
70 | } as any;
71 | </script>
72 |
73 | <style scoped>
74 | .hint {
75 | color: #64748b;
76 | margin-top: 8px;
77 | }
78 | .mt-2 {
79 | margin-top: 8px;
80 | }
81 | .form-row {
82 | display: flex;
83 | align-items: center;
84 | gap: 8px;
85 | margin: 6px 0;
86 | }
87 | .form-label {
88 | width: 80px;
89 | color: #334155;
90 | font-size: 12px;
91 | }
92 | .form-input-sm,
93 | .form-select-sm {
94 | flex: 1;
95 | padding: 6px 8px;
96 | border: 1px solid var(--rr-border);
97 | border-radius: 6px;
98 | }
99 | </style>
100 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/widgets/FieldTargetLocator.vue:
--------------------------------------------------------------------------------
```vue
1 | <template>
2 | <div class="target-locator">
3 | <!-- Reuse FieldSelector UI for picking/typing a selector -->
4 | <FieldSelector v-model="text" :field="{ placeholder }" />
5 | </div>
6 | </template>
7 |
8 | <script lang="ts" setup>
9 | import { ref, watch, nextTick } from 'vue';
10 | import FieldSelector from './FieldSelector.vue';
11 |
12 | type Candidate = { type: 'css' | 'attr' | 'aria' | 'text' | 'xpath'; value: string };
13 | type TargetLocator = { ref?: string; candidates?: Candidate[] };
14 |
15 | const props = defineProps<{ modelValue?: TargetLocator | string; field?: any }>();
16 | const emit = defineEmits<{ (e: 'update:modelValue', v?: TargetLocator): void }>();
17 |
18 | const placeholder = props.field?.placeholder || '.btn.primary';
19 | const text = ref<string>('');
20 | // guard to prevent emitting during initial/prop-driven sync
21 | const updatingFromProps = ref<boolean>(false);
22 |
23 | // derive text from incoming modelValue (supports string or structured object)
24 | watch(
25 | () => props.modelValue,
26 | (mv: any) => {
27 | updatingFromProps.value = true;
28 | if (!mv) {
29 | text.value = '';
30 | nextTick(() => (updatingFromProps.value = false));
31 | return;
32 | }
33 | if (typeof mv === 'string') {
34 | text.value = mv;
35 | nextTick(() => (updatingFromProps.value = false));
36 | return;
37 | }
38 | try {
39 | const arr: Candidate[] = Array.isArray(mv.candidates) ? mv.candidates : [];
40 | const prefer = ['css', 'attr', 'aria', 'text', 'xpath'];
41 | let val = '';
42 | for (const t of prefer) {
43 | const c = arr.find((x) => x && x.type === t && x.value);
44 | if (c) {
45 | val = String(c.value || '');
46 | break;
47 | }
48 | }
49 | if (!val) val = arr[0]?.value ? String(arr[0].value) : '';
50 | text.value = val;
51 | } catch {
52 | text.value = '';
53 | }
54 | nextTick(() => (updatingFromProps.value = false));
55 | },
56 | { immediate: true, deep: true },
57 | );
58 |
59 | // whenever text changes, emit structured TargetLocator (skip when syncing from props)
60 | watch(
61 | () => text.value,
62 | (v) => {
63 | if (updatingFromProps.value) return;
64 | const s = String(v || '').trim();
65 | if (!s) {
66 | emit('update:modelValue', { candidates: [] });
67 | } else {
68 | emit('update:modelValue', {
69 | ...(typeof props.modelValue === 'object' && props.modelValue
70 | ? (props.modelValue as any)
71 | : {}),
72 | candidates: [{ type: 'css', value: s }],
73 | });
74 | }
75 | },
76 | );
77 | </script>
78 |
79 | <style scoped>
80 | .target-locator {
81 | display: flex;
82 | flex-direction: column;
83 | gap: 4px;
84 | }
85 | </style>
86 |
```
--------------------------------------------------------------------------------
/app/native-server/src/agent/tool-bridge.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2 | import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3 | import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
4 | import { NATIVE_SERVER_PORT } from '../constant/index.js';
5 |
6 | export interface CliToolInvocation {
7 | /**
8 | * The MCP server identifier (if provided by CLI).
9 | * When omitted, this bridge defaults to the local chrome MCP server.
10 | */
11 | server?: string;
12 | /**
13 | * The MCP tool name to invoke.
14 | */
15 | tool: string;
16 | /**
17 | * JSON-serializable arguments for the tool call.
18 | */
19 | args?: Record<string, unknown>;
20 | }
21 |
22 | export interface AgentToolBridgeOptions {
23 | /**
24 | * Base URL of the local MCP HTTP endpoint (e.g. http://127.0.0.1:12306/mcp).
25 | * If omitted, DEFAULT_SERVER_PORT from chrome-mcp-shared is used.
26 | */
27 | mcpUrl?: string;
28 | }
29 |
30 | /**
31 | * AgentToolBridge maps CLI tool events (Codex, etc.) to MCP tool calls
32 | * against the local chrome MCP server via the official MCP SDK client.
33 | *
34 | * 中文说明:该桥接层负责将 CLI 上报的工具调用统一转为标准 MCP CallTool 请求,
35 | * 复用现有 /mcp HTTP server,而不是在本项目内自研额外协议。
36 | */
37 | export class AgentToolBridge {
38 | private readonly client: Client;
39 | private readonly transport: StreamableHTTPClientTransport;
40 |
41 | constructor(options: AgentToolBridgeOptions = {}) {
42 | const url =
43 | options.mcpUrl || `http://127.0.0.1:${process.env.MCP_HTTP_PORT || NATIVE_SERVER_PORT}/mcp`;
44 |
45 | this.transport = new StreamableHTTPClientTransport(new URL(url));
46 | this.client = new Client(
47 | {
48 | name: 'chrome-mcp-agent-bridge',
49 | version: '1.0.0',
50 | },
51 | {},
52 | );
53 | }
54 |
55 | /**
56 | * Connects the MCP client over Streamable HTTP if not already connected.
57 | */
58 | async ensureConnected(): Promise<void> {
59 | // Client.connect is idempotent; repeated calls reuse the same transport session.
60 | if ((this.transport as any)._sessionId) {
61 | return;
62 | }
63 | await this.client.connect(this.transport);
64 | }
65 |
66 | /**
67 | * Invoke an MCP tool based on a CLI tool event.
68 | * Returns the raw result from MCP client.callTool().
69 | */
70 | async callTool(invocation: CliToolInvocation): Promise<CallToolResult> {
71 | await this.ensureConnected();
72 |
73 | const args = invocation.args ?? {};
74 | const result = await this.client.callTool({
75 | name: invocation.tool,
76 | arguments: args,
77 | });
78 |
79 | // The SDK returns a compatible structure; cast to satisfy strict typing.
80 | return result as unknown as CallToolResult;
81 | }
82 | }
83 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/sidepanel/composables/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Agent Chat Composables
3 | * Export all composables for agent chat functionality.
4 | */
5 | export { useAgentServer } from './useAgentServer';
6 | export { useAgentChat } from './useAgentChat';
7 | export { useAgentProjects } from './useAgentProjects';
8 | export { useAgentSessions } from './useAgentSessions';
9 | export { useAttachments, type AttachmentWithPreview } from './useAttachments';
10 | export { useAgentTheme, preloadAgentTheme, THEME_LABELS } from './useAgentTheme';
11 | export { useAgentThreads, AGENT_SERVER_PORT_KEY } from './useAgentThreads';
12 | export { useWebEditorTxState, WEB_EDITOR_TX_STATE_INJECTION_KEY } from './useWebEditorTxState';
13 | export { useAgentChatViewRoute } from './useAgentChatViewRoute';
14 |
15 | export type { UseAgentServerOptions } from './useAgentServer';
16 | export type { UseAgentChatOptions } from './useAgentChat';
17 | export type { UseAgentProjectsOptions } from './useAgentProjects';
18 | export type { UseAgentSessionsOptions } from './useAgentSessions';
19 | export type { AgentThemeId, UseAgentTheme } from './useAgentTheme';
20 | export type {
21 | AgentThread,
22 | TimelineItem,
23 | ToolPresentation,
24 | ToolKind,
25 | ToolSeverity,
26 | AgentThreadState,
27 | UseAgentThreadsOptions,
28 | ThreadHeader,
29 | WebEditorApplyMeta,
30 | } from './useAgentThreads';
31 | export type { UseWebEditorTxStateOptions, WebEditorTxStateReturn } from './useWebEditorTxState';
32 | export type {
33 | AgentChatView,
34 | AgentChatRouteState,
35 | UseAgentChatViewRouteOptions,
36 | UseAgentChatViewRoute,
37 | } from './useAgentChatViewRoute';
38 |
39 | // RR V3 Composables
40 | export { useRRV3Rpc } from './useRRV3Rpc';
41 | export { useRRV3Debugger } from './useRRV3Debugger';
42 | export type { UseRRV3Rpc, UseRRV3RpcOptions, RpcRequestOptions } from './useRRV3Rpc';
43 | export type { UseRRV3Debugger, UseRRV3DebuggerOptions } from './useRRV3Debugger';
44 |
45 | // Textarea Auto-Resize
46 | export { useTextareaAutoResize } from './useTextareaAutoResize';
47 | export type {
48 | UseTextareaAutoResizeOptions,
49 | UseTextareaAutoResizeReturn,
50 | } from './useTextareaAutoResize';
51 |
52 | // Fake Caret (comet tail animation)
53 | export { useFakeCaret } from './useFakeCaret';
54 | export type { UseFakeCaretOptions, UseFakeCaretReturn, FakeCaretTrailPoint } from './useFakeCaret';
55 |
56 | // Open Project Preference
57 | export { useOpenProjectPreference } from './useOpenProjectPreference';
58 | export type {
59 | UseOpenProjectPreferenceOptions,
60 | UseOpenProjectPreference,
61 | } from './useOpenProjectPreference';
62 |
63 | // Agent Input Preferences (fake caret, etc.)
64 | export { useAgentInputPreferences } from './useAgentInputPreferences';
65 | export type { UseAgentInputPreferences } from './useAgentInputPreferences';
66 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/common/element-marker-types.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Element marker types shared across background, content scripts, and popup
2 |
3 | export type UrlMatchType = 'exact' | 'prefix' | 'host';
4 |
5 | export interface ElementMarker {
6 | id: string;
7 | // Original URL where the marker was created
8 | url: string;
9 | // Normalized pieces to support matching
10 | origin: string; // scheme + host + port
11 | host: string; // hostname
12 | path: string; // pathname part only
13 | matchType: UrlMatchType; // default: 'prefix'
14 |
15 | name: string; // Human-friendly name, e.g., "Login Button"
16 | selector: string; // Selector string
17 | selectorType?: 'css' | 'xpath'; // Default: css
18 | listMode?: boolean; // Whether this marker was created in list mode (allows multiple matches)
19 | action?: 'click' | 'fill' | 'custom'; // Intended action hint (optional)
20 |
21 | createdAt: number;
22 | updatedAt: number;
23 | }
24 |
25 | export interface UpsertMarkerRequest {
26 | id?: string;
27 | url: string;
28 | name: string;
29 | selector: string;
30 | selectorType?: 'css' | 'xpath';
31 | listMode?: boolean;
32 | matchType?: UrlMatchType;
33 | action?: 'click' | 'fill' | 'custom';
34 | }
35 |
36 | // Validation actions for MCP-integrated verification
37 | export enum MarkerValidationAction {
38 | Hover = 'hover',
39 | LeftClick = 'left_click',
40 | RightClick = 'right_click',
41 | DoubleClick = 'double_click',
42 | TypeText = 'type_text',
43 | PressKeys = 'press_keys',
44 | Scroll = 'scroll',
45 | }
46 |
47 | export interface MarkerValidationRequest {
48 | selector: string;
49 | selectorType?: 'css' | 'xpath';
50 | action: MarkerValidationAction;
51 | // Optional payload for certain actions
52 | text?: string; // for type_text
53 | keys?: string; // for press_keys
54 | // Event options for click-like actions
55 | button?: 'left' | 'right' | 'middle';
56 | bubbles?: boolean;
57 | cancelable?: boolean;
58 | modifiers?: { altKey?: boolean; ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean };
59 | // Targeting options
60 | coordinates?: { x: number; y: number }; // absolute viewport coords
61 | offsetX?: number; // relative to element center if relativeTo = 'element'
62 | offsetY?: number;
63 | relativeTo?: 'element' | 'viewport';
64 | // Navigation options for click-like actions
65 | waitForNavigation?: boolean;
66 | timeoutMs?: number;
67 | // Scroll options
68 | scrollDirection?: 'up' | 'down' | 'left' | 'right';
69 | scrollAmount?: number; // pixels per tick
70 | }
71 |
72 | export interface MarkerValidationResponse {
73 | success: boolean;
74 | resolved?: boolean;
75 | ref?: string;
76 | center?: { x: number; y: number };
77 | tool?: { name: string; ok: boolean; error?: string };
78 | error?: string;
79 | }
80 |
81 | export interface MarkerQuery {
82 | url?: string; // If present, query by URL match; otherwise list all
83 | }
84 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/tests/vitest.setup.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Vitest Global Setup
3 | * @description Provides global configuration and polyfills for test environment
4 | */
5 |
6 | import { vi } from 'vitest';
7 |
8 | // Provide IndexedDB globals (jsdom doesn't include them)
9 | import 'fake-indexeddb/auto';
10 |
11 | // Mock chrome API (basic placeholder)
12 | if (typeof globalThis.chrome === 'undefined') {
13 | (globalThis as unknown as { chrome: object }).chrome = {
14 | runtime: {
15 | id: 'test-extension-id',
16 | sendMessage: vi.fn().mockResolvedValue(undefined),
17 | onMessage: {
18 | addListener: vi.fn(),
19 | removeListener: vi.fn(),
20 | },
21 | connect: vi.fn().mockReturnValue({
22 | onMessage: { addListener: vi.fn(), removeListener: vi.fn() },
23 | onDisconnect: { addListener: vi.fn(), removeListener: vi.fn() },
24 | postMessage: vi.fn(),
25 | disconnect: vi.fn(),
26 | }),
27 | },
28 | storage: {
29 | local: {
30 | get: vi.fn().mockResolvedValue({}),
31 | set: vi.fn().mockResolvedValue(undefined),
32 | remove: vi.fn().mockResolvedValue(undefined),
33 | },
34 | },
35 | tabs: {
36 | query: vi.fn().mockResolvedValue([]),
37 | get: vi.fn().mockResolvedValue(null),
38 | create: vi.fn().mockResolvedValue({ id: 1 }),
39 | update: vi.fn().mockResolvedValue({}),
40 | remove: vi.fn().mockResolvedValue(undefined),
41 | captureVisibleTab: vi.fn().mockResolvedValue('data:image/png;base64,'),
42 | onRemoved: { addListener: vi.fn(), removeListener: vi.fn() },
43 | onCreated: { addListener: vi.fn(), removeListener: vi.fn() },
44 | onUpdated: { addListener: vi.fn(), removeListener: vi.fn() },
45 | },
46 | webRequest: {
47 | onBeforeRequest: { addListener: vi.fn(), removeListener: vi.fn() },
48 | onCompleted: { addListener: vi.fn(), removeListener: vi.fn() },
49 | onErrorOccurred: { addListener: vi.fn(), removeListener: vi.fn() },
50 | },
51 | webNavigation: {
52 | onCommitted: { addListener: vi.fn(), removeListener: vi.fn() },
53 | onDOMContentLoaded: { addListener: vi.fn(), removeListener: vi.fn() },
54 | onCompleted: { addListener: vi.fn(), removeListener: vi.fn() },
55 | },
56 | debugger: {
57 | onEvent: { addListener: vi.fn(), removeListener: vi.fn() },
58 | onDetach: { addListener: vi.fn(), removeListener: vi.fn() },
59 | attach: vi.fn().mockResolvedValue(undefined),
60 | detach: vi.fn().mockResolvedValue(undefined),
61 | sendCommand: vi.fn().mockResolvedValue({}),
62 | },
63 | commands: {
64 | onCommand: { addListener: vi.fn(), removeListener: vi.fn() },
65 | },
66 | contextMenus: {
67 | create: vi.fn(),
68 | remove: vi.fn(),
69 | onClicked: { addListener: vi.fn(), removeListener: vi.fn() },
70 | },
71 | };
72 | }
73 |
```