#
tokens: 48750/50000 38/574 files (page 3/43)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 of 43. Use http://codebase.md/hangwin/mcp-chrome?page={x} to view the full context.

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/element-marker/element-marker-storage.ts:
--------------------------------------------------------------------------------

```typescript
// IndexedDB storage for element markers (URL -> marked selectors)
// Uses the shared IndexedDbClient for robust transaction handling.

import { IndexedDbClient } from '@/utils/indexeddb-client';
import type { ElementMarker, UpsertMarkerRequest } from '@/common/element-marker-types';

const DB_NAME = 'element_marker_storage';
const DB_VERSION = 1;
const STORE = 'markers';

const idb = new IndexedDbClient(DB_NAME, DB_VERSION, (db, oldVersion) => {
  switch (oldVersion) {
    case 0: {
      const store = db.createObjectStore(STORE, { keyPath: 'id' });
      // Useful indexes for lookups
      store.createIndex('by_host', 'host', { unique: false });
      store.createIndex('by_origin', 'origin', { unique: false });
      store.createIndex('by_path', 'path', { unique: false });
    }
  }
});

function normalizeUrl(raw: string): { url: string; origin: string; host: string; path: string } {
  try {
    const u = new URL(raw);
    return { url: raw, origin: u.origin, host: u.hostname, path: u.pathname };
  } catch {
    return { url: raw, origin: '', host: '', path: '' };
  }
}

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

export async function listAllMarkers(): Promise<ElementMarker[]> {
  return idb.getAll<ElementMarker>(STORE);
}

export async function listMarkersForUrl(url: string): Promise<ElementMarker[]> {
  const { origin, path, host } = normalizeUrl(url);
  const all = await idb.getAll<ElementMarker>(STORE);
  // Simple matching policy:
  // - exact: origin + path must match exactly
  // - prefix: origin matches and marker.path is a prefix of current path
  // - host: host matches regardless of path
  return all.filter((m) => {
    if (!m) return false;
    if (m.matchType === 'exact') return m.origin === origin && m.path === path;
    if (m.matchType === 'host') return !!m.host && m.host === host;
    // default 'prefix'
    return m.origin === origin && (m.path ? path.startsWith(m.path) : true);
  });
}

export async function saveMarker(req: UpsertMarkerRequest): Promise<ElementMarker> {
  const { url: rawUrl, selector } = req;
  if (!rawUrl || !selector) throw new Error('url and selector are required');
  const { url, origin, host, path } = normalizeUrl(rawUrl);
  const ts = now();
  const marker: ElementMarker = {
    id: req.id || (globalThis.crypto?.randomUUID?.() ?? `${ts}_${Math.random()}`),
    url,
    origin,
    host,
    path,
    matchType: req.matchType || 'prefix',
    name: req.name || selector,
    selector,
    selectorType: req.selectorType || 'css',
    listMode: req.listMode || false,
    action: req.action || 'custom',
    createdAt: ts,
    updatedAt: ts,
  };
  await idb.put<ElementMarker>(STORE, marker);
  return marker;
}

export async function updateMarker(marker: ElementMarker): Promise<void> {
  const existing = await idb.get<ElementMarker>(STORE, marker.id);
  if (!existing) throw new Error('marker not found');

  // Preserve createdAt from existing record, only update updatedAt
  const updated: ElementMarker = {
    ...marker,
    createdAt: existing.createdAt, // Never overwrite createdAt
    updatedAt: now(),
  };
  await idb.put<ElementMarker>(STORE, updated);
}

export async function deleteMarker(id: string): Promise<void> {
  await idb.delete(STORE, id);
}

```

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

```typescript
// engine/policies/wait.ts — wrappers around rr-utils navigation/network waits
// Keep logic centralized to avoid duplication in schedulers and nodes

import { handleCallTool } from '@/entrypoints/background/tools';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { waitForNavigation as rrWaitForNavigation, waitForNetworkIdle } from '../../rr-utils';

export async function waitForNavigationDone(prevUrl: string, timeoutMs?: number) {
  await rrWaitForNavigation(timeoutMs, prevUrl);
}

export async function ensureReadPageIfWeb() {
  try {
    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
    const url = tabs?.[0]?.url || '';
    if (/^(https?:|file:)/i.test(url)) {
      await handleCallTool({ name: TOOL_NAMES.BROWSER.READ_PAGE, args: {} });
    }
  } catch {}
}

export async function maybeQuickWaitForNav(prevUrl: string, timeoutMs?: number) {
  try {
    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
    const tabId = tabs?.[0]?.id;
    if (typeof tabId !== 'number') return;
    const sniffMs = 350;
    const startedAt = Date.now();
    let seen = false;
    await new Promise<void>((resolve) => {
      let timer: any = null;
      const cleanup = () => {
        try {
          chrome.webNavigation.onCommitted.removeListener(onCommitted);
        } catch {}
        try {
          chrome.webNavigation.onCompleted.removeListener(onCompleted);
        } catch {}
        try {
          (chrome.webNavigation as any).onHistoryStateUpdated?.removeListener?.(
            onHistoryStateUpdated,
          );
        } catch {}
        try {
          chrome.tabs.onUpdated.removeListener(onUpdated);
        } catch {}
        if (timer) {
          try {
            clearTimeout(timer);
          } catch {}
        }
      };
      const finish = async () => {
        cleanup();
        if (seen) {
          try {
            await rrWaitForNavigation(
              prevUrl ? Math.min(timeoutMs || 15000, 30000) : undefined,
              prevUrl,
            );
          } catch {}
        }
        resolve();
      };
      const mark = () => {
        seen = true;
      };
      const onCommitted = (d: any) => {
        if (d.tabId === tabId && d.frameId === 0 && d.timeStamp >= startedAt) mark();
      };
      const onCompleted = (d: any) => {
        if (d.tabId === tabId && d.frameId === 0 && d.timeStamp >= startedAt) mark();
      };
      const onHistoryStateUpdated = (d: any) => {
        if (d.tabId === tabId && d.frameId === 0 && d.timeStamp >= startedAt) mark();
      };
      const onUpdated = (updatedId: number, change: chrome.tabs.TabChangeInfo) => {
        if (updatedId !== tabId) return;
        if (change.status === 'loading') mark();
        if (typeof change.url === 'string' && (!prevUrl || change.url !== prevUrl)) mark();
      };

      chrome.webNavigation.onCommitted.addListener(onCommitted);
      chrome.webNavigation.onCompleted.addListener(onCompleted);
      try {
        (chrome.webNavigation as any).onHistoryStateUpdated?.addListener?.(onHistoryStateUpdated);
      } catch {}
      chrome.tabs.onUpdated.addListener(onUpdated);
      timer = setTimeout(finish, sniffMs);
    });
  } catch {}
}

export { waitForNetworkIdle };

```

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

```typescript
/**
 * Screenshot Action Handler
 *
 * Captures screenshots and optionally stores base64 data in variables.
 * Supports full page, selector-based, and viewport screenshots.
 */

import { handleCallTool } from '@/entrypoints/background/tools';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { failed, invalid, ok } from '../registry';
import type { ActionHandler } from '../types';
import { resolveString } from './common';

/** Extract text content from tool result */
function extractToolText(result: unknown): string | undefined {
  const content = (result as { content?: Array<{ type?: string; text?: string }> })?.content;
  const text = content?.find((c) => c?.type === 'text' && typeof c.text === 'string')?.text;
  return typeof text === 'string' && text.trim() ? text : undefined;
}

export const screenshotHandler: ActionHandler<'screenshot'> = {
  type: 'screenshot',

  validate: (action) => {
    const saveAs = action.params.saveAs;
    if (saveAs !== undefined && (!saveAs || String(saveAs).trim().length === 0)) {
      return invalid('saveAs must be a non-empty variable name when provided');
    }
    return ok();
  },

  describe: (action) => {
    if (action.params.fullPage) return 'Screenshot (full page)';
    if (typeof action.params.selector === 'string') {
      const sel =
        action.params.selector.length > 30
          ? action.params.selector.slice(0, 30) + '...'
          : action.params.selector;
      return `Screenshot: ${sel}`;
    }
    if (action.params.selector) return 'Screenshot (dynamic selector)';
    return 'Screenshot';
  },

  run: async (ctx, action) => {
    const tabId = ctx.tabId;
    if (typeof tabId !== 'number') {
      return failed('TAB_NOT_FOUND', 'No active tab found for screenshot action');
    }

    // Resolve optional selector
    let selector: string | undefined;
    if (action.params.selector !== undefined) {
      const resolved = resolveString(action.params.selector, ctx.vars);
      if (!resolved.ok) return failed('VALIDATION_ERROR', resolved.error);
      const s = resolved.value.trim();
      if (s) selector = s;
    }

    // Call screenshot tool
    const res = await handleCallTool({
      name: TOOL_NAMES.BROWSER.SCREENSHOT,
      args: {
        name: 'workflow',
        storeBase64: true,
        fullPage: action.params.fullPage === true,
        selector,
        tabId,
      },
    });

    if ((res as { isError?: boolean })?.isError) {
      return failed('UNKNOWN', extractToolText(res) || 'Screenshot failed');
    }

    // Parse response
    const text = extractToolText(res);
    if (!text) {
      return failed('UNKNOWN', 'Screenshot tool returned an empty response');
    }

    let payload: unknown;
    try {
      payload = JSON.parse(text);
    } catch {
      return failed('UNKNOWN', 'Screenshot tool returned invalid JSON');
    }

    const base64Data = (payload as { base64Data?: unknown })?.base64Data;
    if (typeof base64Data !== 'string' || base64Data.length === 0) {
      return failed('UNKNOWN', 'Screenshot tool returned empty base64Data');
    }

    // Store in variables if saveAs specified
    if (action.params.saveAs) {
      ctx.vars[action.params.saveAs] = base64Data;
    }

    return { status: 'success', output: { base64Data } };
  },
};

```

--------------------------------------------------------------------------------
/app/chrome-extension/utils/cdp-session-manager.ts:
--------------------------------------------------------------------------------

```typescript
import { TOOL_NAMES } from 'chrome-mcp-shared';

type OwnerTag = string;

interface TabSessionState {
  refCount: number;
  owners: Set<OwnerTag>;
  attachedByUs: boolean;
}

const DEBUGGER_PROTOCOL_VERSION = '1.3';

class CDPSessionManager {
  private sessions = new Map<number, TabSessionState>();

  private getState(tabId: number): TabSessionState | undefined {
    return this.sessions.get(tabId);
  }

  private setState(tabId: number, state: TabSessionState) {
    this.sessions.set(tabId, state);
  }

  async attach(tabId: number, owner: OwnerTag = 'unknown'): Promise<void> {
    const state = this.getState(tabId);
    if (state && state.attachedByUs) {
      state.refCount += 1;
      state.owners.add(owner);
      return;
    }

    // Check existing attachments
    const targets = await chrome.debugger.getTargets();
    const existing = targets.find((t) => t.tabId === tabId && t.attached);
    if (existing) {
      if (existing.extensionId === chrome.runtime.id) {
        // Already attached by us (e.g., previous tool). Adopt and refcount.
        this.setState(tabId, {
          refCount: state ? state.refCount + 1 : 1,
          owners: new Set([...(state?.owners || []), owner]),
          attachedByUs: true,
        });
        return;
      }
      // Another client (DevTools/other extension) is attached
      throw new Error(
        `Debugger is already attached to tab ${tabId} by another client (e.g., DevTools/extension)`,
      );
    }

    // Attach freshly
    await chrome.debugger.attach({ tabId }, DEBUGGER_PROTOCOL_VERSION);
    this.setState(tabId, { refCount: 1, owners: new Set([owner]), attachedByUs: true });
  }

  async detach(tabId: number, owner: OwnerTag = 'unknown'): Promise<void> {
    const state = this.getState(tabId);
    if (!state) return; // Nothing to do

    // Update ownership/refcount
    if (state.owners.has(owner)) state.owners.delete(owner);
    state.refCount = Math.max(0, state.refCount - 1);

    if (state.refCount > 0) {
      // Still in use by other owners
      return;
    }

    // We are the last owner
    try {
      if (state.attachedByUs) {
        await chrome.debugger.detach({ tabId });
      }
    } catch (e) {
      // Best-effort detach; ignore
    } finally {
      this.sessions.delete(tabId);
    }
  }

  /**
   * Convenience wrapper: ensures attach before fn, and balanced detach after.
   */
  async withSession<T>(tabId: number, owner: OwnerTag, fn: () => Promise<T>): Promise<T> {
    await this.attach(tabId, owner);
    try {
      return await fn();
    } finally {
      await this.detach(tabId, owner);
    }
  }

  /**
   * Send a CDP command. Requires that this manager has attached to the tab.
   * If not attached by us, will attempt a one-shot attach around the call.
   */
  async sendCommand<T = any>(tabId: number, method: string, params?: object): Promise<T> {
    const state = this.getState(tabId);
    if (state && state.attachedByUs) {
      return (await chrome.debugger.sendCommand({ tabId }, method, params)) as T;
    }
    // Fallback: temporary session
    return await this.withSession<T>(tabId, `send:${method}`, async () => {
      return (await chrome.debugger.sendCommand({ tabId }, method, params)) as T;
    });
  }
}

export const cdpSessionManager = new CDPSessionManager();

```

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

```typescript
/**
 * Navigate Action Handler
 *
 * Handles page navigation actions:
 * - Navigate to URL
 * - Page refresh
 * - Wait for navigation completion
 */

import { handleCallTool } from '@/entrypoints/background/tools';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { ENGINE_CONSTANTS } from '../../engine/constants';
import { ensureReadPageIfWeb, waitForNavigationDone } from '../../engine/policies/wait';
import { failed, invalid, ok } from '../registry';
import type { ActionHandler } from '../types';
import { clampInt, readTabUrl, resolveString } from './common';

export const navigateHandler: ActionHandler<'navigate'> = {
  type: 'navigate',

  validate: (action) => {
    const hasRefresh = action.params.refresh === true;
    const hasUrl = action.params.url !== undefined;
    return hasRefresh || hasUrl ? ok() : invalid('Missing url or refresh parameter');
  },

  describe: (action) => {
    if (action.params.refresh) return 'Refresh page';
    const url = typeof action.params.url === 'string' ? action.params.url : '(dynamic)';
    return `Navigate to ${url}`;
  },

  run: async (ctx, action) => {
    const vars = ctx.vars;
    const tabId = ctx.tabId;
    // Check if StepRunner owns nav-wait (skip internal nav-wait logic)
    const skipNavWait = ctx.execution?.skipNavWait === true;

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

    // Only read beforeUrl and calculate waitMs if we need to do nav-wait
    const beforeUrl = skipNavWait ? '' : await readTabUrl(tabId);
    const waitMs = skipNavWait
      ? 0
      : clampInt(
          action.policy?.timeout?.ms ?? ENGINE_CONSTANTS.DEFAULT_WAIT_MS,
          0,
          ENGINE_CONSTANTS.MAX_WAIT_MS,
        );

    // Handle page refresh
    if (action.params.refresh) {
      const result = await handleCallTool({
        name: TOOL_NAMES.BROWSER.NAVIGATE,
        args: { refresh: true, tabId },
      });

      if ((result as { isError?: boolean })?.isError) {
        const errorContent = (result as { content?: Array<{ text?: string }> })?.content;
        const errorMsg = errorContent?.[0]?.text || 'Page refresh failed';
        return failed('NAVIGATION_FAILED', errorMsg);
      }

      // Skip nav-wait if StepRunner handles it
      if (!skipNavWait) {
        await waitForNavigationDone(beforeUrl, waitMs);
        await ensureReadPageIfWeb();
      }
      return { status: 'success' };
    }

    // Handle URL navigation
    const urlResolved = resolveString(action.params.url, vars);
    if (!urlResolved.ok) {
      return failed('VALIDATION_ERROR', urlResolved.error);
    }

    const url = urlResolved.value.trim();
    if (!url) {
      return failed('VALIDATION_ERROR', 'URL is empty');
    }

    const result = await handleCallTool({
      name: TOOL_NAMES.BROWSER.NAVIGATE,
      args: { url, tabId },
    });

    if ((result as { isError?: boolean })?.isError) {
      const errorContent = (result as { content?: Array<{ text?: string }> })?.content;
      const errorMsg = errorContent?.[0]?.text || `Navigation to ${url} failed`;
      return failed('NAVIGATION_FAILED', errorMsg);
    }

    // Skip nav-wait if StepRunner handles it
    if (!skipNavWait) {
      await waitForNavigationDone(beforeUrl, waitMs);
      await ensureReadPageIfWeb();
    }

    return { status: 'success' };
  },
};

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/record-replay/engine/runners/after-script-queue.ts:
--------------------------------------------------------------------------------

```typescript
// after-script-queue.ts — queue + executor for deferred after-scripts
// Notes:
// - Executes user-provided code in the specified world (ISOLATED by default)
// - Clears queue before execution to avoid leaks; re-queues remainder on failure
// - Logs warnings instead of throwing to keep the main engine resilient

import type { StepScript } from '../../types';
import type { ExecCtx } from '../../nodes';
import { RunLogger } from '../logging/run-logger';
import { applyAssign } from '../../rr-utils';

export class AfterScriptQueue {
  private queue: StepScript[] = [];

  constructor(private logger: RunLogger) {}

  enqueue(script: StepScript) {
    this.queue.push(script);
  }

  size() {
    return this.queue.length;
  }

  async flush(ctx: ExecCtx, vars: Record<string, any>) {
    if (this.queue.length === 0) return;
    const scriptsToFlush = this.queue.splice(0, this.queue.length);
    for (let i = 0; i < scriptsToFlush.length; i++) {
      const s = scriptsToFlush[i]!;
      const tScript = Date.now();
      const world = (s as any).world || 'ISOLATED';
      const code = String((s as any).code || '');
      if (!code.trim()) {
        this.logger.push({ stepId: s.id, status: 'success', tookMs: Date.now() - tScript });
        continue;
      }
      try {
        // Warn on obviously dangerous constructs; not a sandbox, just visibility.
        const dangerous =
          /[;{}]|\b(function|=>|while|for|class|globalThis|window|self|this|constructor|__proto__|prototype|eval|Function|import|require|XMLHttpRequest|fetch|chrome)\b/;
        if (dangerous.test(code)) {
          this.logger.push({
            stepId: s.id,
            status: 'warning',
            message: 'Script contains potentially unsafe tokens; executed in isolated world',
          });
        }
        const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
        const tabId = tabs?.[0]?.id;
        if (typeof tabId !== 'number') throw new Error('Active tab not found');
        const [{ result }] = await chrome.scripting.executeScript({
          target: { tabId },
          func: (userCode: string) => {
            try {
              return (0, eval)(userCode);
            } catch (e) {
              return { __error: true, message: String(e) } as any;
            }
          },
          args: [code],
          world: world as any,
        } as any);
        if ((result as any)?.__error) {
          this.logger.push({
            stepId: s.id,
            status: 'warning',
            message: `After-script error: ${(result as any).message || 'unknown'}`,
          });
        }
        const value = (result as any)?.__error ? null : result;
        if ((s as any).saveAs) (vars as any)[(s as any).saveAs] = value;
        if ((s as any).assign && typeof (s as any).assign === 'object')
          applyAssign(vars, value, (s as any).assign);
      } catch (e: any) {
        // Re-queue remaining and stop flush cycle for now
        const remaining = scriptsToFlush.slice(i + 1);
        if (remaining.length) this.queue.unshift(...remaining);
        this.logger.push({
          stepId: s.id,
          status: 'warning',
          message: `After-script execution failed: ${e?.message || String(e)}`,
        });
        break;
      }
      this.logger.push({ stepId: s.id, status: 'success', tookMs: Date.now() - tScript });
    }
  }
}

```

--------------------------------------------------------------------------------
/app/native-server/src/trace-analyzer.ts:
--------------------------------------------------------------------------------

```typescript
import * as fs from 'fs';

// Import DevTools trace engine and formatters from chrome-devtools-frontend
// We intentionally use deep imports to match the package structure.
// These modules are ESM and require NodeNext module resolution.
// Types are loosely typed to minimize coupling with DevTools internals.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as TraceEngine from 'chrome-devtools-frontend/front_end/models/trace/trace.js';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { PerformanceTraceFormatter } from 'chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { PerformanceInsightFormatter } from 'chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { AgentFocus } from 'chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js';

const engine = TraceEngine.TraceModel.Model.createWithAllHandlers();

function readJsonFile(path: string): any {
  const text = fs.readFileSync(path, 'utf-8');
  return JSON.parse(text);
}

export async function parseTrace(json: any): Promise<{
  parsedTrace: any;
  insights: any | null;
}> {
  engine.resetProcessor();
  const events = Array.isArray(json) ? json : json.traceEvents;
  if (!events || !Array.isArray(events)) {
    throw new Error('Invalid trace format: expected array or {traceEvents: []}');
  }
  await engine.parse(events);
  const parsedTrace = engine.parsedTrace();
  const insights = parsedTrace?.insights ?? null;
  if (!parsedTrace) throw new Error('No parsed trace returned by engine');
  return { parsedTrace, insights };
}

export function getTraceSummary(parsedTrace: any): string {
  const focus = AgentFocus.fromParsedTrace(parsedTrace);
  const formatter = new PerformanceTraceFormatter(focus);
  return formatter.formatTraceSummary();
}

export function getInsightText(parsedTrace: any, insights: any, insightName: string): string {
  if (!insights) throw new Error('No insights available for this trace');
  const mainNavId = parsedTrace.data?.Meta?.mainFrameNavigations?.at(0)?.args?.data?.navigationId;
  const NO_NAV = TraceEngine.Types.Events.NO_NAVIGATION;
  const set = insights.get(mainNavId ?? NO_NAV);
  if (!set) throw new Error('No insights for selected navigation');
  const model = set.model || {};
  if (!(insightName in model)) throw new Error(`Insight not found: ${insightName}`);
  const formatter = new PerformanceInsightFormatter(
    AgentFocus.fromParsedTrace(parsedTrace),
    model[insightName],
  );
  return formatter.formatInsight();
}

export async function analyzeTraceFile(
  filePath: string,
  insightName?: string,
): Promise<{
  summary: string;
  insight?: string;
}> {
  const json = readJsonFile(filePath);
  const { parsedTrace, insights } = await parseTrace(json);
  const summary = getTraceSummary(parsedTrace);
  if (insightName) {
    try {
      const insight = getInsightText(parsedTrace, insights, insightName);
      return { summary, insight };
    } catch {
      // If requested insight missing, still return summary
      return { summary };
    }
  }
  return { summary };
}

export default { analyzeTraceFile };

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/quick-panel.content.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Quick Panel Content Script
 *
 * This content script manages the Quick Panel AI Chat feature on web pages.
 * It responds to:
 * - Background messages (toggle_quick_panel from keyboard shortcut)
 * - Direct programmatic calls
 *
 * The Quick Panel provides a floating AI chat interface that:
 * - Uses Shadow DOM for style isolation
 * - Streams AI responses in real-time
 * - Supports keyboard shortcuts (Enter to send, Esc to close)
 * - Collects page context (URL, selection) automatically
 */

import { createQuickPanelController, type QuickPanelController } from '@/shared/quick-panel';

export default defineContentScript({
  matches: ['<all_urls>'],
  runAt: 'document_idle',

  main() {
    console.log('[QuickPanelContentScript] Content script loaded on:', window.location.href);
    let controller: QuickPanelController | null = null;

    /**
     * Ensure controller is initialized (lazy initialization)
     */
    function ensureController(): QuickPanelController {
      if (!controller) {
        controller = createQuickPanelController({
          title: 'Agent',
          subtitle: 'Quick Panel',
          placeholder: 'Ask about this page...',
        });
      }
      return controller;
    }

    /**
     * Handle messages from background script
     */
    function handleMessage(
      message: unknown,
      _sender: chrome.runtime.MessageSender,
      sendResponse: (response?: unknown) => void,
    ): boolean | void {
      const msg = message as { action?: string } | undefined;

      if (msg?.action === 'toggle_quick_panel') {
        console.log('[QuickPanelContentScript] Received toggle_quick_panel message');
        try {
          const ctrl = ensureController();
          ctrl.toggle();
          const visible = ctrl.isVisible();
          console.log('[QuickPanelContentScript] Toggle completed, visible:', visible);
          sendResponse({ success: true, visible });
        } catch (err) {
          console.error('[QuickPanelContentScript] Toggle error:', err);
          sendResponse({ success: false, error: String(err) });
        }
        return true; // Async response
      }

      if (msg?.action === 'show_quick_panel') {
        try {
          const ctrl = ensureController();
          ctrl.show();
          sendResponse({ success: true });
        } catch (err) {
          console.error('[QuickPanelContentScript] Show error:', err);
          sendResponse({ success: false, error: String(err) });
        }
        return true;
      }

      if (msg?.action === 'hide_quick_panel') {
        try {
          if (controller) {
            controller.hide();
          }
          sendResponse({ success: true });
        } catch (err) {
          console.error('[QuickPanelContentScript] Hide error:', err);
          sendResponse({ success: false, error: String(err) });
        }
        return true;
      }

      if (msg?.action === 'get_quick_panel_status') {
        sendResponse({
          success: true,
          visible: controller?.isVisible() ?? false,
          initialized: controller !== null,
        });
        return true;
      }

      // Not handled
      return false;
    }

    // Register message listener
    chrome.runtime.onMessage.addListener(handleMessage);

    // Cleanup on page unload
    window.addEventListener('unload', () => {
      chrome.runtime.onMessage.removeListener(handleMessage);
      if (controller) {
        controller.dispose();
        controller = null;
      }
    });
  },
});

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/builder/components/nodes/node-util.ts:
--------------------------------------------------------------------------------

```typescript
// node-util.ts - shared UI helpers for node components
// Note: comments in English

import type { NodeBase } from '@/entrypoints/background/record-replay/types';
import { summarizeNode as summarize } from '../../model/transforms';
import ILucideMousePointerClick from '~icons/lucide/mouse-pointer-click';
import ILucideEdit3 from '~icons/lucide/edit-3';
import ILucideKeyboard from '~icons/lucide/keyboard';
import ILucideCompass from '~icons/lucide/compass';
import ILucideGlobe from '~icons/lucide/globe';
import ILucideFileCode2 from '~icons/lucide/file-code-2';
import ILucideScan from '~icons/lucide/scan';
import ILucideHourglass from '~icons/lucide/hourglass';
import ILucideCheckCircle2 from '~icons/lucide/check-circle-2';
import ILucideGitBranch from '~icons/lucide/git-branch';
import ILucideRepeat from '~icons/lucide/repeat';
import ILucideRefreshCcw from '~icons/lucide/refresh-ccw';
import ILucideSquare from '~icons/lucide/square';
import ILucideArrowLeftRight from '~icons/lucide/arrow-left-right';
import ILucideX from '~icons/lucide/x';
import ILucideZap from '~icons/lucide/zap';
import ILucideCamera from '~icons/lucide/camera';
import ILucideBell from '~icons/lucide/bell';
import ILucideWrench from '~icons/lucide/wrench';
import ILucideFrame from '~icons/lucide/frame';
import ILucideDownload from '~icons/lucide/download';
import ILucideArrowUpDown from '~icons/lucide/arrow-up-down';
import ILucideMoveVertical from '~icons/lucide/move-vertical';

export function iconComp(t?: string) {
  switch (t) {
    case 'trigger':
      return ILucideZap;
    case 'click':
    case 'dblclick':
      return ILucideMousePointerClick;
    case 'fill':
      return ILucideEdit3;
    case 'drag':
      return ILucideArrowUpDown;
    case 'scroll':
      return ILucideMoveVertical;
    case 'key':
      return ILucideKeyboard;
    case 'navigate':
      return ILucideCompass;
    case 'http':
      return ILucideGlobe;
    case 'script':
      return ILucideFileCode2;
    case 'screenshot':
      return ILucideCamera;
    case 'triggerEvent':
      return ILucideBell;
    case 'setAttribute':
      return ILucideWrench;
    case 'loopElements':
      return ILucideRepeat;
    case 'switchFrame':
      return ILucideFrame;
    case 'handleDownload':
      return ILucideDownload;
    case 'extract':
      return ILucideScan;
    case 'wait':
      return ILucideHourglass;
    case 'assert':
      return ILucideCheckCircle2;
    case 'if':
      return ILucideGitBranch;
    case 'foreach':
      return ILucideRepeat;
    case 'while':
      return ILucideRefreshCcw;
    case 'openTab':
      return ILucideSquare;
    case 'switchTab':
      return ILucideArrowLeftRight;
    case 'closeTab':
      return ILucideX;
    case 'delay':
      return ILucideHourglass;
    default:
      return ILucideSquare;
  }
}

export function getTypeLabel(type?: string) {
  const labels: Record<string, string> = {
    trigger: '触发器',
    click: '点击',
    fill: '填充',
    navigate: '导航',
    wait: '等待',
    extract: '提取',
    http: 'HTTP',
    script: '脚本',
    if: '条件',
    foreach: '循环',
    assert: '断言',
    key: '键盘',
    drag: '拖拽',
    dblclick: '双击',
    openTab: '打开标签',
    switchTab: '切换标签',
    closeTab: '关闭标签',
    delay: '延迟',
    scroll: '滚动',
    while: '循环',
  };
  return labels[String(type || '')] || type || '';
}

export function nodeSubtitle(node?: NodeBase | null): string {
  if (!node) return '';
  const summary = summarize(node);
  if (!summary) return node.type || '';
  return summary.length > 40 ? summary.slice(0, 40) + '...' : summary;
}

```

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

```typescript
/**
 * @fileoverview FlowV3 持久化
 * @description 实现 Flow 的 CRUD 操作
 */

import type { FlowId } from '../domain/ids';
import type { FlowV3 } from '../domain/flow';
import { FLOW_SCHEMA_VERSION } from '../domain/flow';
import { RR_ERROR_CODES, createRRError } from '../domain/errors';
import type { FlowsStore } from '../engine/storage/storage-port';
import { RR_V3_STORES, withTransaction } from './db';

/**
 * 校验 Flow 结构
 */
function validateFlow(flow: FlowV3): void {
  // 校验 schema 版本
  if (flow.schemaVersion !== FLOW_SCHEMA_VERSION) {
    throw createRRError(
      RR_ERROR_CODES.VALIDATION_ERROR,
      `Invalid schema version: expected ${FLOW_SCHEMA_VERSION}, got ${flow.schemaVersion}`,
    );
  }

  // 校验必填字段
  if (!flow.id) {
    throw createRRError(RR_ERROR_CODES.VALIDATION_ERROR, 'Flow id is required');
  }
  if (!flow.name) {
    throw createRRError(RR_ERROR_CODES.VALIDATION_ERROR, 'Flow name is required');
  }
  if (!flow.entryNodeId) {
    throw createRRError(RR_ERROR_CODES.VALIDATION_ERROR, 'Flow entryNodeId is required');
  }

  // 校验 entryNodeId 存在
  const nodeIds = new Set(flow.nodes.map((n) => n.id));
  if (!nodeIds.has(flow.entryNodeId)) {
    throw createRRError(
      RR_ERROR_CODES.VALIDATION_ERROR,
      `Entry node "${flow.entryNodeId}" does not exist in flow`,
    );
  }

  // 校验边引用
  for (const edge of flow.edges) {
    if (!nodeIds.has(edge.from)) {
      throw createRRError(
        RR_ERROR_CODES.VALIDATION_ERROR,
        `Edge "${edge.id}" references non-existent source node "${edge.from}"`,
      );
    }
    if (!nodeIds.has(edge.to)) {
      throw createRRError(
        RR_ERROR_CODES.VALIDATION_ERROR,
        `Edge "${edge.id}" references non-existent target node "${edge.to}"`,
      );
    }
  }
}

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

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

    async save(flow: FlowV3): Promise<void> {
      // 校验
      validateFlow(flow);

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

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

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/quick-panel/commands.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Quick Panel Commands Handler
 *
 * Handles keyboard shortcuts for Quick Panel functionality.
 * Listens for the 'toggle_quick_panel' command and sends toggle message
 * to the content script in the active tab.
 */

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

const COMMAND_KEY = 'toggle_quick_panel';
const LOG_PREFIX = '[QuickPanelCommands]';

// ============================================================
// Helpers
// ============================================================

/**
 * Get the ID of the currently active tab
 */
async function getActiveTabId(): Promise<number | null> {
  try {
    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
    return tab?.id ?? null;
  } catch (err) {
    console.warn(`${LOG_PREFIX} Failed to get active tab:`, err);
    return null;
  }
}

/**
 * Check if a tab can receive content scripts
 */
function isValidTabUrl(url?: string): boolean {
  if (!url) return false;

  // Cannot inject into browser internal pages
  const invalidPrefixes = [
    'chrome://',
    'chrome-extension://',
    'edge://',
    'about:',
    'moz-extension://',
    'devtools://',
    'view-source:',
    'data:',
    // 'file://',
  ];

  return !invalidPrefixes.some((prefix) => url.startsWith(prefix));
}

// ============================================================
// Main Handler
// ============================================================

/**
 * Toggle Quick Panel in the active tab
 */
async function toggleQuickPanelInActiveTab(): Promise<void> {
  const tabId = await getActiveTabId();
  if (tabId === null) {
    console.warn(`${LOG_PREFIX} No active tab found`);
    return;
  }

  // Get tab info to check URL validity
  try {
    const tab = await chrome.tabs.get(tabId);
    if (!isValidTabUrl(tab.url)) {
      console.warn(`${LOG_PREFIX} Cannot inject into tab URL: ${tab.url}`);
      return;
    }
  } catch (err) {
    console.warn(`${LOG_PREFIX} Failed to get tab info:`, err);
    return;
  }

  // Send toggle message to content script
  try {
    const response = await chrome.tabs.sendMessage(tabId, { action: 'toggle_quick_panel' });
    if (response?.success) {
      console.log(`${LOG_PREFIX} Quick Panel toggled, visible: ${response.visible}`);
    } else {
      console.warn(`${LOG_PREFIX} Toggle failed:`, response?.error);
    }
  } catch (err) {
    // Content script may not be loaded yet; this is expected on some pages
    console.warn(
      `${LOG_PREFIX} Failed to send toggle message (content script may not be loaded):`,
      err,
    );
  }
}

// ============================================================
// Initialization
// ============================================================

/**
 * Initialize Quick Panel keyboard command listener
 */
export function initQuickPanelCommands(): void {
  console.log(`${LOG_PREFIX} initQuickPanelCommands called`);
  chrome.commands.onCommand.addListener(async (command) => {
    console.log(`${LOG_PREFIX} onCommand received:`, command);
    if (command !== COMMAND_KEY) {
      console.log(`${LOG_PREFIX} Command not matched, expected:`, COMMAND_KEY);
      return;
    }
    console.log(`${LOG_PREFIX} Command matched, calling toggleQuickPanelInActiveTab...`);

    try {
      await toggleQuickPanelInActiveTab();
      console.log(`${LOG_PREFIX} toggleQuickPanelInActiveTab completed`);
    } catch (err) {
      console.error(`${LOG_PREFIX} Command handler error:`, err);
    }
  });

  console.log(`${LOG_PREFIX} Command listener registered for: ${COMMAND_KEY}`);
}

```

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

```typescript
/**
 * @fileoverview RunRecordV3 持久化
 * @description 实现 Run 记录的 CRUD 操作
 */

import type { RunId } from '../domain/ids';
import type { RunRecordV3 } from '../domain/events';
import { RUN_SCHEMA_VERSION } from '../domain/events';
import { RR_ERROR_CODES, createRRError } from '../domain/errors';
import type { RunsStore } from '../engine/storage/storage-port';
import { RR_V3_STORES, withTransaction } from './db';

/**
 * 校验 Run 记录结构
 */
function validateRunRecord(record: RunRecordV3): void {
  // 校验 schema 版本
  if (record.schemaVersion !== RUN_SCHEMA_VERSION) {
    throw createRRError(
      RR_ERROR_CODES.VALIDATION_ERROR,
      `Invalid schema version: expected ${RUN_SCHEMA_VERSION}, got ${record.schemaVersion}`,
    );
  }

  // 校验必填字段
  if (!record.id) {
    throw createRRError(RR_ERROR_CODES.VALIDATION_ERROR, 'Run id is required');
  }
  if (!record.flowId) {
    throw createRRError(RR_ERROR_CODES.VALIDATION_ERROR, 'Run flowId is required');
  }
  if (!record.status) {
    throw createRRError(RR_ERROR_CODES.VALIDATION_ERROR, 'Run status is required');
  }
}

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

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

    async save(record: RunRecordV3): Promise<void> {
      // 校验
      validateRunRecord(record);

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

    async patch(id: RunId, patch: Partial<RunRecordV3>): Promise<void> {
      return withTransaction(RR_V3_STORES.RUNS, 'readwrite', async (stores) => {
        const store = stores[RR_V3_STORES.RUNS];

        // 先读取现有记录
        const existing = await new Promise<RunRecordV3 | null>((resolve, reject) => {
          const request = store.get(id);
          request.onsuccess = () => resolve((request.result as RunRecordV3) ?? null);
          request.onerror = () => reject(request.error);
        });

        if (!existing) {
          throw createRRError(RR_ERROR_CODES.INTERNAL, `Run "${id}" not found`);
        }

        // 合并并更新
        const updated: RunRecordV3 = {
          ...existing,
          ...patch,
          id: existing.id, // 确保 id 不变
          schemaVersion: existing.schemaVersion, // 确保版本不变
          updatedAt: Date.now(),
        };

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

```

--------------------------------------------------------------------------------
/app/native-server/src/mcp/mcp-server-stdio.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import {
  CallToolRequestSchema,
  CallToolResult,
  ListToolsRequestSchema,
  ListResourcesRequestSchema,
  ListPromptsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { TOOL_SCHEMAS } from 'chrome-mcp-shared';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import * as fs from 'fs';
import * as path from 'path';

let stdioMcpServer: Server | null = null;
let mcpClient: Client | null = null;

// Read configuration from stdio-config.json
const loadConfig = () => {
  try {
    const configPath = path.join(__dirname, 'stdio-config.json');
    const configData = fs.readFileSync(configPath, 'utf8');
    return JSON.parse(configData);
  } catch (error) {
    console.error('Failed to load stdio-config.json:', error);
    throw new Error('Configuration file stdio-config.json not found or invalid');
  }
};

export const getStdioMcpServer = () => {
  if (stdioMcpServer) {
    return stdioMcpServer;
  }
  stdioMcpServer = new Server(
    {
      name: 'StdioChromeMcpServer',
      version: '1.0.0',
    },
    {
      capabilities: {
        tools: {},
        resources: {},
        prompts: {},
      },
    },
  );

  setupTools(stdioMcpServer);
  return stdioMcpServer;
};

export const ensureMcpClient = async () => {
  try {
    if (mcpClient) {
      const pingResult = await mcpClient.ping();
      if (pingResult) {
        return mcpClient;
      }
    }

    const config = loadConfig();
    mcpClient = new Client({ name: 'Mcp Chrome Proxy', version: '1.0.0' }, { capabilities: {} });
    const transport = new StreamableHTTPClientTransport(new URL(config.url), {});
    await mcpClient.connect(transport);
    return mcpClient;
  } catch (error) {
    mcpClient?.close();
    mcpClient = null;
    console.error('Failed to connect to MCP server:', error);
  }
};

export const setupTools = (server: Server) => {
  // List tools handler
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));

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

  // List resources handler - REQUIRED BY MCP PROTOCOL
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));

  // List prompts handler - REQUIRED BY MCP PROTOCOL
  server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
};

const handleToolCall = async (name: string, args: any): Promise<CallToolResult> => {
  try {
    const client = await ensureMcpClient();
    if (!client) {
      throw new Error('Failed to connect to MCP server');
    }
    // Use a sane default of 2 minutes; the previous value mistakenly used 2*6*1000 (12s)
    const DEFAULT_CALL_TIMEOUT_MS = 2 * 60 * 1000;
    const result = await client.callTool({ name, arguments: args }, undefined, {
      timeout: DEFAULT_CALL_TIMEOUT_MS,
    });
    return result as CallToolResult;
  } catch (error: any) {
    return {
      content: [
        {
          type: 'text',
          text: `Error calling tool: ${error.message}`,
        },
      ],
      isError: true,
    };
  }
};

async function main() {
  const transport = new StdioServerTransport();
  await getStdioMcpServer().connect(transport);
}

main().catch((error) => {
  console.error('Fatal error Chrome MCP Server main():', error);
  process.exit(1);
});

```

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

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

export const executeFlowNode: NodeRuntime<any> = {
  validate: (step) => {
    const s: any = step;
    const ok = typeof s.flowId === 'string' && !!s.flowId;
    return ok ? { ok } : { ok, errors: ['需提供 flowId'] };
  },
  run: async (ctx: ExecCtx, step) => {
    const s: any = step;
    const { getFlow } = await import('../flow-store');
    const flow = await getFlow(String(s.flowId));
    if (!flow) throw new Error('referenced flow not found');
    const inline = s.inline !== false; // default inline
    if (!inline) {
      const { runFlow } = await import('../flow-runner');
      await runFlow(flow, { args: s.args || {}, returnLogs: false });
      return {} as ExecResult;
    }
    const { defaultEdgesOnly, topoOrder, mapDagNodeToStep, waitForNetworkIdle, waitForNavigation } =
      await import('../rr-utils');
    const vars = ctx.vars;
    if (s.args && typeof s.args === 'object') Object.assign(vars, s.args);

    // DAG is required - flow-store guarantees nodes/edges via normalization
    const nodes = ((flow as any).nodes || []) as any[];
    const edges = ((flow as any).edges || []) as any[];
    if (nodes.length === 0) {
      throw new Error(
        'Flow has no DAG nodes. Linear steps are no longer supported. Please migrate this flow to nodes/edges.',
      );
    }
    const defaultEdges = defaultEdgesOnly(edges as any);
    const order = topoOrder(nodes as any, defaultEdges as any);
    const stepsToRun: any[] = order.map((n) => mapDagNodeToStep(n as any));
    for (const st of stepsToRun) {
      const t0 = Date.now();
      const maxRetries = Math.max(0, (st as any).retry?.count ?? 0);
      const baseInterval = Math.max(0, (st as any).retry?.intervalMs ?? 0);
      let attempt = 0;
      const doDelay = async (i: number) => {
        const delay =
          baseInterval > 0
            ? (st as any).retry?.backoff === 'exp'
              ? baseInterval * Math.pow(2, i)
              : baseInterval
            : 0;
        if (delay > 0) await new Promise((r) => setTimeout(r, delay));
      };
      while (true) {
        try {
          const beforeInfo = await (async () => {
            const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
            const tab = tabs[0];
            return { url: tab?.url || '', status: (tab as any)?.status || '' };
          })();
          const { executeStep } = await import('../nodes');
          const result = await executeStep(ctx as any, st as any);
          if ((st.type === 'click' || st.type === 'dblclick') && (st as any).after) {
            const after = (st as any).after as any;
            if (after.waitForNavigation)
              await waitForNavigation((st as any).timeoutMs, beforeInfo.url);
            else if (after.waitForNetworkIdle)
              await waitForNetworkIdle(Math.min((st as any).timeoutMs || 5000, 120000), 1200);
          }
          if (!result?.alreadyLogged)
            ctx.logger({ stepId: st.id, status: 'success', tookMs: Date.now() - t0 } as any);
          break;
        } catch (e: any) {
          if (attempt < maxRetries) {
            ctx.logger({
              stepId: st.id,
              status: 'retrying',
              message: e?.message || String(e),
            } as any);
            await doDelay(attempt);
            attempt += 1;
            continue;
          }
          ctx.logger({
            stepId: st.id,
            status: 'failed',
            message: e?.message || String(e),
            tookMs: Date.now() - t0,
          } as any);
          throw e;
        }
      }
    }
    return {} as ExecResult;
  },
};

```

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

```typescript
/**
 * Disposables Utility
 *
 * Provides deterministic cleanup for event listeners, observers, and other resources.
 * Ensures proper cleanup order (LIFO) and prevents memory leaks.
 */

/** Function that performs cleanup */
export type DisposeFn = () => void;

/**
 * Manages a collection of disposable resources.
 * Resources are disposed in reverse order (LIFO).
 */
export class Disposer {
  private disposed = false;
  private readonly disposers: DisposeFn[] = [];

  /** Whether this disposer has already been disposed */
  get isDisposed(): boolean {
    return this.disposed;
  }

  /**
   * Add a dispose function to be called during cleanup.
   * If already disposed, the function is called immediately.
   */
  add(dispose: DisposeFn): void {
    if (this.disposed) {
      try {
        dispose();
      } catch {
        // Best-effort cleanup for late additions
      }
      return;
    }
    this.disposers.push(dispose);
  }

  /**
   * Add an event listener and automatically remove it on dispose.
   */
  listen<K extends keyof WindowEventMap>(
    target: Window,
    type: K,
    listener: (ev: WindowEventMap[K]) => void,
    options?: boolean | AddEventListenerOptions,
  ): void;
  listen<K extends keyof DocumentEventMap>(
    target: Document,
    type: K,
    listener: (ev: DocumentEventMap[K]) => void,
    options?: boolean | AddEventListenerOptions,
  ): void;
  listen<K extends keyof HTMLElementEventMap>(
    target: HTMLElement,
    type: K,
    listener: (ev: HTMLElementEventMap[K]) => void,
    options?: boolean | AddEventListenerOptions,
  ): void;
  listen(
    target: EventTarget,
    type: string,
    listener: EventListenerOrEventListenerObject,
    options?: boolean | AddEventListenerOptions,
  ): void;
  listen(
    target: EventTarget,
    type: string,
    listener: EventListenerOrEventListenerObject,
    options?: boolean | AddEventListenerOptions,
  ): void {
    target.addEventListener(type, listener, options);
    this.add(() => target.removeEventListener(type, listener, options));
  }

  /**
   * Add a ResizeObserver and automatically disconnect it on dispose.
   */
  observeResize(
    target: Element,
    callback: ResizeObserverCallback,
    options?: ResizeObserverOptions,
  ): ResizeObserver {
    const observer = new ResizeObserver(callback);
    observer.observe(target, options);
    this.add(() => observer.disconnect());
    return observer;
  }

  /**
   * Add a MutationObserver and automatically disconnect it on dispose.
   */
  observeMutation(
    target: Node,
    callback: MutationCallback,
    options?: MutationObserverInit,
  ): MutationObserver {
    const observer = new MutationObserver(callback);
    observer.observe(target, options);
    this.add(() => observer.disconnect());
    return observer;
  }

  /**
   * Add a requestAnimationFrame and automatically cancel it on dispose.
   * Returns a function to manually cancel the frame.
   */
  requestAnimationFrame(callback: FrameRequestCallback): () => void {
    const id = requestAnimationFrame(callback);
    let cancelled = false;

    const cancel = () => {
      if (cancelled) return;
      cancelled = true;
      cancelAnimationFrame(id);
    };

    this.add(cancel);
    return cancel;
  }

  /**
   * Dispose all registered resources in reverse order.
   * Safe to call multiple times.
   */
  dispose(): void {
    if (this.disposed) return;
    this.disposed = true;

    // Dispose in reverse order (LIFO)
    for (let i = this.disposers.length - 1; i >= 0; i--) {
      try {
        this.disposers[i]();
      } catch {
        // Best-effort cleanup, continue with remaining disposers
      }
    }

    this.disposers.length = 0;
  }
}

```

--------------------------------------------------------------------------------
/packages/shared/src/types.ts:
--------------------------------------------------------------------------------

```typescript
export enum NativeMessageType {
  START = 'start',
  STARTED = 'started',
  STOP = 'stop',
  STOPPED = 'stopped',
  PING = 'ping',
  PONG = 'pong',
  ERROR = 'error',
  PROCESS_DATA = 'process_data',
  PROCESS_DATA_RESPONSE = 'process_data_response',
  CALL_TOOL = 'call_tool',
  CALL_TOOL_RESPONSE = 'call_tool_response',
  // Additional message types used in Chrome extension
  SERVER_STARTED = 'server_started',
  SERVER_STOPPED = 'server_stopped',
  ERROR_FROM_NATIVE_HOST = 'error_from_native_host',
  CONNECT_NATIVE = 'connectNative',
  ENSURE_NATIVE = 'ensure_native',
  PING_NATIVE = 'ping_native',
  DISCONNECT_NATIVE = 'disconnect_native',
}

export interface NativeMessage<P = any, E = any> {
  type?: NativeMessageType;
  responseToRequestId?: string;
  payload?: P;
  error?: E;
}

// ============================================================
// Element Picker Types (chrome_request_element_selection)
// ============================================================

/**
 * A single element selection request from the AI.
 */
export interface ElementPickerRequest {
  /**
   * Optional stable request id. If omitted, the extension will generate one.
   */
  id?: string;
  /**
   * Short label shown to the user (e.g., "Login button").
   */
  name: string;
  /**
   * Optional longer instruction shown to the user.
   */
  description?: string;
}

/**
 * Bounding rectangle of a picked element.
 */
export interface PickedElementRect {
  x: number;
  y: number;
  width: number;
  height: number;
}

/**
 * Center point of a picked element.
 */
export interface PickedElementPoint {
  x: number;
  y: number;
}

/**
 * A picked element that can be used with other tools (click, fill, etc.).
 */
export interface PickedElement {
  /**
   * Element ref written into window.__claudeElementMap (frame-local).
   * Can be used directly with chrome_click_element, chrome_fill_or_select, etc.
   */
  ref: string;
  /**
   * Best-effort stable CSS selector.
   */
  selector: string;
  /**
   * Selector type (currently CSS only).
   */
  selectorType: 'css';
  /**
   * Bounding rect in the element's frame viewport coordinates.
   */
  rect: PickedElementRect;
  /**
   * Center point in the element's frame viewport coordinates.
   * Can be used as coordinates for chrome_computer.
   */
  center: PickedElementPoint;
  /**
   * Optional text snippet to help verify the selection.
   */
  text?: string;
  /**
   * Lowercased tag name.
   */
  tagName?: string;
  /**
   * Chrome frameId for iframe targeting.
   * Pass this to chrome_click_element/chrome_fill_or_select for cross-frame support.
   */
  frameId: number;
}

/**
 * Result for a single element selection request.
 */
export interface ElementPickerResultItem {
  /**
   * The request id (matches the input request).
   */
  id: string;
  /**
   * The request name (for reference).
   */
  name: string;
  /**
   * The picked element, or null if not selected.
   */
  element: PickedElement | null;
  /**
   * Error message if selection failed for this request.
   */
  error?: string;
}

/**
 * Result of the chrome_request_element_selection tool.
 */
export interface ElementPickerResult {
  /**
   * True if the user confirmed all selections.
   */
  success: boolean;
  /**
   * Session identifier for this picker session.
   */
  sessionId: string;
  /**
   * Timeout value used for this session.
   */
  timeoutMs: number;
  /**
   * True if the user cancelled the selection.
   */
  cancelled?: boolean;
  /**
   * True if the selection timed out.
   */
  timedOut?: boolean;
  /**
   * List of request IDs that were not selected (for debugging).
   */
  missingRequestIds?: string[];
  /**
   * Results for each requested element.
   */
  results: ElementPickerResultItem[];
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/styles/tailwind.css:
--------------------------------------------------------------------------------

```css
@import 'tailwindcss';

/* App background and card helpers */
@layer base {
  html,
  body,
  #app {
    height: 100%;
  }
  body {
    @apply bg-slate-50 text-slate-800;
  }

  /* Record&Replay builder design tokens */
  .rr-theme {
    --rr-bg: #f8fafc;
    --rr-topbar: rgba(255, 255, 255, 0.9);
    --rr-card: #ffffff;
    --rr-elevated: #ffffff;
    --rr-border: #e5e7eb;
    --rr-subtle: #f3f4f6;
    --rr-text: #0f172a;
    --rr-text-weak: #475569;
    --rr-muted: #64748b;
    --rr-brand: #7c3aed;
    --rr-brand-strong: #5b21b6;
    --rr-accent: #0ea5e9;
    --rr-success: #10b981;
    --rr-warn: #f59e0b;
    --rr-danger: #ef4444;
    --rr-dot: rgba(2, 6, 23, 0.08);
  }
  .rr-theme[data-theme='dark'] {
    --rr-bg: #0b1020;
    --rr-topbar: rgba(12, 15, 24, 0.8);
    --rr-card: #0f1528;
    --rr-elevated: #121a33;
    --rr-border: rgba(255, 255, 255, 0.08);
    --rr-subtle: rgba(255, 255, 255, 0.04);
    --rr-text: #e5e7eb;
    --rr-text-weak: #cbd5e1;
    --rr-muted: #94a3b8;
    --rr-brand: #a78bfa;
    --rr-brand-strong: #7c3aed;
    --rr-accent: #38bdf8;
    --rr-success: #34d399;
    --rr-warn: #fbbf24;
    --rr-danger: #f87171;
    --rr-dot: rgba(226, 232, 240, 0.08);
  }
}

@layer components {
  .card {
    @apply rounded-xl shadow-md border;
    background: var(--rr-card);
    border-color: var(--rr-border);
  }
  /* Generic buttons used across builder */
  .btn {
    @apply inline-flex items-center justify-center rounded-lg px-3 py-2 text-sm font-medium transition;
    background: var(--rr-card);
    color: var(--rr-text);
    border: 1px solid var(--rr-border);
  }
  .btn:hover {
    @apply shadow-sm;
    background: var(--rr-subtle);
  }
  .btn[disabled] {
    @apply opacity-60 cursor-not-allowed;
  }
  .btn.primary {
    color: #fff;
    background: var(--rr-brand-strong);
    border-color: var(--rr-brand-strong);
  }
  .btn.primary:hover {
    filter: brightness(1.05);
  }
  .btn.ghost {
    background: transparent;
    border-color: transparent;
  }

  .mini {
    @apply inline-flex items-center justify-center rounded-md px-2 py-1 text-xs font-medium;
    background: var(--rr-card);
    color: var(--rr-text);
    border: 1px solid var(--rr-border);
  }
  .mini:hover {
    background: var(--rr-subtle);
  }
  .mini.danger {
    background: color-mix(in oklab, var(--rr-danger) 8%, transparent);
    border-color: color-mix(in oklab, var(--rr-danger) 24%, var(--rr-border));
    color: var(--rr-text);
  }

  .input {
    @apply w-full px-3 py-2 rounded-lg text-sm;
    background: var(--rr-card);
    color: var(--rr-text);
    border: 1px solid var(--rr-border);
    outline: none;
  }
  .input:focus {
    box-shadow: 0 0 0 3px color-mix(in oklab, var(--rr-brand) 26%, transparent);
    border-color: var(--rr-brand);
  }
  .select {
    @apply w-full px-3 py-2 rounded-lg text-sm;
    background: var(--rr-card);
    color: var(--rr-text);
    border: 1px solid var(--rr-border);
    outline: none;
  }
  .textarea {
    @apply w-full rounded-lg text-sm;
    padding: 10px 12px;
    background: var(--rr-card);
    color: var(--rr-text);
    border: 1px solid var(--rr-border);
    outline: none;
  }
  .label {
    @apply text-sm;
    color: var(--rr-muted);
  }
  .badge {
    @apply inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium;
  }
  .badge-purple {
    background: color-mix(in oklab, var(--rr-brand) 14%, transparent);
    color: var(--rr-brand);
  }

  /* Builder topbar */
  .rr-topbar {
    height: 56px;
    border-bottom: 1px solid var(--rr-border);
    background: var(--rr-topbar);
  }

  /* Dot grid background utility for canvas container */
  .rr-dot-grid {
    background-image: radial-gradient(var(--rr-dot) 1px, transparent 1px);
    background-size: 20px 20px;
  }
}

```

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

```vue
<template>
  <div class="form-section">
    <div class="section-header">
      <span class="section-title">{{ title || '选择器' }}</span>
      <button v-if="allowPick" class="btn-sm btn-primary" @click="pickFromPage">从页面选择</button>
    </div>
    <div class="selector-list" data-field="target.candidates">
      <div class="selector-item" v-for="(c, i) in list" :key="i">
        <select class="form-select-sm" v-model="c.type">
          <option value="css">CSS</option>
          <option value="attr">Attr</option>
          <option value="aria">ARIA</option>
          <option value="text">Text</option>
          <option value="xpath">XPath</option>
        </select>
        <input class="form-input-sm flex-1" v-model="c.value" placeholder="选择器值" />
        <button class="btn-icon-sm" @click="move(i, -1)" :disabled="i === 0">↑</button>
        <button class="btn-icon-sm" @click="move(i, 1)" :disabled="i === list.length - 1">↓</button>
        <button class="btn-icon-sm danger" @click="remove(i)">×</button>
      </div>
      <button class="btn-sm" @click="add">+ 添加选择器</button>
    </div>
  </div>
</template>

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

const props = defineProps<{
  node: NodeBase;
  allowPick?: boolean;
  targetKey?: string;
  title?: string;
}>();
const key = (props.targetKey || 'target') as string;

function ensureTarget() {
  const n: any = props.node;
  if (!n.config) n.config = {};
  if (!n.config[key]) n.config[key] = { candidates: [] };
  if (!Array.isArray(n.config[key].candidates)) n.config[key].candidates = [];
}

const list = {
  get value() {
    ensureTarget();
    return ((props.node as any).config[key].candidates || []) as Array<{
      type: string;
      value: string;
    }>;
  },
} as any as Array<{ type: string; value: string }>;

function add() {
  ensureTarget();
  (props.node as any).config[key].candidates.push({ type: 'css', value: '' });
}
function remove(i: number) {
  ensureTarget();
  (props.node as any).config[key].candidates.splice(i, 1);
}
function move(i: number, d: number) {
  ensureTarget();
  const arr = (props.node as any).config[key].candidates as any[];
  const j = i + d;
  if (j < 0 || j >= arr.length) return;
  const t = arr[i];
  arr[i] = arr[j];
  arr[j] = t;
}

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

async function pickFromPage() {
  try {
    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
    const tabId = tabs?.[0]?.id;
    if (typeof tabId !== 'number') return;
    await ensurePickerInjected(tabId);
    const resp: any = await chrome.tabs.sendMessage(tabId, { action: 'rr_picker_start' } as any);
    if (!resp || !resp.success) return;
    ensureTarget();
    const n: any = props.node;
    const arr = Array.isArray(resp.candidates) ? resp.candidates : [];
    const seen = new Set<string>();
    const merged: any[] = [];
    for (const c of arr) {
      if (!c || !c.type || !c.value) continue;
      const key = `${c.type}|${c.value}`;
      if (!seen.has(key)) {
        seen.add(key);
        merged.push({ type: String(c.type), value: String(c.value) });
      }
    }
    n.config[key].candidates = merged;
  } catch (e) {
    console.warn('pickFromPage failed:', e);
  }
}
</script>

<style scoped>
/* No local styles; inherit from parent panel via :deep selectors */
</style>

```

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

```typescript
/**
 * TestID Strategy - Attribute-based selector strategy
 *
 * Generates selectors based on stable attributes like data-testid, data-cy,
 * as well as semantic attributes like name, title, and alt.
 */

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

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

/** Tags that commonly use form-related attributes */
const FORM_ELEMENT_TAGS = new Set(['input', 'textarea', 'select', 'button']);

/** Tags that commonly use the 'alt' attribute */
const ALT_ATTRIBUTE_TAGS = new Set(['img', 'area']);

/** Tags that commonly use the 'title' attribute (most elements can have it) */
const TITLE_ATTRIBUTE_TAGS = new Set(['img', 'a', 'abbr', 'iframe', 'link']);

/**
 * Mapping of attributes to their preferred tag prefixes.
 * When an attribute-only selector is not unique, we try tag-prefixed form
 * only for elements where that attribute is semantically meaningful.
 */
const ATTR_TAG_PREFERENCES: Record<string, Set<string>> = {
  name: FORM_ELEMENT_TAGS,
  alt: ALT_ATTRIBUTE_TAGS,
  title: TITLE_ATTRIBUTE_TAGS,
};

// =============================================================================
// Helpers
// =============================================================================

function makeAttrSelector(attr: string, value: string, cssEscape: (v: string) => string): string {
  return `[${attr}="${cssEscape(value)}"]`;
}

/**
 * Determine if tag prefix should be tried for disambiguation.
 *
 * Rules:
 * - data-* attributes: try for form elements only
 * - name: try for form elements (input, textarea, select, button)
 * - alt: try for img, area, input[type=image]
 * - title: try for common elements that use title semantically
 * - Default: try for any tag
 */
function shouldTryTagPrefix(attr: string, tag: string, element: Element): boolean {
  if (!tag) return false;

  // For data-* test attributes, use form element heuristic
  if (attr.startsWith('data-')) {
    return FORM_ELEMENT_TAGS.has(tag);
  }

  // For semantic attributes, check the preference mapping
  const preferredTags = ATTR_TAG_PREFERENCES[attr];
  if (preferredTags) {
    if (preferredTags.has(tag)) return true;

    // Special case: input[type=image] also uses alt
    if (attr === 'alt' && tag === 'input') {
      const type = element.getAttribute('type');
      return type === 'image';
    }

    return false;
  }

  // Default: try tag prefix for any element
  return true;
}

// =============================================================================
// Strategy Export
// =============================================================================

export const testIdStrategy: SelectorStrategy = {
  id: 'testid',

  generate(ctx) {
    const { element, options, helpers } = ctx;
    const out: SelectorCandidate[] = [];
    const tag = element.tagName?.toLowerCase?.() ?? '';

    for (const attr of options.testIdAttributes) {
      const raw = element.getAttribute(attr);
      const value = raw?.trim();
      if (!value) continue;

      const attrOnly = makeAttrSelector(attr, value, helpers.cssEscape);

      // Try attribute-only selector first
      if (helpers.isUnique(attrOnly)) {
        out.push({
          type: 'attr',
          value: attrOnly,
          source: 'generated',
          strategy: 'testid',
        });
        continue;
      }

      // Try tag-prefixed form if appropriate for this attribute/element combo
      if (shouldTryTagPrefix(attr, tag, element)) {
        const withTag = `${tag}${attrOnly}`;
        if (helpers.isUnique(withTag)) {
          out.push({
            type: 'attr',
            value: withTag,
            source: 'generated',
            strategy: 'testid',
          });
        }
      }
    }

    return out;
  },
};

```

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

```vue
<template>
  <div class="space-y-1">
    <div class="flex items-baseline gap-2 flex-wrap">
      <!-- Label -->
      <span
        class="text-[11px] font-bold uppercase tracking-wider flex-shrink-0"
        :style="{
          color: labelColor,
        }"
      >
        {{ item.tool.label }}
      </span>

      <!-- Content based on tool kind -->
      <code
        v-if="item.tool.kind === 'grep' || item.tool.kind === 'read'"
        class="text-xs px-1.5 py-0.5 cursor-pointer ac-chip-hover"
        :style="{
          fontFamily: 'var(--ac-font-mono)',
          backgroundColor: 'var(--ac-chip-bg)',
          color: 'var(--ac-chip-text)',
          borderRadius: 'var(--ac-radius-button)',
        }"
        :title="item.tool.filePath || item.tool.pattern"
      >
        {{ item.tool.title }}
      </code>

      <span
        v-else
        class="text-xs"
        :style="{
          fontFamily: 'var(--ac-font-mono)',
          color: 'var(--ac-text-muted)',
        }"
        :title="item.tool.filePath || item.tool.command"
      >
        {{ item.tool.title }}
      </span>

      <!-- Diff Stats Preview (for edit) -->
      <span
        v-if="hasDiffStats"
        class="text-[10px] px-1.5 py-0.5"
        :style="{
          backgroundColor: 'var(--ac-chip-bg)',
          color: 'var(--ac-text-muted)',
          fontFamily: 'var(--ac-font-mono)',
          borderRadius: 'var(--ac-radius-button)',
        }"
      >
        <span v-if="item.tool.diffStats?.addedLines" class="text-green-600 dark:text-green-400">
          +{{ item.tool.diffStats.addedLines }}
        </span>
        <span v-if="item.tool.diffStats?.addedLines && item.tool.diffStats?.deletedLines">/</span>
        <span v-if="item.tool.diffStats?.deletedLines" class="text-red-600 dark:text-red-400">
          -{{ item.tool.diffStats.deletedLines }}
        </span>
      </span>

      <!-- Streaming indicator -->
      <span
        v-if="item.isStreaming"
        class="text-xs italic"
        :style="{ color: 'var(--ac-text-subtle)' }"
      >
        ...
      </span>
    </div>

    <!-- Subtitle (command description or search path) -->
    <div
      v-if="subtitle"
      class="text-[10px] pl-10 truncate"
      :style="{ color: 'var(--ac-text-subtle)' }"
      :title="subtitleFull"
    >
      {{ subtitle }}
    </div>
  </div>
</template>

<script lang="ts" setup>
import { computed } from 'vue';
import type { TimelineItem } from '../../../composables/useAgentThreads';

const props = defineProps<{
  item: Extract<TimelineItem, { kind: 'tool_use' }>;
}>();

const labelColor = computed(() => {
  if (props.item.tool.kind === 'edit') {
    return 'var(--ac-accent)';
  }
  return 'var(--ac-text-subtle)';
});

const hasDiffStats = computed(() => {
  const stats = props.item.tool.diffStats;
  if (!stats) return false;
  return stats.addedLines !== undefined || stats.deletedLines !== undefined;
});

const subtitle = computed(() => {
  const tool = props.item.tool;

  // For commands: show the actual command if title is description
  if (tool.kind === 'run' && tool.commandDescription && tool.command) {
    return tool.command.length > 60 ? tool.command.slice(0, 57) + '...' : tool.command;
  }

  // For file operations: show full path if title is just filename
  if ((tool.kind === 'edit' || tool.kind === 'read') && tool.filePath) {
    if (tool.filePath !== tool.title && !tool.title.includes('/')) {
      return tool.filePath;
    }
  }

  // For search: show search path if provided
  if (tool.kind === 'grep' && tool.searchPath) {
    return `in ${tool.searchPath}`;
  }

  return undefined;
});

const subtitleFull = computed(() => {
  const tool = props.item.tool;
  if (tool.kind === 'run' && tool.command) return tool.command;
  if (tool.filePath) return tool.filePath;
  if (tool.searchPath) return tool.searchPath;
  return undefined;
});
</script>

```

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

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

/**
 * Get storage statistics
 */
export async function handleGetStorageStats(): Promise<{
  success: boolean;
  stats?: any;
  error?: string;
}> {
  try {
    // Get ContentIndexer statistics
    const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
    const contentIndexer = getGlobalContentIndexer();

    // Note: Semantic engine initialization is now user-controlled
    // ContentIndexer will be initialized when user manually triggers semantic engine initialization

    // Get statistics
    const stats = contentIndexer.getStats();

    return {
      success: true,
      stats: {
        indexedPages: stats.indexedPages || 0,
        totalDocuments: stats.totalDocuments || 0,
        totalTabs: stats.totalTabs || 0,
        indexSize: stats.indexSize || 0,
        isInitialized: stats.isInitialized || false,
        semanticEngineReady: stats.semanticEngineReady || false,
        semanticEngineInitializing: stats.semanticEngineInitializing || false,
      },
    };
  } catch (error: any) {
    console.error('Background: Failed to get storage stats:', error);
    return {
      success: false,
      error: error.message,
      stats: {
        indexedPages: 0,
        totalDocuments: 0,
        totalTabs: 0,
        indexSize: 0,
        isInitialized: false,
        semanticEngineReady: false,
        semanticEngineInitializing: false,
      },
    };
  }
}

/**
 * Clear all data
 */
export async function handleClearAllData(): Promise<{ success: boolean; error?: string }> {
  try {
    // 1. Clear all ContentIndexer indexes
    try {
      const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
      const contentIndexer = getGlobalContentIndexer();

      await contentIndexer.clearAllIndexes();
      console.log('Storage: ContentIndexer indexes cleared successfully');
    } catch (indexerError) {
      console.warn('Background: Failed to clear ContentIndexer indexes:', indexerError);
      // Continue with other cleanup operations
    }

    // 2. Clear all VectorDatabase data
    try {
      const { clearAllVectorData } = await import('@/utils/vector-database');
      await clearAllVectorData();
      console.log('Storage: Vector database data cleared successfully');
    } catch (vectorError) {
      console.warn('Background: Failed to clear vector data:', vectorError);
      // Continue with other cleanup operations
    }

    // 3. Clear related data in chrome.storage (preserve model preferences)
    try {
      const keysToRemove = ['vectorDatabaseStats', 'lastCleanupTime', 'contentIndexerStats'];
      await chrome.storage.local.remove(keysToRemove);
      console.log('Storage: Chrome storage data cleared successfully');
    } catch (storageError) {
      console.warn('Background: Failed to clear chrome storage data:', storageError);
    }

    return { success: true };
  } catch (error: any) {
    console.error('Background: Failed to clear all data:', error);
    return { success: false, error: error.message };
  }
}

/**
 * Initialize storage manager module message listeners
 */
export const initStorageManagerListener = () => {
  chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
    if (message.type === BACKGROUND_MESSAGE_TYPES.GET_STORAGE_STATS) {
      handleGetStorageStats()
        .then((result: { success: boolean; stats?: any; error?: string }) => sendResponse(result))
        .catch((error: any) => sendResponse({ success: false, error: error.message }));
      return true;
    } else if (message.type === BACKGROUND_MESSAGE_TYPES.CLEAR_ALL_DATA) {
      handleClearAllData()
        .then((result: { success: boolean; error?: string }) => sendResponse(result))
        .catch((error: any) => sendResponse({ success: false, error: error.message }));
      return true;
    }
  });
};

```

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

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

    <button
      v-for="t in themes"
      :key="t.id"
      class="w-full px-3 py-2 text-left text-sm flex items-center justify-between ac-menu-item"
      :style="{
        color: theme === t.id ? 'var(--ac-accent, #c87941)' : 'var(--ac-text, #1a1a1a)',
      }"
      @click="$emit('theme:set', t.id)"
    >
      <span>{{ t.label }}</span>
      <svg
        v-if="theme === t.id"
        class="w-4 h-4"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
      >
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
      </svg>
    </button>

    <!-- Divider -->
    <div
      class="my-2"
      :style="{
        borderTop: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)',
      }"
    />

    <!-- Input Section -->
    <div
      class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider"
      :style="{ color: 'var(--ac-text-subtle, #a8a29e)' }"
    >
      Input
    </div>

    <button
      class="w-full px-3 py-2 text-left text-sm flex items-center justify-between ac-menu-item"
      :style="{ color: 'var(--ac-text, #1a1a1a)' }"
      @click="$emit('fakeCaret:toggle', !fakeCaretEnabled)"
    >
      <span>Comet caret</span>
      <svg
        v-if="fakeCaretEnabled"
        class="w-4 h-4"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
      >
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
      </svg>
    </button>

    <!-- Divider -->
    <div
      class="my-2"
      :style="{
        borderTop: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)',
      }"
    />

    <!-- Storage Section -->
    <div
      class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider"
      :style="{ color: 'var(--ac-text-subtle, #a8a29e)' }"
    >
      Storage
    </div>

    <button
      class="w-full px-3 py-2 text-left text-sm ac-menu-item"
      :style="{ color: 'var(--ac-text, #1a1a1a)' }"
      @click="$emit('attachments:open')"
    >
      Clear Attachment Cache
    </button>

    <!-- Divider -->
    <div
      class="my-2"
      :style="{
        borderTop: 'var(--ac-border-width, 1px) solid var(--ac-border, #e5e5e5)',
      }"
    />

    <!-- Reconnect -->
    <button
      class="w-full px-3 py-2 text-left text-sm ac-menu-item"
      :style="{ color: 'var(--ac-text, #1a1a1a)' }"
      @click="$emit('reconnect')"
    >
      Reconnect Server
    </button>
  </div>
</template>

<script lang="ts" setup>
import { type AgentThemeId, THEME_LABELS } from '../../composables';

defineProps<{
  open: boolean;
  theme: AgentThemeId;
  /** Fake caret (comet effect) enabled state */
  fakeCaretEnabled?: boolean;
}>();

defineEmits<{
  'theme:set': [theme: AgentThemeId];
  reconnect: [];
  'attachments:open': [];
  'fakeCaret:toggle': [enabled: boolean];
}>();

const themes: { id: AgentThemeId; label: string }[] = [
  { id: 'warm-editorial', label: THEME_LABELS['warm-editorial'] },
  { id: 'blueprint-architect', label: THEME_LABELS['blueprint-architect'] },
  { id: 'zen-journal', label: THEME_LABELS['zen-journal'] },
  { id: 'neo-pop', label: THEME_LABELS['neo-pop'] },
  { id: 'dark-console', label: THEME_LABELS['dark-console'] },
  { id: 'swiss-grid', label: THEME_LABELS['swiss-grid'] },
];
</script>

```

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

```vue
<template>
  <div class="form-section">
    <div class="section-header">
      <span class="section-title">If / else</span>
      <button class="btn-sm" @click="addIfCase">+ Add</button>
    </div>
    <div class="text-xs text-slate-500" style="padding: 0 20px"
      >使用表达式定义分支,支持变量与常见比较运算符。</div
    >
    <div class="if-case-list" data-field="if.branches">
      <div class="if-case-item" v-for="(c, i) in ifBranches" :key="c.id">
        <div class="if-case-header">
          <input class="form-input-sm flex-1" v-model="c.name" placeholder="分支名称(可选)" />
          <button class="btn-icon-sm danger" @click="removeIfCase(i)" title="删除">×</button>
        </div>
        <div class="if-case-expr">
          <VarInput
            v-model="c.expr"
            :variables="variablesNormalized"
            format="workflowDot"
            :placeholder="'workflow.' + (variablesNormalized[0]?.key || 'var') + ' == 5'"
          />
          <div class="if-toolbar">
            <select
              class="form-select-sm"
              @change="(e: any) => insertVar(e.target.value, i)"
              :value="''"
            >
              <option value="" disabled>插入变量</option>
              <option v-for="v in variables" :key="v.key" :value="v.key">{{ v.key }}</option>
            </select>
            <select
              class="form-select-sm"
              @change="(e: any) => insertOp(e.target.value, i)"
              :value="''"
            >
              <option value="" disabled>运算符</option>
              <option v-for="op in ops" :key="op" :value="op">{{ op }}</option>
            </select>
          </div>
        </div>
      </div>
      <div class="if-case-else" v-if="elseEnabled">
        <div class="text-xs text-slate-500">Else 分支(无需表达式,将匹配以上条件都不成立时)</div>
      </div>
    </div>
  </div>
</template>

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

import VarInput from '@/entrypoints/popup/components/builder/widgets/VarInput.vue';
import type { VariableOption } from '@/entrypoints/popup/components/builder/model/variables';
const props = defineProps<{ node: NodeBase; variables?: Array<{ key: string }> }>();
const variablesNormalized = computed<VariableOption[]>(() =>
  (props.variables || []).map((v) => ({ key: v.key, origin: 'global' }) as VariableOption),
);

const ops = ['==', '!=', '>', '>=', '<', '<=', '&&', '||'];
const ifBranches = computed<Array<{ id: string; name?: string; expr: string }>>({
  get() {
    try {
      return Array.isArray((props.node as any)?.config?.branches)
        ? ((props.node as any).config.branches as any[])
        : [];
    } catch {
      return [] as any;
    }
  },
  set(arr) {
    try {
      (props.node as any).config.branches = arr;
    } catch {}
  },
});
const elseEnabled = computed<boolean>({
  get() {
    try {
      return (props.node as any)?.config?.else !== false;
    } catch {
      return true;
    }
  },
  set(v) {
    try {
      (props.node as any).config.else = !!v;
    } catch {}
  },
});

function addIfCase() {
  const arr = ifBranches.value.slice();
  arr.push({ id: newId('case'), name: '', expr: '' });
  ifBranches.value = arr;
}
function removeIfCase(i: number) {
  const arr = ifBranches.value.slice();
  arr.splice(i, 1);
  ifBranches.value = arr;
}
function insertVar(key: string, idx: number) {
  if (!key) return;
  const arr = ifBranches.value.slice();
  const token = `workflow.${key}`;
  arr[idx].expr = String(arr[idx].expr || '') + (arr[idx].expr ? ' ' : '') + token;
  ifBranches.value = arr;
}
function insertOp(op: string, idx: number) {
  if (!op) return;
  const arr = ifBranches.value.slice();
  arr[idx].expr = String(arr[idx].expr || '') + (arr[idx].expr ? ' ' : '') + op;
  ifBranches.value = arr;
}
</script>

<style scoped></style>

```

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

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

export const assertNode: NodeRuntime<StepAssert> = {
  validate: (step) => {
    const s = step as any;
    const ok = !!s.assert;
    if (ok && s.assert && 'attribute' in s.assert) {
      const a = s.assert.attribute || {};
      if (!a.selector || !a.name)
        return { ok: false, errors: ['assert.attribute: 需提供 selector 与 name'] };
    }
    return ok ? { ok } : { ok, errors: ['缺少断言条件'] };
  },
  run: async (ctx: ExecCtx, step: StepAssert) => {
    const s = expandTemplatesDeep(step as StepAssert, ctx.vars) as any;
    const failStrategy = (s as any).failStrategy || 'stop';
    const fail = (msg: string) => {
      if (failStrategy === 'warn') {
        ctx.logger({ stepId: (step as any).id, status: 'warning', message: msg });
        return { alreadyLogged: true } as any;
      }
      throw new Error(msg);
    };
    if ('textPresent' in s.assert) {
      const text = (s.assert as any).textPresent;
      const res = await handleCallTool({
        name: TOOL_NAMES.BROWSER.COMPUTER,
        args: { action: 'wait', text, appear: true, timeout: (step as any).timeoutMs || 5000 },
      });
      if ((res as any).isError) return fail('assert text failed');
    } else if ('exists' in s.assert || 'visible' in s.assert) {
      const selector = (s.assert as any).exists || (s.assert as any).visible;
      const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
      const firstTab = tabs && tabs[0];
      const tabId = firstTab && typeof firstTab.id === 'number' ? firstTab.id : undefined;
      if (!tabId) return fail('Active tab not found');
      await handleCallTool({ name: TOOL_NAMES.BROWSER.READ_PAGE, args: {} });
      const ensured: any = (await chrome.tabs.sendMessage(
        tabId,
        {
          action: 'ensureRefForSelector',
          selector,
        } as any,
        { frameId: ctx.frameId } as any,
      )) as any;
      if (!ensured || !ensured.success) return fail('assert selector not found');
      if ('visible' in s.assert) {
        const rect = ensured && ensured.center ? ensured.center : null;
        if (!rect) return fail('assert visible failed');
      }
    } else if ('attribute' in s.assert) {
      const { selector, name, equals, matches } = (s.assert as any).attribute || {};
      const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
      const firstTab = tabs && tabs[0];
      const tabId = firstTab && typeof firstTab.id === 'number' ? firstTab.id : undefined;
      if (!tabId) return fail('Active tab not found');
      await handleCallTool({ name: TOOL_NAMES.BROWSER.READ_PAGE, args: {} });
      const resp: any = (await chrome.tabs.sendMessage(
        tabId,
        { action: 'getAttributeForSelector', selector, name } as any,
        { frameId: ctx.frameId } as any,
      )) as any;
      if (!resp || !resp.success) return fail('assert attribute: element not found');
      const actual: string | null = resp.value ?? null;
      if (equals !== undefined && equals !== null) {
        const expected = String(equals);
        if (String(actual) !== String(expected))
          return fail(
            `assert attribute equals failed: ${name} actual=${String(actual)} expected=${String(expected)}`,
          );
      } else if (matches !== undefined && matches !== null) {
        try {
          const re = new RegExp(String(matches));
          if (!re.test(String(actual)))
            return fail(
              `assert attribute matches failed: ${name} actual=${String(actual)} regex=${String(matches)}`,
            );
        } catch {
          return fail(`invalid regex for attribute matches: ${String(matches)}`);
        }
      } else {
        if (actual == null) return fail(`assert attribute failed: ${name} missing`);
      }
    }
    return {} as ExecResult;
  },
};

```

--------------------------------------------------------------------------------
/app/native-server/src/agent/engines/types.ts:
--------------------------------------------------------------------------------

```typescript
import type { AgentAttachment, RealtimeEvent } from '../types';
import type { CodexEngineConfig } from 'chrome-mcp-shared';

export interface EngineInitOptions {
  sessionId: string;
  instruction: string;
  model?: string;
  projectRoot?: string;
  requestId: string;
  /**
   * AbortSignal for cancellation support.
   */
  signal?: AbortSignal;
  /**
   * Optional attachments (images/files) to include with the instruction.
   * Note: When using persisted attachments, use resolvedImagePaths instead.
   */
  attachments?: AgentAttachment[];
  /**
   * Resolved absolute paths to persisted image files.
   * These are used by engines instead of writing temp files from base64.
   * Set by chat-service after saving attachments to persistent storage.
   */
  resolvedImagePaths?: string[];
  /**
   * Optional project ID for session persistence.
   * When provided, engines can use this to save/load session state.
   */
  projectId?: string;
  /**
   * Optional database session ID (sessions.id) for session-scoped configuration and persistence.
   */
  dbSessionId?: string;
  /**
   * Optional session-scoped permission mode override (Claude SDK option).
   */
  permissionMode?: string;
  /**
   * Optional session-scoped permission bypass override (Claude SDK option).
   */
  allowDangerouslySkipPermissions?: boolean;
  /**
   * Optional session-scoped system prompt configuration.
   */
  systemPromptConfig?: unknown;
  /**
   * Optional session-scoped engine option overrides.
   */
  optionsConfig?: unknown;
  /**
   * Optional Claude session ID (UUID) for resuming a previous session.
   * Only applicable to ClaudeEngine; retrieved from sessions.engineSessionId (preferred)
   * or project's activeClaudeSessionId (legacy fallback).
   */
  resumeClaudeSessionId?: string;
  /**
   * Whether to use Claude Code Router (CCR) for this request.
   * Only applicable to ClaudeEngine; when true, CCR will be auto-detected.
   */
  useCcr?: boolean;
  /**
   * Optional Codex-specific configuration overrides.
   * Only applicable to CodexEngine; merged with DEFAULT_CODEX_CONFIG.
   */
  codexConfig?: Partial<CodexEngineConfig>;
}

/**
 * Callback to persist Claude session ID after initialization.
 */
export type ClaudeSessionPersistCallback = (sessionId: string) => Promise<void>;

/**
 * Management information extracted from Claude SDK system:init message.
 */
export interface ClaudeManagementInfo {
  tools?: string[];
  agents?: string[];
  /** Plugins with name and path (SDK returns { name, path }[]) */
  plugins?: Array<{ name: string; path?: string }>;
  skills?: string[];
  mcpServers?: Array<{ name: string; status: string }>;
  slashCommands?: string[];
  model?: string;
  permissionMode?: string;
  cwd?: string;
  outputStyle?: string;
  betas?: string[];
  claudeCodeVersion?: string;
  apiKeySource?: string;
}

/**
 * Callback to persist management information after SDK initialization.
 */
export type ManagementInfoPersistCallback = (info: ClaudeManagementInfo) => Promise<void>;

export type EngineName = 'claude' | 'codex' | 'cursor' | 'qwen' | 'glm';

export interface EngineExecutionContext {
  /**
   * Emit a realtime event to all connected clients for the current session.
   */
  emit(event: RealtimeEvent): void;
  /**
   * Optional callback to persist Claude session ID after SDK initialization.
   * Only called by ClaudeEngine when projectId is provided.
   */
  persistClaudeSessionId?: ClaudeSessionPersistCallback;
  /**
   * Optional callback to persist management information after SDK initialization.
   * Only called by ClaudeEngine when dbSessionId is provided.
   */
  persistManagementInfo?: ManagementInfoPersistCallback;
}

export interface AgentEngine {
  name: EngineName;
  /**
   * Whether this engine can act as an MCP client natively.
   */
  supportsMcp?: boolean;
  initializeAndRun(options: EngineInitOptions, ctx: EngineExecutionContext): Promise<void>;
}

/**
 * Represents a running engine execution that can be cancelled.
 */
export interface RunningExecution {
  requestId: string;
  sessionId: string;
  engineName: EngineName;
  abortController: AbortController;
  startedAt: Date;
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/utils/indexeddb-client.ts:
--------------------------------------------------------------------------------

```typescript
// indexeddb-client.ts
// Generic IndexedDB client with robust transaction handling and small helpers.

export type UpgradeHandler = (
  db: IDBDatabase,
  oldVersion: number,
  tx: IDBTransaction | null,
) => void;

export class IndexedDbClient {
  private dbPromise: Promise<IDBDatabase> | null = null;

  constructor(
    private name: string,
    private version: number,
    private onUpgrade: UpgradeHandler,
  ) {}

  async openDb(): Promise<IDBDatabase> {
    if (this.dbPromise) return this.dbPromise;
    this.dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
      const req = indexedDB.open(this.name, this.version);
      req.onupgradeneeded = (event) => {
        const db = req.result;
        const oldVersion = (event as IDBVersionChangeEvent).oldVersion || 0;
        const tx = req.transaction as IDBTransaction | null;
        try {
          this.onUpgrade(db, oldVersion, tx);
        } catch (e) {
          console.error('IndexedDbClient upgrade failed:', e);
        }
      };
      req.onsuccess = () => resolve(req.result);
      req.onerror = () =>
        reject(new Error(`IndexedDB open failed: ${req.error?.message || req.error}`));
    });
    return this.dbPromise;
  }

  async tx<T>(
    storeName: string,
    mode: IDBTransactionMode,
    op: (store: IDBObjectStore, txn: IDBTransaction) => T | Promise<T>,
  ): Promise<T> {
    const db = await this.openDb();
    return new Promise<T>((resolve, reject) => {
      const transaction = db.transaction(storeName, mode);
      const st = transaction.objectStore(storeName);
      let opResult: T | undefined;
      let opError: any;
      transaction.oncomplete = () => resolve(opResult as T);
      transaction.onerror = () =>
        reject(
          new Error(
            `IDB transaction error on ${storeName}: ${transaction.error?.message || transaction.error}`,
          ),
        );
      transaction.onabort = () =>
        reject(
          new Error(
            `IDB transaction aborted on ${storeName}: ${transaction.error?.message || opError || 'unknown'}`,
          ),
        );
      Promise.resolve()
        .then(() => op(st, transaction))
        .then((res) => {
          opResult = res as T;
        })
        .catch((err) => {
          opError = err;
          try {
            transaction.abort();
          } catch {}
        });
    });
  }

  async getAll<T>(store: string): Promise<T[]> {
    return this.tx<T[]>(store, 'readonly', (st) =>
      this.promisifyRequest<any[]>(st.getAll(), store, 'getAll').then((res) => (res as T[]) || []),
    );
  }

  async get<T>(store: string, key: IDBValidKey): Promise<T | undefined> {
    return this.tx<T | undefined>(store, 'readonly', (st) =>
      this.promisifyRequest<T | undefined>(st.get(key), store, `get(${String(key)})`).then(
        (res) => res as any,
      ),
    );
  }

  async put<T>(store: string, value: T): Promise<void> {
    return this.tx<void>(store, 'readwrite', (st) =>
      this.promisifyRequest<any>(st.put(value as any), store, 'put').then(() => undefined),
    );
  }

  async delete(store: string, key: IDBValidKey): Promise<void> {
    return this.tx<void>(store, 'readwrite', (st) =>
      this.promisifyRequest<any>(st.delete(key), store, `delete(${String(key)})`).then(
        () => undefined,
      ),
    );
  }

  async clear(store: string): Promise<void> {
    return this.tx<void>(store, 'readwrite', (st) =>
      this.promisifyRequest<any>(st.clear(), store, 'clear').then(() => undefined),
    );
  }

  async putMany<T>(store: string, values: T[]): Promise<void> {
    return this.tx<void>(store, 'readwrite', async (st) => {
      for (const v of values) st.put(v as any);
      return;
    });
  }

  // Expose helper for advanced callers if needed
  promisifyRequest<R>(req: IDBRequest<R>, store: string, action: string): Promise<R> {
    return new Promise<R>((resolve, reject) => {
      req.onsuccess = () => resolve(req.result as R);
      req.onerror = () =>
        reject(
          new Error(
            `IDB ${action} error on ${store}: ${(req.error as any)?.message || (req.error as any)}`,
          ),
        );
    });
  }
}

```

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

```typescript
/**
 * Composable for managing user preference for opening project directory.
 * Stores the default target (vscode/terminal) in chrome.storage.local.
 */
import { ref, type Ref } from 'vue';
import type { OpenProjectTarget, OpenProjectResponse } from 'chrome-mcp-shared';

// Storage key for default open target
const STORAGE_KEY = 'agent-open-project-default';

export interface UseOpenProjectPreferenceOptions {
  /**
   * Server port for API calls.
   * Should be provided from useAgentServer.
   */
  getServerPort: () => number | null;
}

export interface UseOpenProjectPreference {
  /** Current default target (null if not set) */
  defaultTarget: Ref<OpenProjectTarget | null>;
  /** Loading state */
  loading: Ref<boolean>;
  /** Load default target from storage */
  loadDefaultTarget: () => Promise<void>;
  /** Save default target to storage */
  saveDefaultTarget: (target: OpenProjectTarget) => Promise<void>;
  /** Open project by session ID */
  openBySession: (sessionId: string, target: OpenProjectTarget) => Promise<OpenProjectResponse>;
  /** Open project by project ID */
  openByProject: (projectId: string, target: OpenProjectTarget) => Promise<OpenProjectResponse>;
}

export function useOpenProjectPreference(
  options: UseOpenProjectPreferenceOptions,
): UseOpenProjectPreference {
  const defaultTarget = ref<OpenProjectTarget | null>(null);
  const loading = ref(false);

  /**
   * Load default target from chrome.storage.local.
   */
  async function loadDefaultTarget(): Promise<void> {
    try {
      const result = await chrome.storage.local.get(STORAGE_KEY);
      const stored = result[STORAGE_KEY];
      if (stored === 'vscode' || stored === 'terminal') {
        defaultTarget.value = stored;
      }
    } catch (error) {
      console.error('[OpenProjectPreference] Failed to load default target:', error);
    }
  }

  /**
   * Save default target to chrome.storage.local.
   */
  async function saveDefaultTarget(target: OpenProjectTarget): Promise<void> {
    try {
      await chrome.storage.local.set({ [STORAGE_KEY]: target });
      defaultTarget.value = target;
    } catch (error) {
      console.error('[OpenProjectPreference] Failed to save default target:', error);
    }
  }

  /**
   * Open project directory by session ID.
   */
  async function openBySession(
    sessionId: string,
    target: OpenProjectTarget,
  ): Promise<OpenProjectResponse> {
    const port = options.getServerPort();
    if (!port) {
      return { success: false, error: 'Server not connected' };
    }

    loading.value = true;
    try {
      const url = `http://127.0.0.1:${port}/agent/sessions/${encodeURIComponent(sessionId)}/open`;
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ target }),
      });

      const data = (await response.json()) as OpenProjectResponse;
      return data;
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      return { success: false, error: message };
    } finally {
      loading.value = false;
    }
  }

  /**
   * Open project directory by project ID.
   */
  async function openByProject(
    projectId: string,
    target: OpenProjectTarget,
  ): Promise<OpenProjectResponse> {
    const port = options.getServerPort();
    if (!port) {
      return { success: false, error: 'Server not connected' };
    }

    loading.value = true;
    try {
      const url = `http://127.0.0.1:${port}/agent/projects/${encodeURIComponent(projectId)}/open`;
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ target }),
      });

      const data = (await response.json()) as OpenProjectResponse;
      return data;
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      return { success: false, error: message };
    } finally {
      loading.value = false;
    }
  }

  return {
    defaultTarget,
    loading,
    loadDefaultTarget,
    saveDefaultTarget,
    openBySession,
    openByProject,
  };
}

```

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

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

interface HandleDownloadParams {
  filenameContains?: string;
  timeoutMs?: number; // default 60000
  waitForComplete?: boolean; // default true
}

/**
 * Tool: wait for a download and return info
 */
class HandleDownloadTool extends BaseBrowserToolExecutor {
  name = TOOL_NAMES.BROWSER.HANDLE_DOWNLOAD as any;

  async execute(args: HandleDownloadParams): Promise<ToolResult> {
    const filenameContains = String(args?.filenameContains || '').trim();
    const waitForComplete = args?.waitForComplete !== false;
    const timeoutMs = Math.max(1000, Math.min(Number(args?.timeoutMs ?? 60000), 300000));

    try {
      const result = await waitForDownload({ filenameContains, waitForComplete, timeoutMs });
      return {
        content: [{ type: 'text', text: JSON.stringify({ success: true, download: result }) }],
        isError: false,
      };
    } catch (e: any) {
      return createErrorResponse(`Handle download failed: ${e?.message || String(e)}`);
    }
  }
}

async function waitForDownload(opts: {
  filenameContains?: string;
  waitForComplete: boolean;
  timeoutMs: number;
}) {
  const { filenameContains, waitForComplete, timeoutMs } = opts;
  return new Promise<any>((resolve, reject) => {
    let timer: any = null;
    const onError = (err: any) => {
      cleanup();
      reject(err instanceof Error ? err : new Error(String(err)));
    };
    const cleanup = () => {
      try {
        if (timer) clearTimeout(timer);
      } catch {}
      try {
        chrome.downloads.onCreated.removeListener(onCreated);
      } catch {}
      try {
        chrome.downloads.onChanged.removeListener(onChanged);
      } catch {}
    };
    const matches = (item: chrome.downloads.DownloadItem) => {
      if (!filenameContains) return true;
      const name = (item.filename || '').split(/[/\\]/).pop() || '';
      return name.includes(filenameContains) || (item.url || '').includes(filenameContains);
    };
    const fulfill = async (item: chrome.downloads.DownloadItem) => {
      // try to fill more details via downloads.search
      try {
        const [found] = await chrome.downloads.search({ id: item.id });
        const out = found || item;
        cleanup();
        resolve({
          id: out.id,
          filename: out.filename,
          url: out.url,
          mime: (out as any).mime || undefined,
          fileSize: out.fileSize ?? out.totalBytes ?? undefined,
          state: out.state,
          danger: out.danger,
          startTime: out.startTime,
          endTime: (out as any).endTime || undefined,
          exists: (out as any).exists,
        });
        return;
      } catch {
        cleanup();
        resolve({ id: item.id, filename: item.filename, url: item.url, state: item.state });
      }
    };
    const onCreated = (item: chrome.downloads.DownloadItem) => {
      try {
        if (!matches(item)) return;
        if (!waitForComplete) {
          fulfill(item);
        }
      } catch {}
    };
    const onChanged = (delta: chrome.downloads.DownloadDelta) => {
      try {
        if (!delta || typeof delta.id !== 'number') return;
        // pull item and check
        chrome.downloads
          .search({ id: delta.id })
          .then((arr) => {
            const item = arr && arr[0];
            if (!item) return;
            if (!matches(item)) return;
            if (waitForComplete && item.state === 'complete') fulfill(item);
          })
          .catch(() => {});
      } catch {}
    };
    chrome.downloads.onCreated.addListener(onCreated);
    chrome.downloads.onChanged.addListener(onChanged);
    timer = setTimeout(() => onError(new Error('Download wait timed out')), timeoutMs);
    // Try to find an already-running matching download
    chrome.downloads
      .search({ state: waitForComplete ? 'in_progress' : undefined })
      .then((arr) => {
        const hit = (arr || []).find((d) => matches(d));
        if (hit && !waitForComplete) fulfill(hit);
      })
      .catch(() => {});
  });
}

export const handleDownloadTool = new HandleDownloadTool();

```

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

```vue
<template>
  <div class="flex flex-col gap-2">
    <!-- Root override -->
    <div class="flex items-center gap-2">
      <span class="whitespace-nowrap">Root override</span>
      <input
        :value="projectRoot"
        class="flex-1 border border-slate-200 rounded px-2 py-1 text-xs bg-white focus:outline-none focus:ring-1 focus:ring-slate-400"
        placeholder="Optional override path; defaults to selected project workspace"
        @input="$emit('update:project-root', ($event.target as HTMLInputElement).value)"
        @change="$emit('save-root')"
      />
      <button
        class="btn-secondary !px-2 !py-1 text-[11px]"
        type="button"
        :disabled="isSavingRoot"
        @click="$emit('save-root')"
      >
        {{ isSavingRoot ? 'Saving...' : 'Save' }}
      </button>
    </div>

    <!-- CLI & Model selection -->
    <div class="flex items-center gap-2">
      <span class="whitespace-nowrap">CLI</span>
      <select
        :value="selectedCli"
        class="border border-slate-200 rounded px-2 py-1 text-xs bg-white focus:outline-none focus:ring-1 focus:ring-slate-400"
        @change="handleCliChange"
      >
        <option value="">Auto (per project / server default)</option>
        <option v-for="e in engines" :key="e.name" :value="e.name">
          {{ e.name }}
        </option>
      </select>
      <span class="whitespace-nowrap">Model</span>
      <select
        :value="model"
        class="flex-1 border border-slate-200 rounded px-2 py-1 text-xs bg-white focus:outline-none focus:ring-1 focus:ring-slate-400"
        @change="$emit('update:model', ($event.target as HTMLSelectElement).value)"
      >
        <option value="">Default</option>
        <option v-for="m in availableModels" :key="m.id" :value="m.id">
          {{ m.name }}
        </option>
      </select>
      <!-- CCR option (Claude Code Router) - only shown when Claude CLI is selected -->
      <label
        v-if="showCcrOption"
        class="flex items-center gap-1 whitespace-nowrap cursor-pointer"
        title="Use Claude Code Router for API routing"
      >
        <input
          type="checkbox"
          :checked="useCcr"
          class="w-3 h-3 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
          @change="$emit('update:use-ccr', ($event.target as HTMLInputElement).checked)"
        />
        <span class="text-[11px] text-slate-600">CCR</span>
      </label>
      <button
        class="btn-secondary !px-2 !py-1 text-[11px]"
        type="button"
        :disabled="!selectedProject || isSavingPreference"
        @click="$emit('save-preference')"
      >
        {{ isSavingPreference ? 'Saving...' : 'Save' }}
      </button>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { computed } from 'vue';
import type { AgentProject, AgentEngineInfo } from 'chrome-mcp-shared';
import {
  getModelsForCli,
  getDefaultModelForCli,
  type ModelDefinition,
} from '@/common/agent-models';

const props = defineProps<{
  projectRoot: string;
  selectedCli: string;
  model: string;
  useCcr: boolean;
  engines: AgentEngineInfo[];
  selectedProject: AgentProject | null;
  isSavingRoot: boolean;
  isSavingPreference: boolean;
}>();

const emit = defineEmits<{
  'update:project-root': [value: string];
  'update:selected-cli': [value: string];
  'update:model': [value: string];
  'update:use-ccr': [value: boolean];
  'save-root': [];
  'save-preference': [];
}>();

// Get available models based on selected CLI
const availableModels = computed<ModelDefinition[]>(() => {
  return getModelsForCli(props.selectedCli);
});

// Show CCR option only when Claude CLI is selected
const showCcrOption = computed(() => {
  return props.selectedCli === 'claude';
});

// Handle CLI change - auto-select default model for the CLI
function handleCliChange(event: Event): void {
  const cli = (event.target as HTMLSelectElement).value;
  emit('update:selected-cli', cli);

  // Auto-select default model when CLI changes
  if (cli) {
    const defaultModel = getDefaultModelForCli(cli);
    emit('update:model', defaultModel);
  } else {
    emit('update:model', '');
  }

  // Reset CCR when switching away from Claude
  if (cli !== 'claude') {
    emit('update:use-ccr', false);
  }
}
</script>

```

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

```typescript
/**
 * @fileoverview Command Trigger Handler (P4-04)
 * @description
 * Listens to `chrome.commands.onCommand` and fires installed command triggers.
 *
 * Command triggers allow users to execute flows via keyboard shortcuts
 * defined in the extension's manifest.
 *
 * Design notes:
 * - Commands must be registered in manifest.json under the "commands" key
 * - Each command is identified by its commandKey (e.g., "run-flow-1")
 * - Active tab info is captured when available
 */

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

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

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

type CommandTriggerSpec = TriggerSpecByKind<'command'>;

interface InstalledCommandTrigger {
  spec: CommandTriggerSpec;
}

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

/**
 * Create command trigger handler factory
 */
export function createCommandTriggerHandlerFactory(
  deps?: CommandTriggerHandlerDeps,
): TriggerHandlerFactory<'command'> {
  return (fireCallback) => createCommandTriggerHandler(fireCallback, deps);
}

/**
 * Create command trigger handler
 */
export function createCommandTriggerHandler(
  fireCallback: TriggerFireCallback,
  deps?: CommandTriggerHandlerDeps,
): TriggerHandler<'command'> {
  const logger = deps?.logger ?? console;

  // Map commandKey -> triggerId for fast lookup
  const commandKeyToTriggerId = new Map<string, TriggerId>();
  const installed = new Map<TriggerId, InstalledCommandTrigger>();
  let listening = false;

  /**
   * Handle chrome.commands.onCommand event
   */
  const onCommand = (command: string, tab?: chrome.tabs.Tab): void => {
    const triggerId = commandKeyToTriggerId.get(command);
    if (!triggerId) return;

    const trigger = installed.get(triggerId);
    if (!trigger) return;

    // Fire and forget: chrome event listeners should not block
    Promise.resolve(
      fireCallback.onFire(triggerId, {
        sourceTabId: tab?.id,
        sourceUrl: tab?.url,
      }),
    ).catch((e) => {
      logger.error(`[CommandTriggerHandler] onFire failed for trigger "${triggerId}":`, e);
    });
  };

  /**
   * Ensure listener is registered
   */
  function ensureListening(): void {
    if (listening) return;
    if (!chrome.commands?.onCommand?.addListener) {
      logger.warn('[CommandTriggerHandler] chrome.commands.onCommand is unavailable');
      return;
    }
    chrome.commands.onCommand.addListener(onCommand);
    listening = true;
  }

  /**
   * Stop listening
   */
  function stopListening(): void {
    if (!listening) return;
    try {
      chrome.commands.onCommand.removeListener(onCommand);
    } catch (e) {
      logger.debug('[CommandTriggerHandler] removeListener failed:', e);
    } finally {
      listening = false;
    }
  }

  return {
    kind: 'command',

    async install(trigger: CommandTriggerSpec): Promise<void> {
      const { id, commandKey } = trigger;

      // Warn if commandKey already used by another trigger
      const existingTriggerId = commandKeyToTriggerId.get(commandKey);
      if (existingTriggerId && existingTriggerId !== id) {
        logger.warn(
          `[CommandTriggerHandler] Command "${commandKey}" already used by trigger "${existingTriggerId}", overwriting with "${id}"`,
        );
        // Remove old mapping
        installed.delete(existingTriggerId);
      }

      installed.set(id, { spec: trigger });
      commandKeyToTriggerId.set(commandKey, id);
      ensureListening();
    },

    async uninstall(triggerId: string): Promise<void> {
      const trigger = installed.get(triggerId as TriggerId);
      if (trigger) {
        commandKeyToTriggerId.delete(trigger.spec.commandKey);
        installed.delete(triggerId as TriggerId);
      }

      if (installed.size === 0) {
        stopListening();
      }
    },

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

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

```

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

```typescript
/**
 * Composable for textarea auto-resize functionality.
 * Automatically adjusts textarea height based on content while respecting min/max constraints.
 */
import { ref, watch, nextTick, onMounted, onUnmounted, type Ref } from 'vue';

export interface UseTextareaAutoResizeOptions {
  /** Ref to the textarea element */
  textareaRef: Ref<HTMLTextAreaElement | null>;
  /** Ref to the textarea value (for watching changes) */
  value: Ref<string>;
  /** Minimum height in pixels */
  minHeight?: number;
  /** Maximum height in pixels */
  maxHeight?: number;
}

export interface UseTextareaAutoResizeReturn {
  /** Current calculated height */
  height: Ref<number>;
  /** Whether content exceeds max height (textarea is overflowing) */
  isOverflowing: Ref<boolean>;
  /** Manually trigger height recalculation */
  recalculate: () => void;
}

const DEFAULT_MIN_HEIGHT = 50;
const DEFAULT_MAX_HEIGHT = 200;

/**
 * Composable for auto-resizing textarea based on content.
 *
 * Features:
 * - Automatically adjusts height on input
 * - Respects min/max height constraints
 * - Handles width changes (line wrapping affects height)
 * - Uses requestAnimationFrame for performance
 */
export function useTextareaAutoResize(
  options: UseTextareaAutoResizeOptions,
): UseTextareaAutoResizeReturn {
  const {
    textareaRef,
    value,
    minHeight = DEFAULT_MIN_HEIGHT,
    maxHeight = DEFAULT_MAX_HEIGHT,
  } = options;

  const height = ref<number>(minHeight);
  const isOverflowing = ref(false);

  let scheduled = false;
  let resizeObserver: ResizeObserver | null = null;
  let lastWidth = 0;

  /**
   * Calculate textarea height based on content.
   * Only updates the reactive `height` and `isOverflowing` refs.
   * The actual DOM height is controlled via :style binding in the template.
   */
  function recalculate(): void {
    const el = textareaRef.value;
    if (!el) return;

    // Temporarily set height to 'auto' to get accurate scrollHeight
    // Save current height to minimize visual flicker
    const currentHeight = el.style.height;
    el.style.height = 'auto';

    const contentHeight = el.scrollHeight;
    const clampedHeight = Math.min(maxHeight, Math.max(minHeight, contentHeight));

    // Restore height immediately (the actual height is controlled by Vue binding)
    el.style.height = currentHeight;

    // Update reactive state
    height.value = clampedHeight;
    // Add small tolerance (1px) to account for rounding
    isOverflowing.value = contentHeight > maxHeight + 1;
  }

  /**
   * Schedule height recalculation using requestAnimationFrame.
   * Batches multiple calls within the same frame for performance.
   */
  function scheduleRecalculate(): void {
    if (scheduled) return;
    scheduled = true;
    requestAnimationFrame(() => {
      scheduled = false;
      recalculate();
    });
  }

  // Watch value changes
  watch(
    value,
    async () => {
      await nextTick();
      scheduleRecalculate();
    },
    { flush: 'post' },
  );

  // Watch textarea ref changes (in case it's replaced)
  watch(
    textareaRef,
    async (newEl, oldEl) => {
      // Cleanup old observer
      if (resizeObserver && oldEl) {
        resizeObserver.unobserve(oldEl);
      }

      if (!newEl) return;

      await nextTick();
      scheduleRecalculate();

      // Setup new observer for width changes
      if (resizeObserver) {
        lastWidth = newEl.offsetWidth;
        resizeObserver.observe(newEl);
      }
    },
    { immediate: true },
  );

  onMounted(() => {
    const el = textareaRef.value;
    if (!el) return;

    // Initial calculation
    scheduleRecalculate();

    // Setup ResizeObserver for width changes
    // Width changes affect line wrapping, which affects scrollHeight
    if (typeof ResizeObserver !== 'undefined') {
      lastWidth = el.offsetWidth;
      resizeObserver = new ResizeObserver(() => {
        const current = textareaRef.value;
        if (!current) return;

        const currentWidth = current.offsetWidth;
        if (currentWidth !== lastWidth) {
          lastWidth = currentWidth;
          scheduleRecalculate();
        }
      });
      resizeObserver.observe(el);
    }
  });

  onUnmounted(() => {
    resizeObserver?.disconnect();
    resizeObserver = null;
  });

  return {
    height,
    isOverflowing,
    recalculate,
  };
}

```

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

```typescript
/**
 * Composable for managing AgentChat theme.
 * Handles theme persistence and application.
 */
import { ref, type Ref } from 'vue';

/** Available theme identifiers */
export type AgentThemeId =
  | 'warm-editorial'
  | 'blueprint-architect'
  | 'zen-journal'
  | 'neo-pop'
  | 'dark-console'
  | 'swiss-grid';

/** Storage key for persisting theme preference */
const STORAGE_KEY_THEME = 'agentTheme';

/** Default theme when none is set */
const DEFAULT_THEME: AgentThemeId = 'warm-editorial';

/** Valid theme IDs for validation */
const VALID_THEMES: AgentThemeId[] = [
  'warm-editorial',
  'blueprint-architect',
  'zen-journal',
  'neo-pop',
  'dark-console',
  'swiss-grid',
];

/** Theme display names for UI */
export const THEME_LABELS: Record<AgentThemeId, string> = {
  'warm-editorial': 'Editorial',
  'blueprint-architect': 'Blueprint',
  'zen-journal': 'Zen',
  'neo-pop': 'Neo-Pop',
  'dark-console': 'Console',
  'swiss-grid': 'Swiss',
};

export interface UseAgentTheme {
  /** Current theme ID */
  theme: Ref<AgentThemeId>;
  /** Whether theme has been loaded from storage */
  ready: Ref<boolean>;
  /** Set and persist a new theme */
  setTheme: (id: AgentThemeId) => Promise<void>;
  /** Load theme from storage (call on mount) */
  initTheme: () => Promise<void>;
  /** Apply theme to a DOM element */
  applyTo: (el: HTMLElement) => void;
  /** Get the preloaded theme from document (set by main.ts) */
  getPreloadedTheme: () => AgentThemeId;
}

/**
 * Check if a string is a valid theme ID
 */
function isValidTheme(value: unknown): value is AgentThemeId {
  return typeof value === 'string' && VALID_THEMES.includes(value as AgentThemeId);
}

/**
 * Get theme from document element (preloaded by main.ts)
 */
function getThemeFromDocument(): AgentThemeId {
  const value = document.documentElement.dataset.agentTheme;
  return isValidTheme(value) ? value : DEFAULT_THEME;
}

/**
 * Composable for managing AgentChat theme
 */
export function useAgentTheme(): UseAgentTheme {
  // Initialize with preloaded theme (or default)
  const theme = ref<AgentThemeId>(getThemeFromDocument());
  const ready = ref(false);

  /**
   * Load theme from chrome.storage.local
   */
  async function initTheme(): Promise<void> {
    try {
      const result = await chrome.storage.local.get(STORAGE_KEY_THEME);
      const stored = result[STORAGE_KEY_THEME];

      if (isValidTheme(stored)) {
        theme.value = stored;
      } else {
        // Use preloaded or default
        theme.value = getThemeFromDocument();
      }
    } catch (error) {
      console.error('[useAgentTheme] Failed to load theme:', error);
      theme.value = getThemeFromDocument();
    } finally {
      ready.value = true;
    }
  }

  /**
   * Set and persist a new theme
   */
  async function setTheme(id: AgentThemeId): Promise<void> {
    if (!isValidTheme(id)) {
      console.warn('[useAgentTheme] Invalid theme ID:', id);
      return;
    }

    // Update immediately for responsive UI
    theme.value = id;

    // Also update document element for consistency
    document.documentElement.dataset.agentTheme = id;

    // Persist to storage
    try {
      await chrome.storage.local.set({ [STORAGE_KEY_THEME]: id });
    } catch (error) {
      console.error('[useAgentTheme] Failed to save theme:', error);
    }
  }

  /**
   * Apply theme to a DOM element
   */
  function applyTo(el: HTMLElement): void {
    el.dataset.agentTheme = theme.value;
  }

  /**
   * Get the preloaded theme from document
   */
  function getPreloadedTheme(): AgentThemeId {
    return getThemeFromDocument();
  }

  return {
    theme,
    ready,
    setTheme,
    initTheme,
    applyTo,
    getPreloadedTheme,
  };
}

/**
 * Preload theme before Vue mounts (call in main.ts)
 * This prevents theme flashing on page load.
 */
export async function preloadAgentTheme(): Promise<AgentThemeId> {
  let themeId: AgentThemeId = DEFAULT_THEME;

  try {
    const result = await chrome.storage.local.get(STORAGE_KEY_THEME);
    const stored = result[STORAGE_KEY_THEME];

    if (isValidTheme(stored)) {
      themeId = stored;
    }
  } catch (error) {
    console.error('[preloadAgentTheme] Failed to load theme:', error);
  }

  // Set on document element for immediate application
  document.documentElement.dataset.agentTheme = themeId;

  return themeId;
}

```

--------------------------------------------------------------------------------
/app/chrome-extension/tests/record-replay-v3/manual-trigger.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Manual Trigger Handler 测试 (P4-08)
 * @description
 * Tests for:
 * - Basic install/uninstall operations
 * - getInstalledIds tracking
 */

import { describe, expect, it, vi } from 'vitest';

import type { TriggerSpecByKind } from '@/entrypoints/background/record-replay-v3/domain/triggers';
import type { TriggerFireCallback } from '@/entrypoints/background/record-replay-v3/engine/triggers/trigger-handler';
import { createManualTriggerHandlerFactory } from '@/entrypoints/background/record-replay-v3/engine/triggers/manual-trigger';

// ==================== Test Utilities ====================

function createSilentLogger(): Pick<Console, 'debug' | 'info' | 'warn' | 'error'> {
  return {
    debug: () => {},
    info: () => {},
    warn: () => {},
    error: () => {},
  };
}

// ==================== Manual Trigger Tests ====================

describe('V3 ManualTriggerHandler', () => {
  describe('Installation', () => {
    it('installs trigger', async () => {
      const fireCallback: TriggerFireCallback = { onFire: vi.fn(async () => {}) };
      const handler = createManualTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      const trigger: TriggerSpecByKind<'manual'> = {
        id: 't1' as never,
        kind: 'manual',
        enabled: true,
        flowId: 'flow-1' as never,
      };

      await handler.install(trigger);

      expect(handler.getInstalledIds()).toEqual(['t1']);
    });

    it('installs multiple triggers', async () => {
      const fireCallback: TriggerFireCallback = { onFire: vi.fn(async () => {}) };
      const handler = createManualTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      await handler.install({
        id: 't1' as never,
        kind: 'manual',
        enabled: true,
        flowId: 'flow-1' as never,
      });

      await handler.install({
        id: 't2' as never,
        kind: 'manual',
        enabled: true,
        flowId: 'flow-2' as never,
      });

      expect(handler.getInstalledIds().sort()).toEqual(['t1', 't2']);
    });
  });

  describe('Uninstallation', () => {
    it('uninstalls trigger', async () => {
      const fireCallback: TriggerFireCallback = { onFire: vi.fn(async () => {}) };
      const handler = createManualTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      await handler.install({
        id: 't1' as never,
        kind: 'manual',
        enabled: true,
        flowId: 'flow-1' as never,
      });

      await handler.uninstall('t1');

      expect(handler.getInstalledIds()).toEqual([]);
    });

    it('uninstallAll clears all triggers', async () => {
      const fireCallback: TriggerFireCallback = { onFire: vi.fn(async () => {}) };
      const handler = createManualTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      await handler.install({
        id: 't1' as never,
        kind: 'manual',
        enabled: true,
        flowId: 'flow-1' as never,
      });

      await handler.install({
        id: 't2' as never,
        kind: 'manual',
        enabled: true,
        flowId: 'flow-2' as never,
      });

      await handler.uninstallAll();

      expect(handler.getInstalledIds()).toEqual([]);
    });
  });

  describe('getInstalledIds', () => {
    it('returns empty array when no triggers installed', async () => {
      const fireCallback: TriggerFireCallback = { onFire: vi.fn(async () => {}) };
      const handler = createManualTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      expect(handler.getInstalledIds()).toEqual([]);
    });

    it('tracks partial uninstall', async () => {
      const fireCallback: TriggerFireCallback = { onFire: vi.fn(async () => {}) };
      const handler = createManualTriggerHandlerFactory({ logger: createSilentLogger() })(
        fireCallback,
      );

      await handler.install({
        id: 't1' as never,
        kind: 'manual',
        enabled: true,
        flowId: 'flow-1' as never,
      });

      await handler.install({
        id: 't2' as never,
        kind: 'manual',
        enabled: true,
        flowId: 'flow-2' as never,
      });

      await handler.uninstall('t1');

      expect(handler.getInstalledIds()).toEqual(['t2']);
    });
  });
});

```

--------------------------------------------------------------------------------
/app/native-server/src/agent/directory-picker.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Directory Picker Service.
 *
 * Provides cross-platform directory selection using native system dialogs.
 * Uses platform-specific commands:
 * - macOS: osascript (AppleScript)
 * - Windows: PowerShell
 * - Linux: zenity or kdialog
 */
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import os from 'node:os';

const execAsync = promisify(exec);

export interface DirectoryPickerResult {
  success: boolean;
  path?: string;
  cancelled?: boolean;
  error?: string;
}

/**
 * Open a native directory picker dialog.
 * Returns the selected directory path or indicates cancellation.
 */
export async function openDirectoryPicker(
  title = 'Select Project Directory',
): Promise<DirectoryPickerResult> {
  const platform = os.platform();

  try {
    switch (platform) {
      case 'darwin':
        return await openMacOSPicker(title);
      case 'win32':
        return await openWindowsPicker(title);
      case 'linux':
        return await openLinuxPicker(title);
      default:
        return {
          success: false,
          error: `Unsupported platform: ${platform}`,
        };
    }
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : String(error),
    };
  }
}

/**
 * macOS: Use osascript to open Finder folder picker.
 */
async function openMacOSPicker(title: string): Promise<DirectoryPickerResult> {
  const script = `
    set selectedFolder to choose folder with prompt "${title}"
    return POSIX path of selectedFolder
  `;

  try {
    const { stdout } = await execAsync(`osascript -e '${script}'`);
    const path = stdout.trim();
    if (path) {
      return { success: true, path };
    }
    return { success: false, cancelled: true };
  } catch (error) {
    // User cancelled returns error code 1
    const err = error as { code?: number; stderr?: string };
    if (err.code === 1) {
      return { success: false, cancelled: true };
    }
    throw error;
  }
}

/**
 * Windows: Use PowerShell to open folder browser dialog.
 */
async function openWindowsPicker(title: string): Promise<DirectoryPickerResult> {
  const psScript = `
    Add-Type -AssemblyName System.Windows.Forms
    $dialog = New-Object System.Windows.Forms.FolderBrowserDialog
    $dialog.Description = "${title}"
    $dialog.ShowNewFolderButton = $true
    $result = $dialog.ShowDialog()
    if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
      Write-Output $dialog.SelectedPath
    }
  `;

  // Escape for command line
  const escapedScript = psScript.replace(/"/g, '\\"').replace(/\n/g, ' ');

  try {
    const { stdout } = await execAsync(
      `powershell -NoProfile -Command "${escapedScript}"`,
      { timeout: 60000 }, // 60 second timeout
    );
    const path = stdout.trim();
    if (path) {
      return { success: true, path };
    }
    return { success: false, cancelled: true };
  } catch (error) {
    const err = error as { killed?: boolean };
    if (err.killed) {
      return { success: false, error: 'Dialog timed out' };
    }
    throw error;
  }
}

/**
 * Linux: Try zenity first, then kdialog as fallback.
 */
async function openLinuxPicker(title: string): Promise<DirectoryPickerResult> {
  // Try zenity first (GTK)
  try {
    const { stdout } = await execAsync(`zenity --file-selection --directory --title="${title}"`, {
      timeout: 60000,
    });
    const path = stdout.trim();
    if (path) {
      return { success: true, path };
    }
    return { success: false, cancelled: true };
  } catch (zenityError) {
    // zenity returns exit code 1 on cancel, 5 if not installed
    const err = zenityError as { code?: number };
    if (err.code === 1) {
      return { success: false, cancelled: true };
    }

    // Try kdialog as fallback (KDE)
    try {
      const { stdout } = await execAsync(`kdialog --getexistingdirectory ~ --title "${title}"`, {
        timeout: 60000,
      });
      const path = stdout.trim();
      if (path) {
        return { success: true, path };
      }
      return { success: false, cancelled: true };
    } catch (kdialogError) {
      const kdErr = kdialogError as { code?: number };
      if (kdErr.code === 1) {
        return { success: false, cancelled: true };
      }

      return {
        success: false,
        error: 'No directory picker available. Please install zenity or kdialog.',
      };
    }
  }
}

```

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

```vue
<template>
  <div
    ref="chipRef"
    class="relative inline-flex items-center gap-1.5 text-[11px] leading-none flex-shrink-0 select-none"
    :style="chipStyle"
    @mouseenter="handleMouseEnter"
    @mouseleave="handleMouseLeave"
  >
    <!-- Selection Icon -->
    <span class="inline-flex items-center justify-center w-3.5 h-3.5" :style="iconStyle">
      <svg
        class="w-3.5 h-3.5"
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
        aria-hidden="true"
      >
        <path
          stroke-linecap="round"
          stroke-linejoin="round"
          stroke-width="2"
          d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
        />
      </svg>
    </span>

    <!-- Element Label (tagName only) -->
    <span class="truncate max-w-[140px] px-1 py-0.5" :style="labelStyle">
      {{ chipTagName }}
    </span>

    <!-- "Selected" Indicator -->
    <span class="px-1 py-0.5 text-[9px] uppercase tracking-wider" :style="pillStyle"> sel </span>
  </div>
</template>

<script lang="ts" setup>
import { computed, ref, onUnmounted } from 'vue';
import type { SelectedElementSummary } from '@/common/web-editor-types';

// =============================================================================
// Props & Emits
// =============================================================================

const props = defineProps<{
  /** Selected element summary to display */
  selected: SelectedElementSummary;
}>();

const emit = defineEmits<{
  /** Mouse enter - start highlight */
  'hover:start': [selected: SelectedElementSummary];
  /** Mouse leave - clear highlight */
  'hover:end': [selected: SelectedElementSummary];
}>();

// =============================================================================
// Local State
// =============================================================================

const chipRef = ref<HTMLDivElement | null>(null);
const isHovering = ref(false);

// =============================================================================
// Computed: UI State
// =============================================================================

/**
 * Use tagName for compact chip display.
 * Falls back to extracting from label if tagName is not available.
 */
const chipTagName = computed(() => {
  // First try explicit tagName
  if (props.selected.tagName) {
    return props.selected.tagName.toLowerCase();
  }
  // Fallback: extract from label
  const label = (props.selected.label || '').trim();
  const match = label.match(/^([a-zA-Z][a-zA-Z0-9-]*)/);
  return match?.[1]?.toLowerCase() || 'element';
});

// =============================================================================
// Computed: Styles
// =============================================================================

const chipStyle = computed(() => ({
  backgroundColor: isHovering.value ? 'var(--ac-hover-bg)' : 'var(--ac-surface)',
  border: `var(--ac-border-width) solid ${isHovering.value ? 'var(--ac-accent)' : 'var(--ac-border)'}`,
  borderRadius: 'var(--ac-radius-button)',
  boxShadow: isHovering.value ? 'var(--ac-shadow-card)' : 'none',
  color: 'var(--ac-text)',
  cursor: 'default',
}));

const iconStyle = computed(() => ({
  color: 'var(--ac-accent)',
}));

const labelStyle = computed(() => ({
  fontFamily: 'var(--ac-font-mono)',
}));

const pillStyle = computed(() => ({
  backgroundColor: 'var(--ac-accent)',
  color: 'var(--ac-accent-contrast)',
  borderRadius: 'var(--ac-radius-button)',
  fontFamily: 'var(--ac-font-mono)',
  fontWeight: '600',
}));

// =============================================================================
// Event Handlers
// =============================================================================

function handleMouseEnter(): void {
  isHovering.value = true;
  emit('hover:start', props.selected);
}

function handleMouseLeave(): void {
  isHovering.value = false;
  emit('hover:end', props.selected);
}

// =============================================================================
// Lifecycle
// =============================================================================

onUnmounted(() => {
  // Clear any active highlight when chip is unmounted
  // (e.g., when selection changes or element appears in edits)
  if (isHovering.value) {
    emit('hover:end', props.selected);
  }
});
</script>

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/style.css:
--------------------------------------------------------------------------------

```css
/* 现代化全局样式 */
:root {
  /* 字体系统 */
  font-family:
    -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  line-height: 1.6;
  font-weight: 400;

  /* 颜色系统 */
  --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  --primary-color: #667eea;
  --primary-dark: #5a67d8;
  --secondary-color: #764ba2;

  --success-color: #48bb78;
  --warning-color: #ed8936;
  --error-color: #f56565;
  --info-color: #4299e1;

  --text-primary: #2d3748;
  --text-secondary: #4a5568;
  --text-muted: #718096;
  --text-light: #a0aec0;

  --bg-primary: #ffffff;
  --bg-secondary: #f7fafc;
  --bg-tertiary: #edf2f7;
  --bg-overlay: rgba(255, 255, 255, 0.95);

  --border-color: #e2e8f0;
  --border-light: #f1f5f9;
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
  --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1);

  /* 间距系统 */
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 12px;
  --spacing-lg: 16px;
  --spacing-xl: 20px;
  --spacing-2xl: 24px;
  --spacing-3xl: 32px;

  /* 圆角系统 */
  --radius-sm: 4px;
  --radius-md: 6px;
  --radius-lg: 8px;
  --radius-xl: 12px;
  --radius-2xl: 16px;

  /* 动画 */
  --transition-fast: 0.15s ease;
  --transition-normal: 0.3s ease;
  --transition-slow: 0.5s ease;

  /* 字体渲染优化 */
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
}

/* 重置样式 */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  margin: 0;
  padding: 0;
  width: 400px;
  min-height: 500px;
  max-height: 600px;
  overflow: hidden;
  font-family: inherit;
  background: var(--bg-secondary);
  color: var(--text-primary);
}

#app {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

/* 链接样式 */
a {
  color: var(--primary-color);
  text-decoration: none;
  transition: color var(--transition-fast);
}

a:hover {
  color: var(--primary-dark);
}

/* 按钮基础样式重置 */
button {
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  border: none;
  background: none;
  cursor: pointer;
  transition: all var(--transition-normal);
}

button:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

/* 输入框基础样式 */
input,
textarea,
select {
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  padding: var(--spacing-sm) var(--spacing-md);
  background: var(--bg-primary);
  color: var(--text-primary);
  transition: all var(--transition-fast);
}

input:focus,
textarea:focus,
select:focus {
  outline: none;
  border-color: var(--primary-color);
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}

/* 滚动条样式 */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-track {
  background: var(--bg-tertiary);
  border-radius: var(--radius-sm);
}

::-webkit-scrollbar-thumb {
  background: var(--border-color);
  border-radius: var(--radius-sm);
  transition: background var(--transition-fast);
}

::-webkit-scrollbar-thumb:hover {
  background: var(--text-muted);
}

/* 选择文本样式 */
::selection {
  background: rgba(102, 126, 234, 0.2);
  color: var(--text-primary);
}

/* 焦点可见性 */
:focus-visible {
  outline: 2px solid var(--primary-color);
  outline-offset: 2px;
}

/* 动画关键帧 */
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes slideDown {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes scaleIn {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* 响应式断点 */
@media (max-width: 420px) {
  :root {
    --spacing-xs: 3px;
    --spacing-sm: 6px;
    --spacing-md: 10px;
    --spacing-lg: 14px;
    --spacing-xl: 18px;
    --spacing-2xl: 22px;
    --spacing-3xl: 28px;
  }
}

/* 高对比度模式支持 */
@media (prefers-contrast: high) {
  :root {
    --border-color: #000000;
    --text-muted: #000000;
  }
}

/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

```

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

```typescript
/**
 * @fileoverview RunEvent 持久化
 * @description 实现事件的原子 seq 分配和存储
 */

import type { RunId } from '../domain/ids';
import type { RunEvent, RunEventInput, RunRecordV3 } from '../domain/events';
import { RR_ERROR_CODES, createRRError } from '../domain/errors';
import type { EventsStore } from '../engine/storage/storage-port';
import { RR_V3_STORES, withTransaction } from './db';

/**
 * IDB request helper - promisify IDBRequest with RRError wrapping
 */
function idbRequest<T>(request: IDBRequest<T>, context: string): Promise<T> {
  return new Promise((resolve, reject) => {
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => {
      const error = request.error;
      reject(
        createRRError(
          RR_ERROR_CODES.INTERNAL,
          `IDB error in ${context}: ${error?.message ?? 'unknown'}`,
        ),
      );
    };
  });
}

/**
 * 创建 EventsStore 实现
 * @description
 * - append() 在单个事务中原子分配 seq
 * - seq 由 RunRecordV3.nextSeq 作为单一事实来源
 */
export function createEventsStore(): EventsStore {
  return {
    /**
     * 追加事件并原子分配 seq
     * @description 在单个事务中:读取 RunRecordV3.nextSeq -> 写入事件 -> 递增 nextSeq
     */
    async append(input: RunEventInput): Promise<RunEvent> {
      return withTransaction(
        [RR_V3_STORES.RUNS, RR_V3_STORES.EVENTS],
        'readwrite',
        async (stores) => {
          const runsStore = stores[RR_V3_STORES.RUNS];
          const eventsStore = stores[RR_V3_STORES.EVENTS];

          // Step 1: Read nextSeq from RunRecordV3 (single source of truth)
          const run = await idbRequest<RunRecordV3 | undefined>(
            runsStore.get(input.runId),
            `append.getRun(${input.runId})`,
          );

          if (!run) {
            throw createRRError(
              RR_ERROR_CODES.INTERNAL,
              `Run "${input.runId}" not found when appending event`,
            );
          }

          const seq = run.nextSeq;

          // Validate seq integrity
          if (!Number.isSafeInteger(seq) || seq < 0) {
            throw createRRError(
              RR_ERROR_CODES.INVARIANT_VIOLATION,
              `Invalid nextSeq for run "${input.runId}": ${String(seq)}`,
            );
          }

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

          // Step 3: Write event to events store
          await idbRequest(eventsStore.add(event), `append.addEvent(${input.runId}, seq=${seq})`);

          // Step 4: Increment nextSeq in runs store (same transaction)
          const updatedRun: RunRecordV3 = {
            ...run,
            nextSeq: seq + 1,
            updatedAt: Date.now(),
          };

          await idbRequest(
            runsStore.put(updatedRun),
            `append.updateNextSeq(${input.runId}, nextSeq=${seq + 1})`,
          );

          return event;
        },
      );
    },

    /**
     * 列出事件
     * @description 利用复合主键 [runId, seq] 实现高效范围查询
     */
    async list(runId: RunId, opts?: { fromSeq?: number; limit?: number }): Promise<RunEvent[]> {
      return withTransaction(RR_V3_STORES.EVENTS, 'readonly', async (stores) => {
        const store = stores[RR_V3_STORES.EVENTS];
        const fromSeq = opts?.fromSeq ?? 0;
        const limit = opts?.limit;

        // Early return for zero limit
        if (limit === 0) {
          return [];
        }

        return new Promise<RunEvent[]>((resolve, reject) => {
          const results: RunEvent[] = [];

          // Use compound primary key [runId, seq] for efficient range query
          // This yields events in seq-ascending order naturally
          const range = IDBKeyRange.bound([runId, fromSeq], [runId, Number.MAX_SAFE_INTEGER]);

          const request = store.openCursor(range);

          request.onsuccess = () => {
            const cursor = request.result;

            if (!cursor) {
              resolve(results);
              return;
            }

            const event = cursor.value as RunEvent;
            results.push(event);

            // Check limit
            if (limit !== undefined && results.length >= limit) {
              resolve(results);
              return;
            }

            cursor.continue();
          };

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

```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/web-editor-v2/ui/property-panel/components/slider-input.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Slider Input Component
 *
 * A reusable "slider + input" control for numeric values:
 * - Left: native range slider for visual manipulation
 * - Right: InputContainer-backed numeric input for precise values
 *
 * Features:
 * - Bidirectional synchronization between slider and input
 * - Supports disabled state
 * - Accessible with ARIA labels
 *
 * Styling is defined in shadow-host.ts:
 * - `.we-slider-input`
 * - `.we-slider-input__slider`
 * - `.we-slider-input__number`
 */

import { createInputContainer, type InputContainer } from './input-container';

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

export interface SliderInputOptions {
  /** Accessible label for the range slider */
  sliderAriaLabel: string;
  /** Accessible label for the numeric input */
  inputAriaLabel: string;
  /** Minimum value for the slider */
  min: number;
  /** Maximum value for the slider */
  max: number;
  /** Step increment for the slider */
  step: number;
  /** Input mode for the numeric input (default: "decimal") */
  inputMode?: string;
  /** Fixed width for the numeric input in pixels (default: 72) */
  inputWidthPx?: number;
}

export interface SliderInput {
  /** Root container element */
  root: HTMLDivElement;
  /** Range slider element */
  slider: HTMLInputElement;
  /** Numeric input element */
  input: HTMLInputElement;
  /** Input container instance for advanced customization */
  inputContainer: InputContainer;
  /** Set disabled state for both controls */
  setDisabled(disabled: boolean): void;
  /** Set disabled state for slider only */
  setSliderDisabled(disabled: boolean): void;
  /** Set value for both controls */
  setValue(value: number): void;
  /** Set slider value only (without affecting input) */
  setSliderValue(value: number): void;
}

// =============================================================================
// Factory
// =============================================================================

/**
 * Create a slider input component with synchronized slider and input
 */
export function createSliderInput(options: SliderInputOptions): SliderInput {
  const {
    sliderAriaLabel,
    inputAriaLabel,
    min,
    max,
    step,
    inputMode = 'decimal',
    inputWidthPx = 72,
  } = options;

  // Root container
  const root = document.createElement('div');
  root.className = 'we-slider-input';

  // Range slider
  const slider = document.createElement('input');
  slider.type = 'range';
  slider.className = 'we-slider-input__slider';
  slider.min = String(min);
  slider.max = String(max);
  slider.step = String(step);
  slider.value = String(min);
  slider.setAttribute('aria-label', sliderAriaLabel);

  /**
   * Update the slider's progress color based on current value.
   * Uses CSS custom property --progress for the gradient.
   */
  function updateSliderProgress(): void {
    const value = parseFloat(slider.value);
    const minVal = parseFloat(slider.min);
    const maxVal = parseFloat(slider.max);
    const percent = ((value - minVal) / (maxVal - minVal)) * 100;
    slider.style.setProperty('--progress', `${percent}%`);
  }

  // Initialize progress
  updateSliderProgress();

  // Update progress on input
  slider.addEventListener('input', updateSliderProgress);

  // Numeric input using InputContainer
  const inputContainer = createInputContainer({
    ariaLabel: inputAriaLabel,
    inputMode,
    prefix: null,
    suffix: null,
    rootClassName: 'we-slider-input__number',
  });
  inputContainer.root.style.width = `${inputWidthPx}px`;
  inputContainer.root.style.flex = '0 0 auto';

  root.append(slider, inputContainer.root);

  // Public methods
  function setDisabled(disabled: boolean): void {
    slider.disabled = disabled;
    inputContainer.input.disabled = disabled;
  }

  function setSliderDisabled(disabled: boolean): void {
    slider.disabled = disabled;
  }

  function setValue(value: number): void {
    const stringValue = String(value);
    slider.value = stringValue;
    inputContainer.input.value = stringValue;
    updateSliderProgress();
  }

  function setSliderValue(value: number): void {
    slider.value = String(value);
    updateSliderProgress();
  }

  return {
    root,
    slider,
    input: inputContainer.input,
    inputContainer,
    setDisabled,
    setSliderDisabled,
    setValue,
    setSliderValue,
  };
}

```
Page 3/43FirstPrevNextLast