#
tokens: 44138/50000 2/574 files (page 46/60)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 46 of 60. Use http://codebase.md/hangwin/mcp-chrome?lines=true&page={x} to view the full context.

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/app/chrome-extension/utils/vector-database.ts:
--------------------------------------------------------------------------------

```typescript
   1 | /**
   2 |  * Vector database manager
   3 |  * Uses hnswlib-wasm for high-performance vector similarity search
   4 |  * Implements singleton pattern to avoid duplicate WASM module initialization
   5 |  */
   6 | 
   7 | import { loadHnswlib } from 'hnswlib-wasm-static';
   8 | import type { TextChunk } from './text-chunker';
   9 | 
  10 | export interface VectorDocument {
  11 |   id: string;
  12 |   tabId: number;
  13 |   url: string;
  14 |   title: string;
  15 |   chunk: TextChunk;
  16 |   embedding: Float32Array;
  17 |   timestamp: number;
  18 | }
  19 | 
  20 | export interface SearchResult {
  21 |   document: VectorDocument;
  22 |   similarity: number;
  23 |   distance: number;
  24 | }
  25 | 
  26 | export interface VectorDatabaseConfig {
  27 |   dimension: number;
  28 |   maxElements: number;
  29 |   efConstruction: number;
  30 |   M: number;
  31 |   efSearch: number;
  32 |   indexFileName: string;
  33 |   enableAutoCleanup?: boolean;
  34 |   maxRetentionDays?: number;
  35 | }
  36 | 
  37 | let globalHnswlib: any = null;
  38 | let globalHnswlibInitPromise: Promise<any> | null = null;
  39 | let globalHnswlibInitialized = false;
  40 | 
  41 | let syncInProgress = false;
  42 | let pendingSyncPromise: Promise<void> | null = null;
  43 | 
  44 | const DB_NAME = 'VectorDatabaseStorage';
  45 | const DB_VERSION = 1;
  46 | const STORE_NAME = 'documentMappings';
  47 | 
  48 | /**
  49 |  * IndexedDB helper functions
  50 |  */
  51 | class IndexedDBHelper {
  52 |   private static dbPromise: Promise<IDBDatabase> | null = null;
  53 | 
  54 |   static async getDB(): Promise<IDBDatabase> {
  55 |     if (!this.dbPromise) {
  56 |       this.dbPromise = new Promise((resolve, reject) => {
  57 |         const request = indexedDB.open(DB_NAME, DB_VERSION);
  58 | 
  59 |         request.onerror = () => reject(request.error);
  60 |         request.onsuccess = () => resolve(request.result);
  61 | 
  62 |         request.onupgradeneeded = (event) => {
  63 |           const db = (event.target as IDBOpenDBRequest).result;
  64 | 
  65 |           if (!db.objectStoreNames.contains(STORE_NAME)) {
  66 |             const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' });
  67 |             store.createIndex('indexFileName', 'indexFileName', { unique: false });
  68 |           }
  69 |         };
  70 |       });
  71 |     }
  72 |     return this.dbPromise;
  73 |   }
  74 | 
  75 |   static async saveData(indexFileName: string, data: any): Promise<void> {
  76 |     const db = await this.getDB();
  77 |     const transaction = db.transaction([STORE_NAME], 'readwrite');
  78 |     const store = transaction.objectStore(STORE_NAME);
  79 | 
  80 |     await new Promise<void>((resolve, reject) => {
  81 |       const request = store.put({
  82 |         id: indexFileName,
  83 |         indexFileName,
  84 |         data,
  85 |         timestamp: Date.now(),
  86 |       });
  87 | 
  88 |       request.onsuccess = () => resolve();
  89 |       request.onerror = () => reject(request.error);
  90 |     });
  91 |   }
  92 | 
  93 |   static async loadData(indexFileName: string): Promise<any | null> {
  94 |     const db = await this.getDB();
  95 |     const transaction = db.transaction([STORE_NAME], 'readonly');
  96 |     const store = transaction.objectStore(STORE_NAME);
  97 | 
  98 |     return new Promise<any | null>((resolve, reject) => {
  99 |       const request = store.get(indexFileName);
 100 | 
 101 |       request.onsuccess = () => {
 102 |         const result = request.result;
 103 |         resolve(result ? result.data : null);
 104 |       };
 105 |       request.onerror = () => reject(request.error);
 106 |     });
 107 |   }
 108 | 
 109 |   static async deleteData(indexFileName: string): Promise<void> {
 110 |     const db = await this.getDB();
 111 |     const transaction = db.transaction([STORE_NAME], 'readwrite');
 112 |     const store = transaction.objectStore(STORE_NAME);
 113 | 
 114 |     await new Promise<void>((resolve, reject) => {
 115 |       const request = store.delete(indexFileName);
 116 |       request.onsuccess = () => resolve();
 117 |       request.onerror = () => reject(request.error);
 118 |     });
 119 |   }
 120 | 
 121 |   /**
 122 |    * Clear all IndexedDB data (for complete cleanup during model switching)
 123 |    */
 124 |   static async clearAllData(): Promise<void> {
 125 |     try {
 126 |       const db = await this.getDB();
 127 |       const transaction = db.transaction([STORE_NAME], 'readwrite');
 128 |       const store = transaction.objectStore(STORE_NAME);
 129 | 
 130 |       await new Promise<void>((resolve, reject) => {
 131 |         const request = store.clear();
 132 |         request.onsuccess = () => {
 133 |           console.log('IndexedDBHelper: All data cleared from IndexedDB');
 134 |           resolve();
 135 |         };
 136 |         request.onerror = () => reject(request.error);
 137 |       });
 138 |     } catch (error) {
 139 |       console.error('IndexedDBHelper: Failed to clear all data:', error);
 140 |       throw error;
 141 |     }
 142 |   }
 143 | 
 144 |   /**
 145 |    * Get all stored keys (for debugging)
 146 |    */
 147 |   static async getAllKeys(): Promise<string[]> {
 148 |     try {
 149 |       const db = await this.getDB();
 150 |       const transaction = db.transaction([STORE_NAME], 'readonly');
 151 |       const store = transaction.objectStore(STORE_NAME);
 152 | 
 153 |       return new Promise<string[]>((resolve, reject) => {
 154 |         const request = store.getAllKeys();
 155 |         request.onsuccess = () => resolve(request.result as string[]);
 156 |         request.onerror = () => reject(request.error);
 157 |       });
 158 |     } catch (error) {
 159 |       console.error('IndexedDBHelper: Failed to get all keys:', error);
 160 |       return [];
 161 |     }
 162 |   }
 163 | }
 164 | 
 165 | /**
 166 |  * Global hnswlib-wasm initialization function
 167 |  * Ensures initialization only once across the entire application
 168 |  */
 169 | async function initializeGlobalHnswlib(): Promise<any> {
 170 |   if (globalHnswlibInitialized && globalHnswlib) {
 171 |     return globalHnswlib;
 172 |   }
 173 | 
 174 |   if (globalHnswlibInitPromise) {
 175 |     return globalHnswlibInitPromise;
 176 |   }
 177 | 
 178 |   globalHnswlibInitPromise = (async () => {
 179 |     try {
 180 |       console.log('VectorDatabase: Initializing global hnswlib-wasm instance...');
 181 |       globalHnswlib = await loadHnswlib();
 182 |       globalHnswlibInitialized = true;
 183 |       console.log('VectorDatabase: Global hnswlib-wasm instance initialized successfully');
 184 |       return globalHnswlib;
 185 |     } catch (error) {
 186 |       console.error('VectorDatabase: Failed to initialize global hnswlib-wasm:', error);
 187 |       globalHnswlibInitPromise = null;
 188 |       throw error;
 189 |     }
 190 |   })();
 191 | 
 192 |   return globalHnswlibInitPromise;
 193 | }
 194 | 
 195 | export class VectorDatabase {
 196 |   private index: any = null;
 197 |   private isInitialized = false;
 198 |   private isInitializing = false;
 199 |   private initPromise: Promise<void> | null = null;
 200 | 
 201 |   private documents = new Map<number, VectorDocument>();
 202 |   private tabDocuments = new Map<number, Set<number>>();
 203 |   private nextLabel = 0;
 204 | 
 205 |   private readonly config: VectorDatabaseConfig;
 206 | 
 207 |   constructor(config?: Partial<VectorDatabaseConfig>) {
 208 |     this.config = {
 209 |       dimension: 384,
 210 |       maxElements: 100000,
 211 |       efConstruction: 200,
 212 |       M: 48,
 213 |       efSearch: 50,
 214 |       indexFileName: 'tab_content_index.dat',
 215 |       enableAutoCleanup: true,
 216 |       maxRetentionDays: 30,
 217 |       ...config,
 218 |     };
 219 | 
 220 |     console.log('VectorDatabase: Initialized with config:', {
 221 |       dimension: this.config.dimension,
 222 |       efSearch: this.config.efSearch,
 223 |       M: this.config.M,
 224 |       efConstruction: this.config.efConstruction,
 225 |       enableAutoCleanup: this.config.enableAutoCleanup,
 226 |       maxRetentionDays: this.config.maxRetentionDays,
 227 |     });
 228 |   }
 229 | 
 230 |   /**
 231 |    * Initialize vector database
 232 |    */
 233 |   public async initialize(): Promise<void> {
 234 |     if (this.isInitialized) return;
 235 |     if (this.isInitializing && this.initPromise) return this.initPromise;
 236 | 
 237 |     this.isInitializing = true;
 238 |     this.initPromise = this._doInitialize().finally(() => {
 239 |       this.isInitializing = false;
 240 |     });
 241 | 
 242 |     return this.initPromise;
 243 |   }
 244 | 
 245 |   private async _doInitialize(): Promise<void> {
 246 |     try {
 247 |       console.log('VectorDatabase: Initializing...');
 248 | 
 249 |       const hnswlib = await initializeGlobalHnswlib();
 250 | 
 251 |       hnswlib.EmscriptenFileSystemManager.setDebugLogs(true);
 252 | 
 253 |       this.index = new hnswlib.HierarchicalNSW(
 254 |         'cosine',
 255 |         this.config.dimension,
 256 |         this.config.indexFileName,
 257 |       );
 258 | 
 259 |       await this.syncFileSystem('read');
 260 | 
 261 |       const indexExists = hnswlib.EmscriptenFileSystemManager.checkFileExists(
 262 |         this.config.indexFileName,
 263 |       );
 264 | 
 265 |       if (indexExists) {
 266 |         console.log('VectorDatabase: Loading existing index...');
 267 |         try {
 268 |           await this.index.readIndex(this.config.indexFileName, this.config.maxElements);
 269 |           this.index.setEfSearch(this.config.efSearch);
 270 | 
 271 |           await this.loadDocumentMappings();
 272 | 
 273 |           if (this.documents.size > 0) {
 274 |             const maxLabel = Math.max(...Array.from(this.documents.keys()));
 275 |             this.nextLabel = maxLabel + 1;
 276 |             console.log(
 277 |               `VectorDatabase: Loaded existing index with ${this.documents.size} documents, next label: ${this.nextLabel}`,
 278 |             );
 279 |           } else {
 280 |             const indexCount = this.index.getCurrentCount();
 281 |             if (indexCount > 0) {
 282 |               console.warn(
 283 |                 `VectorDatabase: Index has ${indexCount} vectors but no document mappings found. This may cause label mismatch.`,
 284 |               );
 285 |               this.nextLabel = indexCount;
 286 |             } else {
 287 |               this.nextLabel = 0;
 288 |             }
 289 |             console.log(
 290 |               `VectorDatabase: No document mappings found, starting with next label: ${this.nextLabel}`,
 291 |             );
 292 |           }
 293 |         } catch (loadError) {
 294 |           console.warn(
 295 |             'VectorDatabase: Failed to load existing index, creating new one:',
 296 |             loadError,
 297 |           );
 298 | 
 299 |           this.index.initIndex(
 300 |             this.config.maxElements,
 301 |             this.config.M,
 302 |             this.config.efConstruction,
 303 |             200,
 304 |           );
 305 |           this.index.setEfSearch(this.config.efSearch);
 306 |           this.nextLabel = 0;
 307 |         }
 308 |       } else {
 309 |         console.log('VectorDatabase: Creating new index...');
 310 |         this.index.initIndex(
 311 |           this.config.maxElements,
 312 |           this.config.M,
 313 |           this.config.efConstruction,
 314 |           200,
 315 |         );
 316 |         this.index.setEfSearch(this.config.efSearch);
 317 |         this.nextLabel = 0;
 318 |       }
 319 | 
 320 |       this.isInitialized = true;
 321 |       console.log('VectorDatabase: Initialization completed successfully');
 322 |     } catch (error) {
 323 |       console.error('VectorDatabase: Initialization failed:', error);
 324 |       this.isInitialized = false;
 325 |       throw error;
 326 |     }
 327 |   }
 328 | 
 329 |   /**
 330 |    * Add document to vector database
 331 |    */
 332 |   public async addDocument(
 333 |     tabId: number,
 334 |     url: string,
 335 |     title: string,
 336 |     chunk: TextChunk,
 337 |     embedding: Float32Array,
 338 |   ): Promise<number> {
 339 |     if (!this.isInitialized) {
 340 |       await this.initialize();
 341 |     }
 342 | 
 343 |     const documentId = this.generateDocumentId(tabId, chunk.index);
 344 |     const document: VectorDocument = {
 345 |       id: documentId,
 346 |       tabId,
 347 |       url,
 348 |       title,
 349 |       chunk,
 350 |       embedding,
 351 |       timestamp: Date.now(),
 352 |     };
 353 | 
 354 |     try {
 355 |       // Validate vector data
 356 |       if (!embedding || embedding.length !== this.config.dimension) {
 357 |         const errorMsg = `Invalid embedding dimension: expected ${this.config.dimension}, got ${embedding?.length || 0}`;
 358 |         console.error('VectorDatabase: Dimension mismatch detected!', {
 359 |           expectedDimension: this.config.dimension,
 360 |           actualDimension: embedding?.length || 0,
 361 |           documentId,
 362 |           tabId,
 363 |           url,
 364 |           title: title.substring(0, 50) + '...',
 365 |         });
 366 | 
 367 |         // This might be caused by model switching, suggest reinitialization
 368 |         console.warn(
 369 |           'VectorDatabase: This might be caused by model switching. Consider reinitializing the vector database with the correct dimension.',
 370 |         );
 371 | 
 372 |         throw new Error(errorMsg);
 373 |       }
 374 | 
 375 |       // Check if vector data contains invalid values
 376 |       for (let i = 0; i < embedding.length; i++) {
 377 |         if (!isFinite(embedding[i])) {
 378 |           throw new Error(`Invalid embedding value at index ${i}: ${embedding[i]}`);
 379 |         }
 380 |       }
 381 | 
 382 |       // Ensure we have a clean Float32Array
 383 |       let cleanEmbedding: Float32Array;
 384 |       if (embedding instanceof Float32Array) {
 385 |         cleanEmbedding = embedding;
 386 |       } else {
 387 |         cleanEmbedding = new Float32Array(embedding);
 388 |       }
 389 | 
 390 |       // Use current nextLabel as label
 391 |       const label = this.nextLabel++;
 392 | 
 393 |       console.log(
 394 |         `VectorDatabase: Adding document with label ${label}, embedding dimension: ${embedding.length}`,
 395 |       );
 396 | 
 397 |       // Add vector to index
 398 |       // According to hnswlib-wasm-static emscripten binding requirements, need to create VectorFloat type
 399 |       console.log(`VectorDatabase: 🔧 DEBUGGING - About to call addPoint with:`, {
 400 |         embeddingType: typeof cleanEmbedding,
 401 |         isFloat32Array: cleanEmbedding instanceof Float32Array,
 402 |         length: cleanEmbedding.length,
 403 |         firstFewValues: Array.from(cleanEmbedding.slice(0, 3)),
 404 |         label: label,
 405 |         replaceDeleted: false,
 406 |       });
 407 | 
 408 |       // Method 1: Try using VectorFloat constructor (if available)
 409 |       let vectorToAdd;
 410 |       try {
 411 |         // Check if VectorFloat constructor exists
 412 |         if (globalHnswlib && globalHnswlib.VectorFloat) {
 413 |           console.log('VectorDatabase: Using VectorFloat constructor');
 414 |           vectorToAdd = new globalHnswlib.VectorFloat();
 415 |           // Add elements to VectorFloat one by one
 416 |           for (let i = 0; i < cleanEmbedding.length; i++) {
 417 |             vectorToAdd.push_back(cleanEmbedding[i]);
 418 |           }
 419 |         } else {
 420 |           // Method 2: Use plain JS array (fallback)
 421 |           console.log('VectorDatabase: Using plain JS array as fallback');
 422 |           vectorToAdd = Array.from(cleanEmbedding);
 423 |         }
 424 | 
 425 |         // Call addPoint with constructed vector
 426 |         this.index.addPoint(vectorToAdd, label, false);
 427 | 
 428 |         // Clean up VectorFloat object (if manually created)
 429 |         if (vectorToAdd && typeof vectorToAdd.delete === 'function') {
 430 |           vectorToAdd.delete();
 431 |         }
 432 |       } catch (vectorError) {
 433 |         console.error(
 434 |           'VectorDatabase: VectorFloat approach failed, trying alternatives:',
 435 |           vectorError,
 436 |         );
 437 | 
 438 |         // Method 3: Try passing Float32Array directly
 439 |         try {
 440 |           console.log('VectorDatabase: Trying Float32Array directly');
 441 |           this.index.addPoint(cleanEmbedding, label, false);
 442 |         } catch (float32Error) {
 443 |           console.error('VectorDatabase: Float32Array approach failed:', float32Error);
 444 | 
 445 |           // Method 4: Last resort - use spread operator
 446 |           console.log('VectorDatabase: Trying spread operator as last resort');
 447 |           this.index.addPoint([...cleanEmbedding], label, false);
 448 |         }
 449 |       }
 450 |       console.log(`VectorDatabase: ✅ Successfully added document with label ${label}`);
 451 | 
 452 |       // Store document mapping
 453 |       this.documents.set(label, document);
 454 | 
 455 |       // Update tab document mapping
 456 |       if (!this.tabDocuments.has(tabId)) {
 457 |         this.tabDocuments.set(tabId, new Set());
 458 |       }
 459 |       this.tabDocuments.get(tabId)!.add(label);
 460 | 
 461 |       // Save index and mappings
 462 |       await this.saveIndex();
 463 |       await this.saveDocumentMappings();
 464 | 
 465 |       // Check if auto cleanup is needed
 466 |       if (this.config.enableAutoCleanup) {
 467 |         await this.checkAndPerformAutoCleanup();
 468 |       }
 469 | 
 470 |       console.log(`VectorDatabase: Successfully added document ${documentId} with label ${label}`);
 471 |       return label;
 472 |     } catch (error) {
 473 |       console.error('VectorDatabase: Failed to add document:', error);
 474 |       console.error('VectorDatabase: Embedding info:', {
 475 |         type: typeof embedding,
 476 |         constructor: embedding?.constructor?.name,
 477 |         length: embedding?.length,
 478 |         isFloat32Array: embedding instanceof Float32Array,
 479 |         firstFewValues: embedding ? Array.from(embedding.slice(0, 5)) : null,
 480 |       });
 481 |       throw error;
 482 |     }
 483 |   }
 484 | 
 485 |   /**
 486 |    * Search similar documents
 487 |    */
 488 |   public async search(queryEmbedding: Float32Array, topK: number = 10): Promise<SearchResult[]> {
 489 |     if (!this.isInitialized) {
 490 |       await this.initialize();
 491 |     }
 492 | 
 493 |     try {
 494 |       // Validate query vector
 495 |       if (!queryEmbedding || queryEmbedding.length !== this.config.dimension) {
 496 |         throw new Error(
 497 |           `Invalid query embedding dimension: expected ${this.config.dimension}, got ${queryEmbedding?.length || 0}`,
 498 |         );
 499 |       }
 500 | 
 501 |       // Check if query vector contains invalid values
 502 |       for (let i = 0; i < queryEmbedding.length; i++) {
 503 |         if (!isFinite(queryEmbedding[i])) {
 504 |           throw new Error(`Invalid query embedding value at index ${i}: ${queryEmbedding[i]}`);
 505 |         }
 506 |       }
 507 | 
 508 |       console.log(
 509 |         `VectorDatabase: Searching with query embedding dimension: ${queryEmbedding.length}, topK: ${topK}`,
 510 |       );
 511 | 
 512 |       // Check if index is empty
 513 |       const currentCount = this.index.getCurrentCount();
 514 |       if (currentCount === 0) {
 515 |         console.log('VectorDatabase: Index is empty, returning no results');
 516 |         return [];
 517 |       }
 518 | 
 519 |       console.log(`VectorDatabase: Index contains ${currentCount} vectors`);
 520 | 
 521 |       // Check if document mapping and index are synchronized
 522 |       const mappingCount = this.documents.size;
 523 |       if (mappingCount === 0 && currentCount > 0) {
 524 |         console.warn(
 525 |           `VectorDatabase: Index has ${currentCount} vectors but document mapping is empty. Attempting to reload mappings...`,
 526 |         );
 527 |         await this.loadDocumentMappings();
 528 | 
 529 |         if (this.documents.size === 0) {
 530 |           console.error(
 531 |             'VectorDatabase: Failed to load document mappings. Index and mappings are out of sync.',
 532 |           );
 533 |           return [];
 534 |         }
 535 |         console.log(
 536 |           `VectorDatabase: Successfully reloaded ${this.documents.size} document mappings`,
 537 |         );
 538 |       }
 539 | 
 540 |       // Process query vector according to hnswlib-wasm-static emscripten binding requirements
 541 |       let queryVector;
 542 |       let searchResult;
 543 | 
 544 |       try {
 545 |         // Method 1: Try using VectorFloat constructor (if available)
 546 |         if (globalHnswlib && globalHnswlib.VectorFloat) {
 547 |           console.log('VectorDatabase: Using VectorFloat for search query');
 548 |           queryVector = new globalHnswlib.VectorFloat();
 549 |           // Add elements to VectorFloat one by one
 550 |           for (let i = 0; i < queryEmbedding.length; i++) {
 551 |             queryVector.push_back(queryEmbedding[i]);
 552 |           }
 553 |           searchResult = this.index.searchKnn(queryVector, topK, undefined);
 554 | 
 555 |           // Clean up VectorFloat object
 556 |           if (queryVector && typeof queryVector.delete === 'function') {
 557 |             queryVector.delete();
 558 |           }
 559 |         } else {
 560 |           // Method 2: Use plain JS array (fallback)
 561 |           console.log('VectorDatabase: Using plain JS array for search query');
 562 |           const queryArray = Array.from(queryEmbedding);
 563 |           searchResult = this.index.searchKnn(queryArray, topK, undefined);
 564 |         }
 565 |       } catch (vectorError) {
 566 |         console.error(
 567 |           'VectorDatabase: VectorFloat search failed, trying alternatives:',
 568 |           vectorError,
 569 |         );
 570 | 
 571 |         // Method 3: Try passing Float32Array directly
 572 |         try {
 573 |           console.log('VectorDatabase: Trying Float32Array directly for search');
 574 |           searchResult = this.index.searchKnn(queryEmbedding, topK, undefined);
 575 |         } catch (float32Error) {
 576 |           console.error('VectorDatabase: Float32Array search failed:', float32Error);
 577 | 
 578 |           // Method 4: Last resort - use spread operator
 579 |           console.log('VectorDatabase: Trying spread operator for search as last resort');
 580 |           searchResult = this.index.searchKnn([...queryEmbedding], topK, undefined);
 581 |         }
 582 |       }
 583 | 
 584 |       const results: SearchResult[] = [];
 585 | 
 586 |       console.log(`VectorDatabase: Processing ${searchResult.neighbors.length} search neighbors`);
 587 |       console.log(`VectorDatabase: Available documents in mapping: ${this.documents.size}`);
 588 |       console.log(`VectorDatabase: Index current count: ${this.index.getCurrentCount()}`);
 589 | 
 590 |       for (let i = 0; i < searchResult.neighbors.length; i++) {
 591 |         const label = searchResult.neighbors[i];
 592 |         const distance = searchResult.distances[i];
 593 |         const similarity = 1 - distance; // Convert cosine distance to similarity
 594 | 
 595 |         console.log(
 596 |           `VectorDatabase: Processing neighbor ${i}: label=${label}, distance=${distance}, similarity=${similarity}`,
 597 |         );
 598 | 
 599 |         // Find corresponding document by label
 600 |         const document = this.findDocumentByLabel(label);
 601 |         if (document) {
 602 |           console.log(`VectorDatabase: Found document for label ${label}: ${document.id}`);
 603 |           results.push({
 604 |             document,
 605 |             similarity,
 606 |             distance,
 607 |           });
 608 |         } else {
 609 |           console.warn(`VectorDatabase: No document found for label ${label}`);
 610 | 
 611 |           // Detailed debug information
 612 |           if (i < 5) {
 613 |             // Only show detailed info for first 5 neighbors to avoid log spam
 614 |             console.warn(
 615 |               `VectorDatabase: Available labels (first 20): ${Array.from(this.documents.keys()).slice(0, 20).join(', ')}`,
 616 |             );
 617 |             console.warn(`VectorDatabase: Total available labels: ${this.documents.size}`);
 618 |             console.warn(
 619 |               `VectorDatabase: Label type: ${typeof label}, Available label types: ${Array.from(
 620 |                 this.documents.keys(),
 621 |               )
 622 |                 .slice(0, 3)
 623 |                 .map((k) => typeof k)
 624 |                 .join(', ')}`,
 625 |             );
 626 |           }
 627 |         }
 628 |       }
 629 | 
 630 |       console.log(
 631 |         `VectorDatabase: Found ${results.length} search results out of ${searchResult.neighbors.length} neighbors`,
 632 |       );
 633 | 
 634 |       // If no results found but index has data, indicates label mismatch
 635 |       if (results.length === 0 && searchResult.neighbors.length > 0) {
 636 |         console.error(
 637 |           'VectorDatabase: Label mismatch detected! Index has vectors but no matching documents found.',
 638 |         );
 639 |         console.error(
 640 |           'VectorDatabase: This usually indicates the index and document mappings are out of sync.',
 641 |         );
 642 |         console.error('VectorDatabase: Consider rebuilding the index to fix this issue.');
 643 | 
 644 |         // Provide some diagnostic information
 645 |         const sampleLabels = searchResult.neighbors.slice(0, 5);
 646 |         const availableLabels = Array.from(this.documents.keys()).slice(0, 5);
 647 |         console.error('VectorDatabase: Sample search labels:', sampleLabels);
 648 |         console.error('VectorDatabase: Sample available labels:', availableLabels);
 649 |       }
 650 | 
 651 |       return results.sort((a, b) => b.similarity - a.similarity);
 652 |     } catch (error) {
 653 |       console.error('VectorDatabase: Search failed:', error);
 654 |       console.error('VectorDatabase: Query embedding info:', {
 655 |         type: typeof queryEmbedding,
 656 |         constructor: queryEmbedding?.constructor?.name,
 657 |         length: queryEmbedding?.length,
 658 |         isFloat32Array: queryEmbedding instanceof Float32Array,
 659 |         firstFewValues: queryEmbedding ? Array.from(queryEmbedding.slice(0, 5)) : null,
 660 |       });
 661 |       throw error;
 662 |     }
 663 |   }
 664 | 
 665 |   /**
 666 |    * Remove all documents for a tab
 667 |    */
 668 |   public async removeTabDocuments(tabId: number): Promise<void> {
 669 |     if (!this.isInitialized) {
 670 |       await this.initialize();
 671 |     }
 672 | 
 673 |     const documentLabels = this.tabDocuments.get(tabId);
 674 |     if (!documentLabels) {
 675 |       return;
 676 |     }
 677 | 
 678 |     try {
 679 |       // Remove documents from mapping (hnswlib-wasm doesn't support direct deletion, only mark as deleted)
 680 |       for (const label of documentLabels) {
 681 |         this.documents.delete(label);
 682 |       }
 683 | 
 684 |       // Clean up tab mapping
 685 |       this.tabDocuments.delete(tabId);
 686 | 
 687 |       // Save changes
 688 |       await this.saveDocumentMappings();
 689 | 
 690 |       console.log(`VectorDatabase: Removed ${documentLabels.size} documents for tab ${tabId}`);
 691 |     } catch (error) {
 692 |       console.error('VectorDatabase: Failed to remove tab documents:', error);
 693 |       throw error;
 694 |     }
 695 |   }
 696 | 
 697 |   /**
 698 |    * Get database statistics
 699 |    */
 700 |   public getStats(): {
 701 |     totalDocuments: number;
 702 |     totalTabs: number;
 703 |     indexSize: number;
 704 |     isInitialized: boolean;
 705 |   } {
 706 |     return {
 707 |       totalDocuments: this.documents.size,
 708 |       totalTabs: this.tabDocuments.size,
 709 |       indexSize: this.calculateStorageSize(),
 710 |       isInitialized: this.isInitialized,
 711 |     };
 712 |   }
 713 | 
 714 |   /**
 715 |    * Calculate actual storage size (bytes)
 716 |    */
 717 |   private calculateStorageSize(): number {
 718 |     let totalSize = 0;
 719 | 
 720 |     try {
 721 |       // 1. 计算文档映射的大小
 722 |       const documentsSize = this.calculateDocumentMappingsSize();
 723 |       totalSize += documentsSize;
 724 | 
 725 |       // 2. 计算向量数据的大小
 726 |       const vectorsSize = this.calculateVectorsSize();
 727 |       totalSize += vectorsSize;
 728 | 
 729 |       // 3. 估算索引结构的大小
 730 |       const indexStructureSize = this.calculateIndexStructureSize();
 731 |       totalSize += indexStructureSize;
 732 | 
 733 |       console.log(
 734 |         `VectorDatabase: Storage size breakdown - Documents: ${documentsSize}, Vectors: ${vectorsSize}, Index: ${indexStructureSize}, Total: ${totalSize} bytes`,
 735 |       );
 736 |     } catch (error) {
 737 |       console.warn('VectorDatabase: Failed to calculate storage size:', error);
 738 |       // 返回一个基于文档数量的估算值
 739 |       totalSize = this.documents.size * 1024; // 每个文档估算1KB
 740 |     }
 741 | 
 742 |     return totalSize;
 743 |   }
 744 | 
 745 |   /**
 746 |    * Calculate document mappings size
 747 |    */
 748 |   private calculateDocumentMappingsSize(): number {
 749 |     let size = 0;
 750 | 
 751 |     // Calculate documents Map size
 752 |     for (const [label, document] of this.documents.entries()) {
 753 |       // label (number): 8 bytes
 754 |       size += 8;
 755 | 
 756 |       // document object
 757 |       size += this.calculateObjectSize(document);
 758 |     }
 759 | 
 760 |     // Calculate tabDocuments Map size
 761 |     for (const [tabId, labels] of this.tabDocuments.entries()) {
 762 |       // tabId (number): 8 bytes
 763 |       size += 8;
 764 | 
 765 |       // Set of labels: 8 bytes per label + Set overhead
 766 |       size += labels.size * 8 + 32; // 32 bytes Set overhead
 767 |     }
 768 | 
 769 |     return size;
 770 |   }
 771 | 
 772 |   /**
 773 |    * Calculate vectors data size
 774 |    */
 775 |   private calculateVectorsSize(): number {
 776 |     const documentCount = this.documents.size;
 777 |     const dimension = this.config.dimension;
 778 | 
 779 |     // Each vector: dimension * 4 bytes (Float32)
 780 |     const vectorSize = dimension * 4;
 781 | 
 782 |     return documentCount * vectorSize;
 783 |   }
 784 | 
 785 |   /**
 786 |    * Estimate index structure size
 787 |    */
 788 |   private calculateIndexStructureSize(): number {
 789 |     const documentCount = this.documents.size;
 790 | 
 791 |     if (documentCount === 0) return 0;
 792 | 
 793 |     // HNSW index size estimation
 794 |     // Based on papers and actual testing, HNSW index size is about 20-40% of vector data
 795 |     const vectorsSize = this.calculateVectorsSize();
 796 |     const indexOverhead = Math.floor(vectorsSize * 0.3); // 30% overhead
 797 | 
 798 |     // Additional graph structure overhead
 799 |     const graphOverhead = documentCount * 64; // About 64 bytes graph structure overhead per node
 800 | 
 801 |     return indexOverhead + graphOverhead;
 802 |   }
 803 | 
 804 |   /**
 805 |    * Calculate object size (rough estimation)
 806 |    */
 807 |   private calculateObjectSize(obj: any): number {
 808 |     let size = 0;
 809 | 
 810 |     try {
 811 |       const jsonString = JSON.stringify(obj);
 812 |       // UTF-8 encoding, most characters 1 byte, Chinese etc 3 bytes, average 2 bytes
 813 |       size = jsonString.length * 2;
 814 |     } catch (error) {
 815 |       // If JSON serialization fails, use default estimation
 816 |       size = 512; // Default 512 bytes
 817 |     }
 818 | 
 819 |     return size;
 820 |   }
 821 | 
 822 |   /**
 823 |    * Clear entire database
 824 |    */
 825 |   public async clear(): Promise<void> {
 826 |     console.log('VectorDatabase: Starting complete database clear...');
 827 | 
 828 |     try {
 829 |       // Clear in-memory data structures
 830 |       this.documents.clear();
 831 |       this.tabDocuments.clear();
 832 |       this.nextLabel = 0;
 833 | 
 834 |       // Clear HNSW index file (in hnswlib-index database)
 835 |       if (this.isInitialized && this.index) {
 836 |         try {
 837 |           console.log('VectorDatabase: Clearing HNSW index file from IndexedDB...');
 838 | 
 839 |           // 1. First try to physically delete index file (using EmscriptenFileSystemManager)
 840 |           try {
 841 |             if (
 842 |               globalHnswlib &&
 843 |               globalHnswlib.EmscriptenFileSystemManager.checkFileExists(this.config.indexFileName)
 844 |             ) {
 845 |               console.log(
 846 |                 `VectorDatabase: Deleting physical index file: ${this.config.indexFileName}`,
 847 |               );
 848 |               globalHnswlib.EmscriptenFileSystemManager.deleteFile(this.config.indexFileName);
 849 |               await this.syncFileSystem('write'); // Ensure deletion is synced to persistent storage
 850 |               console.log(
 851 |                 `VectorDatabase: Physical index file ${this.config.indexFileName} deleted successfully`,
 852 |               );
 853 |             } else {
 854 |               console.log(
 855 |                 `VectorDatabase: Physical index file ${this.config.indexFileName} does not exist or already deleted`,
 856 |               );
 857 |             }
 858 |           } catch (fileError) {
 859 |             console.warn(
 860 |               `VectorDatabase: Failed to delete physical index file ${this.config.indexFileName}:`,
 861 |               fileError,
 862 |             );
 863 |             // Continue with other cleanup operations, don't block the process
 864 |           }
 865 | 
 866 |           // 2. Delete index file from IndexedDB
 867 |           await this.index.deleteIndex(this.config.indexFileName);
 868 |           console.log('VectorDatabase: HNSW index file cleared from IndexedDB');
 869 | 
 870 |           // 3. Reinitialize empty index
 871 |           console.log('VectorDatabase: Reinitializing empty HNSW index...');
 872 |           this.index.initIndex(
 873 |             this.config.maxElements,
 874 |             this.config.M,
 875 |             this.config.efConstruction,
 876 |             200,
 877 |           );
 878 |           this.index.setEfSearch(this.config.efSearch);
 879 | 
 880 |           // 4. Force save empty index
 881 |           await this.forceSaveIndex();
 882 |         } catch (indexError) {
 883 |           console.warn('VectorDatabase: Failed to clear HNSW index file:', indexError);
 884 |           // Continue with other cleanup operations
 885 |         }
 886 |       }
 887 | 
 888 |       // Clear document mappings from IndexedDB (in VectorDatabaseStorage database)
 889 |       try {
 890 |         console.log('VectorDatabase: Clearing document mappings from IndexedDB...');
 891 |         await IndexedDBHelper.deleteData(this.config.indexFileName);
 892 |         console.log('VectorDatabase: Document mappings cleared from IndexedDB');
 893 |       } catch (idbError) {
 894 |         console.warn(
 895 |           'VectorDatabase: Failed to clear document mappings from IndexedDB, trying chrome.storage fallback:',
 896 |           idbError,
 897 |         );
 898 | 
 899 |         // Clear backup data from chrome.storage
 900 |         try {
 901 |           const storageKey = `hnswlib_document_mappings_${this.config.indexFileName}`;
 902 |           await chrome.storage.local.remove([storageKey]);
 903 |           console.log('VectorDatabase: Chrome storage fallback cleared');
 904 |         } catch (storageError) {
 905 |           console.warn('VectorDatabase: Failed to clear chrome.storage fallback:', storageError);
 906 |         }
 907 |       }
 908 | 
 909 |       // Save empty document mappings to ensure consistency
 910 |       await this.saveDocumentMappings();
 911 | 
 912 |       console.log('VectorDatabase: Complete database clear finished successfully');
 913 |     } catch (error) {
 914 |       console.error('VectorDatabase: Failed to clear database:', error);
 915 |       throw error;
 916 |     }
 917 |   }
 918 | 
 919 |   /**
 920 |    * Force save index and sync filesystem
 921 |    */
 922 |   private async forceSaveIndex(): Promise<void> {
 923 |     try {
 924 |       await this.index.writeIndex(this.config.indexFileName);
 925 |       await this.syncFileSystem('write'); // Force sync
 926 |     } catch (error) {
 927 |       console.error('VectorDatabase: Failed to force save index:', error);
 928 |     }
 929 |   }
 930 | 
 931 |   /**
 932 |    * Check and perform auto cleanup
 933 |    */
 934 |   private async checkAndPerformAutoCleanup(): Promise<void> {
 935 |     try {
 936 |       const currentCount = this.documents.size;
 937 |       const maxElements = this.config.maxElements;
 938 | 
 939 |       console.log(
 940 |         `VectorDatabase: Auto cleanup check - current: ${currentCount}, max: ${maxElements}`,
 941 |       );
 942 | 
 943 |       // Check if maximum element count is exceeded
 944 |       if (currentCount >= maxElements) {
 945 |         console.log('VectorDatabase: Document count reached limit, performing cleanup...');
 946 |         await this.performLRUCleanup(Math.floor(maxElements * 0.2)); // Clean up 20% of data
 947 |       }
 948 | 
 949 |       // Check if there's expired data
 950 |       if (this.config.maxRetentionDays && this.config.maxRetentionDays > 0) {
 951 |         await this.performTimeBasedCleanup();
 952 |       }
 953 |     } catch (error) {
 954 |       console.error('VectorDatabase: Auto cleanup failed:', error);
 955 |     }
 956 |   }
 957 | 
 958 |   /**
 959 |    * Perform LRU-based cleanup (delete oldest documents)
 960 |    */
 961 |   private async performLRUCleanup(cleanupCount: number): Promise<void> {
 962 |     try {
 963 |       console.log(
 964 |         `VectorDatabase: Starting LRU cleanup, removing ${cleanupCount} oldest documents`,
 965 |       );
 966 | 
 967 |       // Get all documents and sort by timestamp
 968 |       const allDocuments = Array.from(this.documents.entries());
 969 |       allDocuments.sort((a, b) => a[1].timestamp - b[1].timestamp);
 970 | 
 971 |       // Select documents to delete
 972 |       const documentsToDelete = allDocuments.slice(0, cleanupCount);
 973 | 
 974 |       for (const [label, _document] of documentsToDelete) {
 975 |         await this.removeDocumentByLabel(label);
 976 |       }
 977 | 
 978 |       // Save updated index and mappings
 979 |       await this.saveIndex();
 980 |       await this.saveDocumentMappings();
 981 | 
 982 |       console.log(
 983 |         `VectorDatabase: LRU cleanup completed, removed ${documentsToDelete.length} documents`,
 984 |       );
 985 |     } catch (error) {
 986 |       console.error('VectorDatabase: LRU cleanup failed:', error);
 987 |     }
 988 |   }
 989 | 
 990 |   /**
 991 |    * Perform time-based cleanup (delete expired documents)
 992 |    */
 993 |   private async performTimeBasedCleanup(): Promise<void> {
 994 |     try {
 995 |       const maxRetentionMs = this.config.maxRetentionDays! * 24 * 60 * 60 * 1000;
 996 |       const cutoffTime = Date.now() - maxRetentionMs;
 997 | 
 998 |       console.log(
 999 |         `VectorDatabase: Starting time-based cleanup, removing documents older than ${this.config.maxRetentionDays} days`,
1000 |       );
1001 | 
1002 |       const documentsToDelete: number[] = [];
1003 | 
1004 |       for (const [label, document] of this.documents.entries()) {
1005 |         if (document.timestamp < cutoffTime) {
1006 |           documentsToDelete.push(label);
1007 |         }
1008 |       }
1009 | 
1010 |       for (const label of documentsToDelete) {
1011 |         await this.removeDocumentByLabel(label);
1012 |       }
1013 | 
1014 |       // Save updated index and mappings
1015 |       if (documentsToDelete.length > 0) {
1016 |         await this.saveIndex();
1017 |         await this.saveDocumentMappings();
1018 |       }
1019 | 
1020 |       console.log(
1021 |         `VectorDatabase: Time-based cleanup completed, removed ${documentsToDelete.length} expired documents`,
1022 |       );
1023 |     } catch (error) {
1024 |       console.error('VectorDatabase: Time-based cleanup failed:', error);
1025 |     }
1026 |   }
1027 | 
1028 |   /**
1029 |    * Remove single document by label
1030 |    */
1031 |   private async removeDocumentByLabel(label: number): Promise<void> {
1032 |     try {
1033 |       const document = this.documents.get(label);
1034 |       if (!document) {
1035 |         console.warn(`VectorDatabase: Document with label ${label} not found`);
1036 |         return;
1037 |       }
1038 | 
1039 |       // Remove vector from HNSW index
1040 |       if (this.index) {
1041 |         try {
1042 |           this.index.markDelete(label);
1043 |         } catch (indexError) {
1044 |           console.warn(
1045 |             `VectorDatabase: Failed to mark delete in index for label ${label}:`,
1046 |             indexError,
1047 |           );
1048 |         }
1049 |       }
1050 | 
1051 |       // Remove from memory mapping
1052 |       this.documents.delete(label);
1053 | 
1054 |       // Remove from tab mapping
1055 |       const tabId = document.tabId;
1056 |       if (this.tabDocuments.has(tabId)) {
1057 |         this.tabDocuments.get(tabId)!.delete(label);
1058 |         // If tab has no other documents, delete entire tab mapping
1059 |         if (this.tabDocuments.get(tabId)!.size === 0) {
1060 |           this.tabDocuments.delete(tabId);
1061 |         }
1062 |       }
1063 | 
1064 |       console.log(`VectorDatabase: Removed document with label ${label} from tab ${tabId}`);
1065 |     } catch (error) {
1066 |       console.error(`VectorDatabase: Failed to remove document with label ${label}:`, error);
1067 |     }
1068 |   }
1069 | 
1070 |   // 私有辅助方法
1071 | 
1072 |   private generateDocumentId(tabId: number, chunkIndex: number): string {
1073 |     return `tab_${tabId}_chunk_${chunkIndex}_${Date.now()}`;
1074 |   }
1075 | 
1076 |   private findDocumentByLabel(label: number): VectorDocument | null {
1077 |     return this.documents.get(label) || null;
1078 |   }
1079 | 
1080 |   private async syncFileSystem(direction: 'read' | 'write'): Promise<void> {
1081 |     try {
1082 |       if (!globalHnswlib) {
1083 |         return;
1084 |       }
1085 | 
1086 |       // If sync operation is already in progress, wait for it to complete
1087 |       if (syncInProgress && pendingSyncPromise) {
1088 |         console.log(`VectorDatabase: Sync already in progress, waiting...`);
1089 |         await pendingSyncPromise;
1090 |         return;
1091 |       }
1092 | 
1093 |       // Mark sync start
1094 |       syncInProgress = true;
1095 | 
1096 |       // Create sync Promise with timeout mechanism
1097 |       pendingSyncPromise = new Promise<void>((resolve, reject) => {
1098 |         const timeout = setTimeout(() => {
1099 |           console.warn(`VectorDatabase: Filesystem sync (${direction}) timeout`);
1100 |           syncInProgress = false;
1101 |           pendingSyncPromise = null;
1102 |           reject(new Error('Sync timeout'));
1103 |         }, 5000); // 5 second timeout
1104 | 
1105 |         try {
1106 |           globalHnswlib.EmscriptenFileSystemManager.syncFS(direction === 'read', () => {
1107 |             clearTimeout(timeout);
1108 |             console.log(`VectorDatabase: Filesystem sync (${direction}) completed`);
1109 |             syncInProgress = false;
1110 |             pendingSyncPromise = null;
1111 |             resolve();
1112 |           });
1113 |         } catch (error) {
1114 |           clearTimeout(timeout);
1115 |           console.warn(`VectorDatabase: Failed to sync filesystem (${direction}):`, error);
1116 |           syncInProgress = false;
1117 |           pendingSyncPromise = null;
1118 |           reject(error);
1119 |         }
1120 |       });
1121 | 
1122 |       await pendingSyncPromise;
1123 |     } catch (error) {
1124 |       console.warn(`VectorDatabase: Failed to sync filesystem (${direction}):`, error);
1125 |       syncInProgress = false;
1126 |       pendingSyncPromise = null;
1127 |     }
1128 |   }
1129 | 
1130 |   private async saveIndex(): Promise<void> {
1131 |     try {
1132 |       await this.index.writeIndex(this.config.indexFileName);
1133 |       // Reduce sync frequency, only sync when necessary
1134 |       if (this.documents.size % 10 === 0) {
1135 |         // Sync every 10 documents
1136 |         await this.syncFileSystem('write');
1137 |       }
1138 |     } catch (error) {
1139 |       console.error('VectorDatabase: Failed to save index:', error);
1140 |     }
1141 |   }
1142 | 
1143 |   private async saveDocumentMappings(): Promise<void> {
1144 |     try {
1145 |       // Save document mappings to IndexedDB
1146 |       const mappingData = {
1147 |         documents: Array.from(this.documents.entries()),
1148 |         tabDocuments: Array.from(this.tabDocuments.entries()).map(([tabId, labels]) => [
1149 |           tabId,
1150 |           Array.from(labels),
1151 |         ]),
1152 |         nextLabel: this.nextLabel,
1153 |       };
1154 | 
1155 |       try {
1156 |         // Use IndexedDB to save data, supports larger storage capacity
1157 |         await IndexedDBHelper.saveData(this.config.indexFileName, mappingData);
1158 |         console.log('VectorDatabase: Document mappings saved to IndexedDB');
1159 |       } catch (idbError) {
1160 |         console.warn(
1161 |           'VectorDatabase: Failed to save to IndexedDB, falling back to chrome.storage:',
1162 |           idbError,
1163 |         );
1164 | 
1165 |         // Fall back to chrome.storage.local
1166 |         try {
1167 |           const storageKey = `hnswlib_document_mappings_${this.config.indexFileName}`;
1168 |           await chrome.storage.local.set({ [storageKey]: mappingData });
1169 |           console.log('VectorDatabase: Document mappings saved to chrome.storage.local (fallback)');
1170 |         } catch (storageError) {
1171 |           console.error(
1172 |             'VectorDatabase: Failed to save to both IndexedDB and chrome.storage:',
1173 |             storageError,
1174 |           );
1175 |         }
1176 |       }
1177 |     } catch (error) {
1178 |       console.error('VectorDatabase: Failed to save document mappings:', error);
1179 |     }
1180 |   }
1181 | 
1182 |   public async loadDocumentMappings(): Promise<void> {
1183 |     try {
1184 |       // Load document mappings from IndexedDB
1185 |       if (!globalHnswlib) {
1186 |         return;
1187 |       }
1188 | 
1189 |       let mappingData = null;
1190 | 
1191 |       try {
1192 |         // First try to read from IndexedDB
1193 |         mappingData = await IndexedDBHelper.loadData(this.config.indexFileName);
1194 |         if (mappingData) {
1195 |           console.log(`VectorDatabase: Loaded document mappings from IndexedDB`);
1196 |         }
1197 |       } catch (idbError) {
1198 |         console.warn(
1199 |           'VectorDatabase: Failed to read from IndexedDB, trying chrome.storage:',
1200 |           idbError,
1201 |         );
1202 |       }
1203 | 
1204 |       // If IndexedDB has no data, try reading from chrome.storage.local (backward compatibility)
1205 |       if (!mappingData) {
1206 |         try {
1207 |           const storageKey = `hnswlib_document_mappings_${this.config.indexFileName}`;
1208 |           const result = await chrome.storage.local.get([storageKey]);
1209 |           mappingData = result[storageKey];
1210 |           if (mappingData) {
1211 |             console.log(
1212 |               `VectorDatabase: Loaded document mappings from chrome.storage.local (fallback)`,
1213 |             );
1214 | 
1215 |             // Migrate to IndexedDB
1216 |             try {
1217 |               await IndexedDBHelper.saveData(this.config.indexFileName, mappingData);
1218 |               console.log('VectorDatabase: Migrated data from chrome.storage to IndexedDB');
1219 |             } catch (migrationError) {
1220 |               console.warn('VectorDatabase: Failed to migrate data to IndexedDB:', migrationError);
1221 |             }
1222 |           }
1223 |         } catch (storageError) {
1224 |           console.warn('VectorDatabase: Failed to read from chrome.storage.local:', storageError);
1225 |         }
1226 |       }
1227 | 
1228 |       if (mappingData) {
1229 |         // Restore document mappings
1230 |         this.documents.clear();
1231 |         for (const [label, doc] of mappingData.documents) {
1232 |           this.documents.set(label, doc);
1233 |         }
1234 | 
1235 |         // Restore tab mappings
1236 |         this.tabDocuments.clear();
1237 |         for (const [tabId, labels] of mappingData.tabDocuments) {
1238 |           this.tabDocuments.set(tabId, new Set(labels));
1239 |         }
1240 | 
1241 |         // Restore nextLabel - use saved value or calculate max label + 1
1242 |         if (mappingData.nextLabel !== undefined) {
1243 |           this.nextLabel = mappingData.nextLabel;
1244 |         } else if (this.documents.size > 0) {
1245 |           // If no saved nextLabel, calculate max label + 1
1246 |           const maxLabel = Math.max(...Array.from(this.documents.keys()));
1247 |           this.nextLabel = maxLabel + 1;
1248 |         } else {
1249 |           this.nextLabel = 0;
1250 |         }
1251 | 
1252 |         console.log(
1253 |           `VectorDatabase: Loaded ${this.documents.size} document mappings, next label: ${this.nextLabel}`,
1254 |         );
1255 |       } else {
1256 |         console.log('VectorDatabase: No existing document mappings found');
1257 |       }
1258 |     } catch (error) {
1259 |       console.error('VectorDatabase: Failed to load document mappings:', error);
1260 |     }
1261 |   }
1262 | }
1263 | 
1264 | // Global VectorDatabase singleton
1265 | let globalVectorDatabase: VectorDatabase | null = null;
1266 | let currentDimension: number | null = null;
1267 | 
1268 | /**
1269 |  * Get global VectorDatabase singleton instance
1270 |  * If dimension changes, will recreate instance to ensure compatibility
1271 |  */
1272 | export async function getGlobalVectorDatabase(
1273 |   config?: Partial<VectorDatabaseConfig>,
1274 | ): Promise<VectorDatabase> {
1275 |   const newDimension = config?.dimension || 384;
1276 | 
1277 |   // If dimension changes, need to recreate vector database
1278 |   if (globalVectorDatabase && currentDimension !== null && currentDimension !== newDimension) {
1279 |     console.log(
1280 |       `VectorDatabase: Dimension changed from ${currentDimension} to ${newDimension}, recreating instance`,
1281 |     );
1282 | 
1283 |     // Clean up old instance - this will clean up index files and document mappings
1284 |     try {
1285 |       await globalVectorDatabase.clear();
1286 |       console.log('VectorDatabase: Successfully cleared old instance for dimension change');
1287 |     } catch (error) {
1288 |       console.warn('VectorDatabase: Error during cleanup:', error);
1289 |     }
1290 | 
1291 |     globalVectorDatabase = null;
1292 |     currentDimension = null;
1293 |   }
1294 | 
1295 |   if (!globalVectorDatabase) {
1296 |     globalVectorDatabase = new VectorDatabase(config);
1297 |     currentDimension = newDimension;
1298 |     console.log(
1299 |       `VectorDatabase: Created global singleton instance with dimension ${currentDimension}`,
1300 |     );
1301 |   }
1302 | 
1303 |   return globalVectorDatabase;
1304 | }
1305 | 
1306 | /**
1307 |  * Synchronous version of getting global VectorDatabase instance (for backward compatibility)
1308 |  * Note: If dimension change is needed, recommend using async version
1309 |  */
1310 | export function getGlobalVectorDatabaseSync(
1311 |   config?: Partial<VectorDatabaseConfig>,
1312 | ): VectorDatabase {
1313 |   const newDimension = config?.dimension || 384;
1314 | 
1315 |   // If dimension changes, log warning but don't clean up (avoid race conditions)
1316 |   if (globalVectorDatabase && currentDimension !== null && currentDimension !== newDimension) {
1317 |     console.warn(
1318 |       `VectorDatabase: Dimension mismatch detected (${currentDimension} vs ${newDimension}). Consider using async version for proper cleanup.`,
1319 |     );
1320 |   }
1321 | 
1322 |   if (!globalVectorDatabase) {
1323 |     globalVectorDatabase = new VectorDatabase(config);
1324 |     currentDimension = newDimension;
1325 |     console.log(
1326 |       `VectorDatabase: Created global singleton instance with dimension ${currentDimension}`,
1327 |     );
1328 |   }
1329 | 
1330 |   return globalVectorDatabase;
1331 | }
1332 | 
1333 | /**
1334 |  * Reset global VectorDatabase instance (mainly for testing or model switching)
1335 |  */
1336 | export async function resetGlobalVectorDatabase(): Promise<void> {
1337 |   console.log('VectorDatabase: Starting global instance reset...');
1338 | 
1339 |   if (globalVectorDatabase) {
1340 |     try {
1341 |       console.log('VectorDatabase: Clearing existing global instance...');
1342 |       await globalVectorDatabase.clear();
1343 |       console.log('VectorDatabase: Global instance cleared successfully');
1344 |     } catch (error) {
1345 |       console.warn('VectorDatabase: Failed to clear during reset:', error);
1346 |     }
1347 |   }
1348 | 
1349 |   // Additional cleanup: ensure all possible IndexedDB data is cleared
1350 |   try {
1351 |     console.log('VectorDatabase: Performing comprehensive IndexedDB cleanup...');
1352 | 
1353 |     // Clear all data in VectorDatabaseStorage database
1354 |     await IndexedDBHelper.clearAllData();
1355 | 
1356 |     // Clear index files from hnswlib-index database
1357 |     try {
1358 |       console.log('VectorDatabase: Clearing HNSW index files from IndexedDB...');
1359 | 
1360 |       // Try to clean up possible existing index files
1361 |       const possibleIndexFiles = ['tab_content_index.dat', 'content_index.dat', 'vector_index.dat'];
1362 | 
1363 |       // If global hnswlib instance exists, try to delete known index files
1364 |       if (typeof globalHnswlib !== 'undefined' && globalHnswlib) {
1365 |         for (const fileName of possibleIndexFiles) {
1366 |           try {
1367 |             // 1. First try to physically delete index file (using EmscriptenFileSystemManager)
1368 |             try {
1369 |               if (globalHnswlib.EmscriptenFileSystemManager.checkFileExists(fileName)) {
1370 |                 console.log(`VectorDatabase: Deleting physical index file: ${fileName}`);
1371 |                 globalHnswlib.EmscriptenFileSystemManager.deleteFile(fileName);
1372 |                 console.log(`VectorDatabase: Physical index file ${fileName} deleted successfully`);
1373 |               }
1374 |             } catch (fileError) {
1375 |               console.log(
1376 |                 `VectorDatabase: Physical index file ${fileName} not found or failed to delete:`,
1377 |                 fileError,
1378 |               );
1379 |             }
1380 | 
1381 |             // 2. Delete index file from IndexedDB
1382 |             const tempIndex = new globalHnswlib.HierarchicalNSW('cosine', 384);
1383 |             await tempIndex.deleteIndex(fileName);
1384 |             console.log(`VectorDatabase: Deleted IndexedDB index file: ${fileName}`);
1385 |           } catch (deleteError) {
1386 |             // File might not exist, this is normal
1387 |             console.log(`VectorDatabase: Index file ${fileName} not found or already deleted`);
1388 |           }
1389 |         }
1390 | 
1391 |         // 3. Force sync filesystem to ensure deletion takes effect
1392 |         try {
1393 |           await new Promise<void>((resolve) => {
1394 |             const timeout = setTimeout(() => {
1395 |               console.warn('VectorDatabase: Filesystem sync timeout during cleanup');
1396 |               resolve(); // Don't block the process
1397 |             }, 3000);
1398 | 
1399 |             globalHnswlib.EmscriptenFileSystemManager.syncFS(false, () => {
1400 |               clearTimeout(timeout);
1401 |               console.log('VectorDatabase: Filesystem sync completed during cleanup');
1402 |               resolve();
1403 |             });
1404 |           });
1405 |         } catch (syncError) {
1406 |           console.warn('VectorDatabase: Failed to sync filesystem during cleanup:', syncError);
1407 |         }
1408 |       }
1409 |     } catch (hnswError) {
1410 |       console.warn('VectorDatabase: Failed to clear HNSW index files:', hnswError);
1411 |     }
1412 | 
1413 |     // Clear possible chrome.storage backup data (only clear vector database related data, preserve user preferences)
1414 |     const possibleKeys = [
1415 |       'hnswlib_document_mappings_tab_content_index.dat',
1416 |       'hnswlib_document_mappings_content_index.dat',
1417 |       'hnswlib_document_mappings_vector_index.dat',
1418 |       // Note: Don't clear selectedModel and selectedVersion, these are user preference settings
1419 |       // Note: Don't clear modelState, this contains model state info and should be handled by model management logic
1420 |     ];
1421 | 
1422 |     if (possibleKeys.length > 0) {
1423 |       try {
1424 |         await chrome.storage.local.remove(possibleKeys);
1425 |         console.log('VectorDatabase: Chrome storage backup data cleared');
1426 |       } catch (storageError) {
1427 |         console.warn('VectorDatabase: Failed to clear chrome.storage backup:', storageError);
1428 |       }
1429 |     }
1430 | 
1431 |     console.log('VectorDatabase: Comprehensive cleanup completed');
1432 |   } catch (cleanupError) {
1433 |     console.warn('VectorDatabase: Comprehensive cleanup failed:', cleanupError);
1434 |   }
1435 | 
1436 |   globalVectorDatabase = null;
1437 |   currentDimension = null;
1438 |   console.log('VectorDatabase: Global singleton instance reset completed');
1439 | }
1440 | 
1441 | /**
1442 |  * Specifically for data cleanup during model switching
1443 |  * Clear all IndexedDB data, including HNSW index files and document mappings
1444 |  */
1445 | export async function clearAllVectorData(): Promise<void> {
1446 |   console.log('VectorDatabase: Starting comprehensive vector data cleanup for model switch...');
1447 | 
1448 |   try {
1449 |     // 1. Clear global instance
1450 |     if (globalVectorDatabase) {
1451 |       try {
1452 |         await globalVectorDatabase.clear();
1453 |       } catch (error) {
1454 |         console.warn('VectorDatabase: Failed to clear global instance:', error);
1455 |       }
1456 |     }
1457 | 
1458 |     // 2. Clear VectorDatabaseStorage database
1459 |     try {
1460 |       console.log('VectorDatabase: Clearing VectorDatabaseStorage database...');
1461 |       await IndexedDBHelper.clearAllData();
1462 |     } catch (error) {
1463 |       console.warn('VectorDatabase: Failed to clear VectorDatabaseStorage:', error);
1464 |     }
1465 | 
1466 |     // 3. Clear hnswlib-index database and physical files
1467 |     try {
1468 |       console.log('VectorDatabase: Clearing hnswlib-index database and physical files...');
1469 | 
1470 |       // 3.1 First try to physically delete index files (using EmscriptenFileSystemManager)
1471 |       if (typeof globalHnswlib !== 'undefined' && globalHnswlib) {
1472 |         const possibleIndexFiles = [
1473 |           'tab_content_index.dat',
1474 |           'content_index.dat',
1475 |           'vector_index.dat',
1476 |         ];
1477 | 
1478 |         for (const fileName of possibleIndexFiles) {
1479 |           try {
1480 |             if (globalHnswlib.EmscriptenFileSystemManager.checkFileExists(fileName)) {
1481 |               console.log(`VectorDatabase: Deleting physical index file: ${fileName}`);
1482 |               globalHnswlib.EmscriptenFileSystemManager.deleteFile(fileName);
1483 |               console.log(`VectorDatabase: Physical index file ${fileName} deleted successfully`);
1484 |             }
1485 |           } catch (fileError) {
1486 |             console.log(
1487 |               `VectorDatabase: Physical index file ${fileName} not found or failed to delete:`,
1488 |               fileError,
1489 |             );
1490 |           }
1491 |         }
1492 | 
1493 |         // Force sync filesystem
1494 |         try {
1495 |           await new Promise<void>((resolve) => {
1496 |             const timeout = setTimeout(() => {
1497 |               console.warn('VectorDatabase: Filesystem sync timeout during model switch cleanup');
1498 |               resolve();
1499 |             }, 3000);
1500 | 
1501 |             globalHnswlib.EmscriptenFileSystemManager.syncFS(false, () => {
1502 |               clearTimeout(timeout);
1503 |               console.log('VectorDatabase: Filesystem sync completed during model switch cleanup');
1504 |               resolve();
1505 |             });
1506 |           });
1507 |         } catch (syncError) {
1508 |           console.warn(
1509 |             'VectorDatabase: Failed to sync filesystem during model switch cleanup:',
1510 |             syncError,
1511 |           );
1512 |         }
1513 |       }
1514 | 
1515 |       // 3.2 Delete entire hnswlib-index database
1516 |       await new Promise<void>((resolve) => {
1517 |         const deleteRequest = indexedDB.deleteDatabase('/hnswlib-index');
1518 |         deleteRequest.onsuccess = () => {
1519 |           console.log('VectorDatabase: Successfully deleted /hnswlib-index database');
1520 |           resolve();
1521 |         };
1522 |         deleteRequest.onerror = () => {
1523 |           console.warn(
1524 |             'VectorDatabase: Failed to delete /hnswlib-index database:',
1525 |             deleteRequest.error,
1526 |           );
1527 |           resolve(); // Don't block the process
1528 |         };
1529 |         deleteRequest.onblocked = () => {
1530 |           console.warn('VectorDatabase: Deletion of /hnswlib-index database was blocked');
1531 |           resolve(); // Don't block the process
1532 |         };
1533 |       });
1534 |     } catch (error) {
1535 |       console.warn(
1536 |         'VectorDatabase: Failed to clear hnswlib-index database and physical files:',
1537 |         error,
1538 |       );
1539 |     }
1540 | 
1541 |     // 4. Clear backup data from chrome.storage
1542 |     try {
1543 |       const storageKeys = [
1544 |         'hnswlib_document_mappings_tab_content_index.dat',
1545 |         'hnswlib_document_mappings_content_index.dat',
1546 |         'hnswlib_document_mappings_vector_index.dat',
1547 |       ];
1548 |       await chrome.storage.local.remove(storageKeys);
1549 |       console.log('VectorDatabase: Chrome storage backup data cleared');
1550 |     } catch (error) {
1551 |       console.warn('VectorDatabase: Failed to clear chrome.storage backup:', error);
1552 |     }
1553 | 
1554 |     // 5. Reset global state
1555 |     globalVectorDatabase = null;
1556 |     currentDimension = null;
1557 | 
1558 |     console.log('VectorDatabase: Comprehensive vector data cleanup completed successfully');
1559 |   } catch (error) {
1560 |     console.error('VectorDatabase: Comprehensive vector data cleanup failed:', error);
1561 |     throw error;
1562 |   }
1563 | }
1564 | 
```

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

```typescript
   1 | /**
   2 |  * Web Editor V2 Core
   3 |  *
   4 |  * Main orchestrator for the visual editor.
   5 |  * Manages lifecycle of all subsystems (Shadow Host, Canvas, Interaction Engine, etc.)
   6 |  */
   7 | 
   8 | import type {
   9 |   WebEditorApplyBatchPayload,
  10 |   WebEditorElementKey,
  11 |   WebEditorRevertElementResponse,
  12 |   WebEditorSelectionChangedPayload,
  13 |   SelectedElementSummary,
  14 |   WebEditorState,
  15 |   WebEditorTxChangedPayload,
  16 |   WebEditorTxChangeAction,
  17 |   WebEditorV2Api,
  18 | } from '@/common/web-editor-types';
  19 | import { BACKGROUND_MESSAGE_TYPES } from '@/common/message-types';
  20 | import { WEB_EDITOR_V2_VERSION, WEB_EDITOR_V2_LOG_PREFIX } from '../constants';
  21 | import { mountShadowHost, type ShadowHostManager } from '../ui/shadow-host';
  22 | import { createToolbar, type Toolbar } from '../ui/toolbar';
  23 | import { createBreadcrumbs, type Breadcrumbs } from '../ui/breadcrumbs';
  24 | import { createPropertyPanel, type PropertyPanel } from '../ui/property-panel';
  25 | import { createPropsBridge, type PropsBridge } from './props-bridge';
  26 | import { createCanvasOverlay, type CanvasOverlay } from '../overlay/canvas-overlay';
  27 | import { createHandlesController, type HandlesController } from '../overlay/handles-controller';
  28 | import {
  29 |   createDragReorderController,
  30 |   type DragReorderController,
  31 | } from '../drag/drag-reorder-controller';
  32 | import {
  33 |   createEventController,
  34 |   type EventController,
  35 |   type EventModifiers,
  36 | } from './event-controller';
  37 | import { createPositionTracker, type PositionTracker, type TrackedRects } from './position-tracker';
  38 | import { createSelectionEngine, type SelectionEngine } from '../selection/selection-engine';
  39 | import {
  40 |   createTransactionManager,
  41 |   type TransactionManager,
  42 |   type TransactionChangeEvent,
  43 | } from './transaction-manager';
  44 | import { locateElement, createElementLocator } from './locator';
  45 | import { sendTransactionToAgent } from './payload-builder';
  46 | import { aggregateTransactionsByElement } from './transaction-aggregator';
  47 | import {
  48 |   generateStableElementKey,
  49 |   generateElementLabel,
  50 |   generateFullElementLabel,
  51 | } from './element-key';
  52 | import {
  53 |   createExecutionTracker,
  54 |   type ExecutionTracker,
  55 |   type ExecutionState,
  56 | } from './execution-tracker';
  57 | import { createHmrConsistencyVerifier, type HmrConsistencyVerifier } from './hmr-consistency';
  58 | import { createPerfMonitor, type PerfMonitor } from './perf-monitor';
  59 | import { createDesignTokensService, type DesignTokensService } from './design-tokens';
  60 | 
  61 | // =============================================================================
  62 | // Types
  63 | // =============================================================================
  64 | 
  65 | /** Apply operation snapshot for rollback tracking */
  66 | interface ApplySnapshot {
  67 |   txId: string;
  68 |   txTimestamp: number;
  69 | }
  70 | 
  71 | /** Internal editor state */
  72 | interface EditorInternalState {
  73 |   active: boolean;
  74 |   shadowHost: ShadowHostManager | null;
  75 |   canvasOverlay: CanvasOverlay | null;
  76 |   handlesController: HandlesController | null;
  77 |   eventController: EventController | null;
  78 |   positionTracker: PositionTracker | null;
  79 |   selectionEngine: SelectionEngine | null;
  80 |   dragReorderController: DragReorderController | null;
  81 |   transactionManager: TransactionManager | null;
  82 |   executionTracker: ExecutionTracker | null;
  83 |   hmrConsistencyVerifier: HmrConsistencyVerifier | null;
  84 |   toolbar: Toolbar | null;
  85 |   breadcrumbs: Breadcrumbs | null;
  86 |   propertyPanel: PropertyPanel | null;
  87 |   /** Runtime props bridge (Phase 7) */
  88 |   propsBridge: PropsBridge | null;
  89 |   /** Design tokens service (Phase 5.3) */
  90 |   tokensService: DesignTokensService | null;
  91 |   /** Performance monitor (Phase 5.3) - disabled by default */
  92 |   perfMonitor: PerfMonitor | null;
  93 |   /** Cleanup function for perf monitor hotkey */
  94 |   perfHotkeyCleanup: (() => void) | null;
  95 |   /** Currently hovered element (for hover highlight) */
  96 |   hoveredElement: Element | null;
  97 |   /** One-shot flag: whether next hover rect update should animate */
  98 |   pendingHoverTransition: boolean;
  99 |   /** Currently selected element (for selection highlight) */
 100 |   selectedElement: Element | null;
 101 |   /** Snapshot of transaction being applied (for rollback on failure) */
 102 |   applyingSnapshot: ApplySnapshot | null;
 103 |   /** Floating toolbar position (viewport coordinates), null when docked */
 104 |   toolbarPosition: { left: number; top: number } | null;
 105 |   /** Floating property panel position (viewport coordinates), null when anchored */
 106 |   propertyPanelPosition: { left: number; top: number } | null;
 107 |   /** Cleanup for window resize clamping (floating UI) */
 108 |   uiResizeCleanup: (() => void) | null;
 109 | }
 110 | 
 111 | // =============================================================================
 112 | // Implementation
 113 | // =============================================================================
 114 | 
 115 | /**
 116 |  * Create the Web Editor V2 instance.
 117 |  *
 118 |  * This is the main factory function that creates the editor API.
 119 |  * The returned object implements WebEditorV2Api and is exposed on window.__MCP_WEB_EDITOR_V2__
 120 |  */
 121 | export function createWebEditorV2(): WebEditorV2Api {
 122 |   const state: EditorInternalState = {
 123 |     active: false,
 124 |     shadowHost: null,
 125 |     canvasOverlay: null,
 126 |     handlesController: null,
 127 |     eventController: null,
 128 |     positionTracker: null,
 129 |     selectionEngine: null,
 130 |     dragReorderController: null,
 131 |     transactionManager: null,
 132 |     executionTracker: null,
 133 |     hmrConsistencyVerifier: null,
 134 |     toolbar: null,
 135 |     breadcrumbs: null,
 136 |     propertyPanel: null,
 137 |     propsBridge: null,
 138 |     tokensService: null,
 139 |     perfMonitor: null,
 140 |     perfHotkeyCleanup: null,
 141 |     hoveredElement: null,
 142 |     pendingHoverTransition: false,
 143 |     selectedElement: null,
 144 |     applyingSnapshot: null,
 145 |     toolbarPosition: null,
 146 |     propertyPanelPosition: null,
 147 |     uiResizeCleanup: null,
 148 |   };
 149 | 
 150 |   /** Default modifiers for programmatic selection (e.g., from breadcrumbs) */
 151 |   const DEFAULT_MODIFIERS: EventModifiers = {
 152 |     alt: false,
 153 |     shift: false,
 154 |     ctrl: false,
 155 |     meta: false,
 156 |   };
 157 | 
 158 |   // ===========================================================================
 159 |   // Text Editing Session (Phase 2.7)
 160 |   // ===========================================================================
 161 | 
 162 |   interface EditSession {
 163 |     element: HTMLElement;
 164 |     beforeText: string;
 165 |     beforeContentEditable: string | null;
 166 |     beforeSpellcheck: boolean;
 167 |     keydownHandler: (ev: KeyboardEvent) => void;
 168 |     blurHandler: () => void;
 169 |   }
 170 | 
 171 |   let editSession: EditSession | null = null;
 172 | 
 173 |   /** Check if element is a valid text edit target */
 174 |   function isTextEditTarget(element: Element): element is HTMLElement {
 175 |     if (!(element instanceof HTMLElement)) return false;
 176 |     // Not for form controls
 177 |     if (element instanceof HTMLInputElement) return false;
 178 |     if (element instanceof HTMLTextAreaElement) return false;
 179 |     // Only for text-only targets (no element children)
 180 |     if (element.childElementCount > 0) return false;
 181 |     return true;
 182 |   }
 183 | 
 184 |   /** Restore element to pre-edit state */
 185 |   function restoreEditTarget(session: EditSession): void {
 186 |     const { element, beforeContentEditable, beforeSpellcheck } = session;
 187 | 
 188 |     if (beforeContentEditable === null) {
 189 |       element.removeAttribute('contenteditable');
 190 |     } else {
 191 |       element.setAttribute('contenteditable', beforeContentEditable);
 192 |     }
 193 | 
 194 |     element.spellcheck = beforeSpellcheck;
 195 | 
 196 |     // Remove event listeners
 197 |     element.removeEventListener('keydown', session.keydownHandler, true);
 198 |     element.removeEventListener('blur', session.blurHandler, true);
 199 |   }
 200 | 
 201 |   /** Commit the current edit session */
 202 |   function commitEdit(): void {
 203 |     const session = editSession;
 204 |     if (!session) return;
 205 | 
 206 |     editSession = null;
 207 | 
 208 |     const element = session.element;
 209 |     const afterText = element.textContent ?? '';
 210 | 
 211 |     // Normalize to text-only to avoid structure drift from contentEditable
 212 |     element.textContent = afterText;
 213 | 
 214 |     restoreEditTarget(session);
 215 | 
 216 |     // Record transaction if text changed
 217 |     if (session.beforeText !== afterText) {
 218 |       state.transactionManager?.recordText(element, session.beforeText, afterText);
 219 |     }
 220 | 
 221 |     console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Text edit committed`);
 222 |   }
 223 | 
 224 |   /** Cancel the current edit session */
 225 |   function cancelEdit(): void {
 226 |     const session = editSession;
 227 |     if (!session) return;
 228 | 
 229 |     editSession = null;
 230 | 
 231 |     // Restore original text
 232 |     session.element.textContent = session.beforeText;
 233 | 
 234 |     restoreEditTarget(session);
 235 |     console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Text edit cancelled`);
 236 |   }
 237 | 
 238 |   /** Start editing an element */
 239 |   function startEdit(element: Element, modifiers: EventModifiers): boolean {
 240 |     if (!isTextEditTarget(element)) return false;
 241 |     if (!element.isConnected) return false;
 242 | 
 243 |     // Ensure element is selected
 244 |     if (state.selectedElement !== element) {
 245 |       handleSelect(element, modifiers);
 246 |     }
 247 | 
 248 |     // If already editing this element, keep editing
 249 |     if (editSession?.element === element) return true;
 250 | 
 251 |     // Commit previous edit if any
 252 |     if (editSession) {
 253 |       commitEdit();
 254 |     }
 255 | 
 256 |     const beforeText = element.textContent ?? '';
 257 |     const beforeContentEditable = element.getAttribute('contenteditable');
 258 |     const beforeSpellcheck = element.spellcheck;
 259 | 
 260 |     // ESC cancels editing
 261 |     const keydownHandler = (ev: KeyboardEvent) => {
 262 |       if (ev.key !== 'Escape') return;
 263 |       ev.preventDefault();
 264 |       ev.stopPropagation();
 265 |       ev.stopImmediatePropagation();
 266 |       cancelEdit();
 267 |       state.eventController?.setMode('selecting');
 268 |     };
 269 | 
 270 |     // Blur commits editing
 271 |     const blurHandler = () => {
 272 |       commitEdit();
 273 |       state.eventController?.setMode('selecting');
 274 |     };
 275 | 
 276 |     element.addEventListener('keydown', keydownHandler, true);
 277 |     element.addEventListener('blur', blurHandler, true);
 278 | 
 279 |     element.setAttribute('contenteditable', 'true');
 280 |     element.spellcheck = false;
 281 | 
 282 |     try {
 283 |       element.focus({ preventScroll: true });
 284 |     } catch {
 285 |       try {
 286 |         element.focus();
 287 |       } catch {
 288 |         // Best-effort only
 289 |       }
 290 |     }
 291 | 
 292 |     editSession = {
 293 |       element,
 294 |       beforeText,
 295 |       beforeContentEditable,
 296 |       beforeSpellcheck,
 297 |       keydownHandler,
 298 |       blurHandler,
 299 |     };
 300 | 
 301 |     console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Text edit started`);
 302 |     return true;
 303 |   }
 304 | 
 305 |   // ===========================================================================
 306 |   // Event Handlers (wired to EventController callbacks)
 307 |   // ===========================================================================
 308 | 
 309 |   /**
 310 |    * Handle hover state changes from EventController
 311 |    */
 312 |   function handleHover(element: Element | null): void {
 313 |     const prevElement = state.hoveredElement;
 314 |     state.hoveredElement = element;
 315 | 
 316 |     // Determine if we should animate the hover rect transition
 317 |     // Only animate when switching between two valid elements (not null)
 318 |     const shouldAnimate = prevElement !== null && element !== null && prevElement !== element;
 319 |     state.pendingHoverTransition = shouldAnimate;
 320 | 
 321 |     // Delegate position tracking to PositionTracker
 322 |     // Use forceUpdate to avoid extra rAF frame delay
 323 |     if (state.positionTracker) {
 324 |       state.positionTracker.setHoverElement(element);
 325 |       state.positionTracker.forceUpdate();
 326 |     }
 327 |   }
 328 | 
 329 |   /**
 330 |    * Handle element selection from EventController
 331 |    */
 332 |   function handleSelect(element: Element, modifiers: EventModifiers): void {
 333 |     // Commit any in-progress edit when selecting a different element
 334 |     if (editSession && editSession.element !== element) {
 335 |       commitEdit();
 336 |     }
 337 | 
 338 |     state.selectedElement = element;
 339 |     state.hoveredElement = null;
 340 | 
 341 |     // Delegate position tracking to PositionTracker
 342 |     // Clear hover, set selection, then force immediate update
 343 |     if (state.positionTracker) {
 344 |       state.positionTracker.setHoverElement(null);
 345 |       state.positionTracker.setSelectionElement(element);
 346 |       state.positionTracker.forceUpdate();
 347 |     }
 348 | 
 349 |     // Update breadcrumbs to show element ancestry
 350 |     state.breadcrumbs?.setTarget(element);
 351 | 
 352 |     // Update property panel with selected element
 353 |     state.propertyPanel?.setTarget(element);
 354 | 
 355 |     // Update resize handles target (Phase 4.9)
 356 |     state.handlesController?.setTarget(element);
 357 | 
 358 |     // Notify HMR consistency verifier of selection change (Phase 4.8)
 359 |     state.hmrConsistencyVerifier?.onSelectionChange(element);
 360 | 
 361 |     // Broadcast selection to sidepanel for AgentChat context
 362 |     broadcastSelectionChanged(element);
 363 | 
 364 |     // Log selection with modifier info for debugging
 365 |     const modInfo = modifiers.alt ? ' (Alt: drill-up)' : '';
 366 |     console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Selected${modInfo}:`, element.tagName, element);
 367 |   }
 368 | 
 369 |   /**
 370 |    * Handle deselection (ESC key) from EventController
 371 |    */
 372 |   function handleDeselect(): void {
 373 |     state.selectedElement = null;
 374 | 
 375 |     // Clear selection tracking and force immediate update
 376 |     if (state.positionTracker) {
 377 |       state.positionTracker.setSelectionElement(null);
 378 |       state.positionTracker.forceUpdate();
 379 |     }
 380 | 
 381 |     // Clear breadcrumbs
 382 |     state.breadcrumbs?.setTarget(null);
 383 | 
 384 |     // Clear property panel
 385 |     state.propertyPanel?.setTarget(null);
 386 | 
 387 |     // Hide resize handles (Phase 4.9)
 388 |     state.handlesController?.setTarget(null);
 389 | 
 390 |     // Notify HMR consistency verifier of deselection (Phase 4.8)
 391 |     // Deselection should cancel any ongoing verification
 392 |     state.hmrConsistencyVerifier?.onSelectionChange(null);
 393 | 
 394 |     // Broadcast deselection to sidepanel
 395 |     broadcastSelectionChanged(null);
 396 | 
 397 |     console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Deselected`);
 398 |   }
 399 | 
 400 |   /**
 401 |    * Handle position updates from PositionTracker (scroll/resize sync)
 402 |    */
 403 |   function handlePositionUpdate(rects: TrackedRects): void {
 404 |     // Anchor breadcrumbs to the selection rect (viewport coordinates)
 405 |     state.breadcrumbs?.setAnchorRect(rects.selection);
 406 | 
 407 |     // Consume one-shot animation flag (must read before clearing)
 408 |     // This flag is only set when hover element changes, not for scroll/resize
 409 |     const animateHover = state.pendingHoverTransition;
 410 |     state.pendingHoverTransition = false;
 411 | 
 412 |     if (!state.canvasOverlay) return;
 413 | 
 414 |     // Update canvas overlay with new positions
 415 |     state.canvasOverlay.setHoverRect(rects.hover, { animate: animateHover });
 416 |     state.canvasOverlay.setSelectionRect(rects.selection);
 417 | 
 418 |     // Sync resize handles with latest selection rect (Phase 4.9)
 419 |     state.handlesController?.setSelectionRect(rects.selection);
 420 | 
 421 |     // Force immediate render to avoid extra rAF delay
 422 |     // This collapses the render to the same frame as position calculation
 423 |     state.canvasOverlay.render();
 424 |   }
 425 | 
 426 |   // ===========================================================================
 427 |   // AgentChat Integration (Phase 1.4)
 428 |   // ===========================================================================
 429 | 
 430 |   const WEB_EDITOR_TX_CHANGED_SESSION_KEY_PREFIX = 'web-editor-v2-tx-changed-' as const;
 431 |   const TX_CHANGED_BROADCAST_DEBOUNCE_MS = 100;
 432 | 
 433 |   let txChangedBroadcastTimer: number | null = null;
 434 |   let pendingTxAction: WebEditorTxChangeAction = 'push';
 435 | 
 436 |   /**
 437 |    * Broadcast aggregated transaction state to extension UI (e.g., Sidepanel).
 438 |    *
 439 |    * This runs on a short debounce because TransactionManager can emit frequent
 440 |    * merge events during continuous interactions (e.g., dragging sliders).
 441 |    *
 442 |    * NOTE: tabId is set to 0 here; background script will fill in the actual
 443 |    * tabId from sender.tab.id and update storage with per-tab keys.
 444 |    */
 445 |   function broadcastTxChanged(action: WebEditorTxChangeAction): void {
 446 |     // Track the action for when debounce fires
 447 |     pendingTxAction = action;
 448 | 
 449 |     // For 'clear' action, broadcast immediately without debounce
 450 |     // This ensures UI updates instantly when user applies changes
 451 |     const shouldBroadcastImmediately = action === 'clear';
 452 | 
 453 |     if (txChangedBroadcastTimer !== null) {
 454 |       window.clearTimeout(txChangedBroadcastTimer);
 455 |       txChangedBroadcastTimer = null;
 456 |     }
 457 | 
 458 |     const doBroadcast = (): void => {
 459 |       const tm = state.transactionManager;
 460 |       if (!tm) return;
 461 | 
 462 |       const undoStack = tm.getUndoStack();
 463 |       const redoStack = tm.getRedoStack();
 464 |       const elements = aggregateTransactionsByElement(undoStack);
 465 | 
 466 |       const payload: WebEditorTxChangedPayload = {
 467 |         tabId: 0, // Will be filled by background script from sender.tab.id
 468 |         action: pendingTxAction,
 469 |         elements,
 470 |         undoCount: undoStack.length,
 471 |         redoCount: redoStack.length,
 472 |         hasApplicableChanges: elements.length > 0,
 473 |         pageUrl: window.location.href,
 474 |       };
 475 | 
 476 |       // Broadcast to extension UI (background will handle storage persistence)
 477 |       if (typeof chrome !== 'undefined' && chrome.runtime?.sendMessage) {
 478 |         chrome.runtime
 479 |           .sendMessage({
 480 |             type: BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_TX_CHANGED,
 481 |             payload,
 482 |           })
 483 |           .catch(() => {
 484 |             // Ignore if no listeners (e.g., sidepanel not open)
 485 |           });
 486 |       }
 487 |     };
 488 | 
 489 |     if (shouldBroadcastImmediately) {
 490 |       doBroadcast();
 491 |     } else {
 492 |       txChangedBroadcastTimer = window.setTimeout(doBroadcast, TX_CHANGED_BROADCAST_DEBOUNCE_MS);
 493 |     }
 494 |   }
 495 | 
 496 |   /** Last broadcasted selection key to avoid duplicate broadcasts */
 497 |   let lastBroadcastedSelectionKey: string | null = null;
 498 | 
 499 |   /**
 500 |    * Broadcast selection change to sidepanel (no debounce - immediate).
 501 |    * Called when user selects or deselects an element.
 502 |    */
 503 |   function broadcastSelectionChanged(element: Element | null): void {
 504 |     // Build selected element summary if element is provided
 505 |     let selected: SelectedElementSummary | null = null;
 506 | 
 507 |     if (element) {
 508 |       const elementKey = generateStableElementKey(element);
 509 | 
 510 |       // Dedupe: skip if same element already broadcasted
 511 |       if (elementKey === lastBroadcastedSelectionKey) return;
 512 |       lastBroadcastedSelectionKey = elementKey;
 513 | 
 514 |       const locator = createElementLocator(element);
 515 |       selected = {
 516 |         elementKey,
 517 |         locator,
 518 |         label: generateElementLabel(element),
 519 |         fullLabel: generateFullElementLabel(element),
 520 |         tagName: element.tagName.toLowerCase(),
 521 |         updatedAt: Date.now(),
 522 |       };
 523 |     } else {
 524 |       // Deselection - clear tracking
 525 |       if (lastBroadcastedSelectionKey === null) return; // Already deselected
 526 |       lastBroadcastedSelectionKey = null;
 527 |     }
 528 | 
 529 |     const payload: WebEditorSelectionChangedPayload = {
 530 |       tabId: 0, // Will be filled by background script from sender.tab.id
 531 |       selected,
 532 |       pageUrl: window.location.href,
 533 |     };
 534 | 
 535 |     // Broadcast immediately (no debounce for selection changes)
 536 |     if (typeof chrome !== 'undefined' && chrome.runtime?.sendMessage) {
 537 |       chrome.runtime
 538 |         .sendMessage({
 539 |           type: BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_SELECTION_CHANGED,
 540 |           payload,
 541 |         })
 542 |         .catch(() => {
 543 |           // Ignore if no listeners (e.g., sidepanel not open)
 544 |         });
 545 |     }
 546 |   }
 547 | 
 548 |   /**
 549 |    * Broadcast "editor cleared" state when stopping.
 550 |    * Sends empty TX and null selection to remove chips from sidepanel.
 551 |    */
 552 |   function broadcastEditorCleared(): void {
 553 |     // Reset selection dedupe so next start can broadcast correctly
 554 |     lastBroadcastedSelectionKey = null;
 555 | 
 556 |     if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) return;
 557 | 
 558 |     const pageUrl = window.location.href;
 559 | 
 560 |     // Send empty TX state
 561 |     const txPayload: WebEditorTxChangedPayload = {
 562 |       tabId: 0,
 563 |       action: 'clear',
 564 |       elements: [],
 565 |       undoCount: 0,
 566 |       redoCount: 0,
 567 |       hasApplicableChanges: false,
 568 |       pageUrl,
 569 |     };
 570 | 
 571 |     // Send null selection
 572 |     const selectionPayload: WebEditorSelectionChangedPayload = {
 573 |       tabId: 0,
 574 |       selected: null,
 575 |       pageUrl,
 576 |     };
 577 | 
 578 |     chrome.runtime
 579 |       .sendMessage({
 580 |         type: BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_TX_CHANGED,
 581 |         payload: txPayload,
 582 |       })
 583 |       .catch(() => {});
 584 | 
 585 |     chrome.runtime
 586 |       .sendMessage({
 587 |         type: BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_SELECTION_CHANGED,
 588 |         payload: selectionPayload,
 589 |       })
 590 |       .catch(() => {});
 591 |   }
 592 | 
 593 |   /**
 594 |    * Handle transaction changes from TransactionManager
 595 |    */
 596 |   function handleTransactionChange(event: TransactionChangeEvent): void {
 597 |     // Log transaction events for debugging
 598 |     const { action, undoCount, redoCount } = event;
 599 |     console.log(
 600 |       `${WEB_EDITOR_V2_LOG_PREFIX} Transaction: ${action} (undo: ${undoCount}, redo: ${redoCount})`,
 601 |     );
 602 | 
 603 |     // Update toolbar UI with undo/redo counts
 604 |     state.toolbar?.setHistory(undoCount, redoCount);
 605 | 
 606 |     // Refresh property panel after undo/redo to reflect current styles
 607 |     if (action === 'undo' || action === 'redo') {
 608 |       state.propertyPanel?.refresh();
 609 |     }
 610 | 
 611 |     // Broadcast aggregated TX state for AgentChat integration (Phase 1.4)
 612 |     broadcastTxChanged(action as WebEditorTxChangeAction);
 613 | 
 614 |     // Notify HMR consistency verifier of transaction change (Phase 4.8)
 615 |     state.hmrConsistencyVerifier?.onTransactionChange(event);
 616 |   }
 617 | 
 618 |   /**
 619 |    * Check if the transaction being applied is still the latest in undo stack.
 620 |    * Used to determine if we should auto-rollback on failure.
 621 |    *
 622 |    * Returns a detailed status to distinguish between:
 623 |    * - 'ok': Transaction is still latest, safe to rollback
 624 |    * - 'no_snapshot': No apply in progress
 625 |    * - 'tm_unavailable': TransactionManager not available
 626 |    * - 'stack_empty': Undo stack is empty (tx was already undone)
 627 |    * - 'tx_changed': User made new edits or tx was merged
 628 |    */
 629 |   type ApplyTxStatus = 'ok' | 'no_snapshot' | 'tm_unavailable' | 'stack_empty' | 'tx_changed';
 630 | 
 631 |   function checkApplyingTxStatus(): ApplyTxStatus {
 632 |     const snapshot = state.applyingSnapshot;
 633 |     if (!snapshot) return 'no_snapshot';
 634 | 
 635 |     const tm = state.transactionManager;
 636 |     if (!tm) return 'tm_unavailable';
 637 | 
 638 |     const undoStack = tm.getUndoStack();
 639 |     if (undoStack.length === 0) return 'stack_empty';
 640 | 
 641 |     const latest = undoStack[undoStack.length - 1]!;
 642 | 
 643 |     // Check both id and timestamp to handle merged transactions
 644 |     if (latest.id !== snapshot.txId || latest.timestamp !== snapshot.txTimestamp) {
 645 |       return 'tx_changed';
 646 |     }
 647 | 
 648 |     return 'ok';
 649 |   }
 650 | 
 651 |   /**
 652 |    * Attempt to rollback the applying transaction on failure.
 653 |    * Returns a descriptive error message based on rollback result.
 654 |    *
 655 |    * Rollback is only attempted when:
 656 |    * - The transaction is still the latest in undo stack
 657 |    * - No new edits were made during the apply operation
 658 |    */
 659 |   function attemptRollbackOnFailure(originalError: string): string {
 660 |     const status = checkApplyingTxStatus();
 661 | 
 662 |     // Cannot rollback: TM not available or no snapshot
 663 |     if (status === 'no_snapshot' || status === 'tm_unavailable') {
 664 |       console.error(`${WEB_EDITOR_V2_LOG_PREFIX} Apply failed, unable to revert (${status})`);
 665 |       return `${originalError} (unable to revert)`;
 666 |     }
 667 | 
 668 |     // Stack is empty - tx was already undone (race condition or user action)
 669 |     if (status === 'stack_empty') {
 670 |       console.warn(`${WEB_EDITOR_V2_LOG_PREFIX} Apply failed, stack empty (already reverted?)`);
 671 |       return `${originalError} (already reverted)`;
 672 |     }
 673 | 
 674 |     // User made new edits during apply - don't rollback their work
 675 |     if (status === 'tx_changed') {
 676 |       console.warn(
 677 |         `${WEB_EDITOR_V2_LOG_PREFIX} Apply failed but new edits detected, skipping auto-rollback`,
 678 |       );
 679 |       return `${originalError} (new edits detected, not reverted)`;
 680 |     }
 681 | 
 682 |     // Status is 'ok' - safe to attempt rollback
 683 |     const tm = state.transactionManager!;
 684 |     const undone = tm.undo();
 685 |     if (undone) {
 686 |       console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Apply failed, changes auto-reverted`);
 687 |       return `${originalError} (changes reverted)`;
 688 |     }
 689 | 
 690 |     // undo() returned null - likely locateElement() failed
 691 |     console.error(`${WEB_EDITOR_V2_LOG_PREFIX} Apply failed and auto-revert also failed`);
 692 |     return `${originalError} (revert failed)`;
 693 |   }
 694 | 
 695 |   /**
 696 |    * Apply the latest transaction to Agent (Apply to Code)
 697 |    *
 698 |    * Phase 2.10: On failure, automatically attempts to undo the transaction
 699 |    * to revert DOM changes. The transaction moves to redo stack so user can retry.
 700 |    */
 701 |   async function applyLatestTransaction(): Promise<{ requestId?: string; sessionId?: string }> {
 702 |     const tm = state.transactionManager;
 703 |     if (!tm) {
 704 |       throw new Error('Transaction manager not ready');
 705 |     }
 706 | 
 707 |     // Prevent concurrent apply operations
 708 |     if (state.applyingSnapshot) {
 709 |       throw new Error('Apply already in progress');
 710 |     }
 711 | 
 712 |     const undoStack = tm.getUndoStack();
 713 |     const tx = undoStack.length > 0 ? undoStack[undoStack.length - 1] : null;
 714 |     if (!tx) {
 715 |       throw new Error('No changes to apply');
 716 |     }
 717 | 
 718 |     // Apply-to-Code currently supports only style/text transactions
 719 |     if (tx.type !== 'style' && tx.type !== 'text') {
 720 |       throw new Error(`Apply does not support "${tx.type}" transactions yet`);
 721 |     }
 722 | 
 723 |     // Snapshot the transaction for rollback tracking
 724 |     state.applyingSnapshot = {
 725 |       txId: tx.id,
 726 |       txTimestamp: tx.timestamp,
 727 |     };
 728 | 
 729 |     // Markers indicating error was already processed by attemptRollbackOnFailure
 730 |     const ROLLBACK_MARKERS = [
 731 |       '(changes reverted)',
 732 |       '(new edits detected',
 733 |       '(revert failed)',
 734 |       '(unable to revert)',
 735 |       '(already reverted)',
 736 |     ];
 737 | 
 738 |     const isAlreadyProcessed = (err: unknown): boolean =>
 739 |       err instanceof Error && ROLLBACK_MARKERS.some((m) => err.message.includes(m));
 740 | 
 741 |     try {
 742 |       const resp = await sendTransactionToAgent(tx);
 743 |       const r = resp as {
 744 |         success?: unknown;
 745 |         requestId?: unknown;
 746 |         sessionId?: unknown;
 747 |         error?: unknown;
 748 |       } | null;
 749 | 
 750 |       if (r && r.success === true) {
 751 |         const requestId = typeof r.requestId === 'string' ? r.requestId : undefined;
 752 |         const sessionId = typeof r.sessionId === 'string' ? r.sessionId : undefined;
 753 | 
 754 |         // Start tracking execution status if we have a requestId
 755 |         if (requestId && sessionId && state.executionTracker) {
 756 |           state.executionTracker.track(requestId, sessionId);
 757 |         }
 758 | 
 759 |         // Start HMR consistency verification (Phase 4.8)
 760 |         state.hmrConsistencyVerifier?.start({
 761 |           tx,
 762 |           requestId,
 763 |           sessionId,
 764 |           element: state.selectedElement,
 765 |         });
 766 | 
 767 |         return { requestId, sessionId };
 768 |       }
 769 | 
 770 |       // Agent returned failure response - attempt rollback
 771 |       const errorMsg = typeof r?.error === 'string' ? r.error : 'Agent request failed';
 772 |       throw new Error(attemptRollbackOnFailure(errorMsg));
 773 |     } catch (error) {
 774 |       // Re-throw if already processed by attemptRollbackOnFailure
 775 |       if (isAlreadyProcessed(error)) {
 776 |         throw error;
 777 |       }
 778 | 
 779 |       // Network error or other unprocessed exception - attempt rollback
 780 |       const originalMsg = error instanceof Error ? error.message : String(error);
 781 |       throw new Error(attemptRollbackOnFailure(originalMsg));
 782 |     } finally {
 783 |       // Clear snapshot regardless of outcome
 784 |       state.applyingSnapshot = null;
 785 |     }
 786 |   }
 787 | 
 788 |   /**
 789 |    * Apply all applicable transactions to Agent (batch Apply to Code)
 790 |    *
 791 |    * Phase 1.4: Aggregates the undo stack by element and sends a single batch request.
 792 |    * Unlike applyLatestTransaction, this does NOT auto-rollback on failure.
 793 |    */
 794 |   async function applyAllTransactions(): Promise<{ requestId?: string; sessionId?: string }> {
 795 |     const tm = state.transactionManager;
 796 |     if (!tm) {
 797 |       throw new Error('Transaction manager not ready');
 798 |     }
 799 | 
 800 |     // Prevent concurrent apply operations
 801 |     if (state.applyingSnapshot) {
 802 |       throw new Error('Apply already in progress');
 803 |     }
 804 | 
 805 |     const undoStack = tm.getUndoStack();
 806 |     if (undoStack.length === 0) {
 807 |       throw new Error('No changes to apply');
 808 |     }
 809 | 
 810 |     // Block unsupported transaction types
 811 |     for (const tx of undoStack) {
 812 |       if (tx.type === 'move') {
 813 |         throw new Error('Apply does not support reorder operations yet');
 814 |       }
 815 |       if (tx.type === 'structure') {
 816 |         throw new Error('Apply does not support structure operations yet');
 817 |       }
 818 |       if (tx.type !== 'style' && tx.type !== 'text' && tx.type !== 'class') {
 819 |         throw new Error(`Apply does not support "${tx.type}" transactions`);
 820 |       }
 821 |     }
 822 | 
 823 |     const elements = aggregateTransactionsByElement(undoStack);
 824 |     if (elements.length === 0) {
 825 |       throw new Error('No net changes to apply');
 826 |     }
 827 | 
 828 |     // Snapshot latest transaction for concurrency tracking
 829 |     const latestTx = undoStack[undoStack.length - 1]!;
 830 |     state.applyingSnapshot = {
 831 |       txId: latestTx.id,
 832 |       txTimestamp: latestTx.timestamp,
 833 |     };
 834 | 
 835 |     try {
 836 |       if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) {
 837 |         throw new Error('Chrome runtime API not available');
 838 |       }
 839 | 
 840 |       const payload: WebEditorApplyBatchPayload = {
 841 |         tabId: 0, // Will be filled by background script
 842 |         elements,
 843 |         excludedKeys: [], // TODO: Read from storage if exclude feature is implemented
 844 |         pageUrl: window.location.href,
 845 |       };
 846 | 
 847 |       const resp = await chrome.runtime.sendMessage({
 848 |         type: BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_APPLY_BATCH,
 849 |         payload,
 850 |       });
 851 | 
 852 |       const r = resp as {
 853 |         success?: unknown;
 854 |         requestId?: unknown;
 855 |         sessionId?: unknown;
 856 |         error?: unknown;
 857 |       } | null;
 858 | 
 859 |       if (r && r.success === true) {
 860 |         const requestId = typeof r.requestId === 'string' ? r.requestId : undefined;
 861 |         const sessionId = typeof r.sessionId === 'string' ? r.sessionId : undefined;
 862 | 
 863 |         // Start tracking execution status if we have a requestId
 864 |         if (requestId && sessionId && state.executionTracker) {
 865 |           state.executionTracker.track(requestId, sessionId);
 866 |         }
 867 | 
 868 |         // Clear transaction history after successful apply
 869 |         // This prevents undo/redo since changes are now committed to code
 870 |         tm.clear();
 871 | 
 872 |         // Deselect current element after successful apply
 873 |         // This clears the selection chip in the UI
 874 |         handleDeselect();
 875 | 
 876 |         return { requestId, sessionId };
 877 |       }
 878 | 
 879 |       const errorMsg = typeof r?.error === 'string' ? r.error : 'Agent request failed';
 880 |       throw new Error(errorMsg);
 881 |     } finally {
 882 |       state.applyingSnapshot = null;
 883 |     }
 884 |   }
 885 | 
 886 |   /**
 887 |    * Revert a specific element to its baseline state (Phase 2 - Selective Undo).
 888 |    * Creates compensating transactions so the user can undo the revert.
 889 |    */
 890 |   async function revertElement(
 891 |     elementKey: WebEditorElementKey,
 892 |   ): Promise<WebEditorRevertElementResponse> {
 893 |     const key = String(elementKey ?? '').trim();
 894 |     if (!key) {
 895 |       return { success: false, error: 'elementKey is required' };
 896 |     }
 897 | 
 898 |     const tm = state.transactionManager;
 899 |     if (!tm) {
 900 |       return { success: false, error: 'Transaction manager not ready' };
 901 |     }
 902 | 
 903 |     if (state.applyingSnapshot) {
 904 |       return { success: false, error: 'Cannot revert while Apply is in progress' };
 905 |     }
 906 | 
 907 |     try {
 908 |       const undoStack = tm.getUndoStack();
 909 |       const summaries = aggregateTransactionsByElement(undoStack);
 910 |       const summary = summaries.find((s) => s.elementKey === key);
 911 | 
 912 |       if (!summary) {
 913 |         return { success: false, error: 'Element not found in current changes' };
 914 |       }
 915 | 
 916 |       const element = locateElement(summary.locator);
 917 |       if (!element || !element.isConnected) {
 918 |         return { success: false, error: 'Failed to locate element for revert' };
 919 |       }
 920 | 
 921 |       const reverted: NonNullable<WebEditorRevertElementResponse['reverted']> = {};
 922 |       let didRevert = false;
 923 | 
 924 |       // Revert class first so subsequent locators are based on baseline classes.
 925 |       const classChanges = summary.netEffect.classChanges;
 926 |       if (classChanges) {
 927 |         const baselineClasses = Array.isArray(classChanges.before) ? classChanges.before : [];
 928 |         const beforeClasses = (() => {
 929 |           try {
 930 |             const list = (element as HTMLElement).classList;
 931 |             if (list && typeof list[Symbol.iterator] === 'function') {
 932 |               return Array.from(list).filter(Boolean);
 933 |             }
 934 |           } catch {
 935 |             // Fallback for non-HTMLElement
 936 |           }
 937 | 
 938 |           const raw = element.getAttribute('class') ?? '';
 939 |           return raw
 940 |             .split(/\s+/)
 941 |             .map((t) => t.trim())
 942 |             .filter(Boolean);
 943 |         })();
 944 | 
 945 |         const tx = tm.recordClass(element, beforeClasses, baselineClasses);
 946 |         if (tx) {
 947 |           reverted.class = true;
 948 |           didRevert = true;
 949 |         }
 950 |       }
 951 | 
 952 |       // Revert text content
 953 |       const textChange = summary.netEffect.textChange;
 954 |       if (textChange) {
 955 |         const baselineText = String(textChange.before ?? '');
 956 |         const beforeText = element.textContent ?? '';
 957 | 
 958 |         if (beforeText !== baselineText) {
 959 |           element.textContent = baselineText;
 960 |           const tx = tm.recordText(element, beforeText, baselineText);
 961 |           if (tx) {
 962 |             reverted.text = true;
 963 |             didRevert = true;
 964 |           }
 965 |         }
 966 |       }
 967 | 
 968 |       // Revert styles
 969 |       const styleChanges = summary.netEffect.styleChanges;
 970 |       if (styleChanges) {
 971 |         const before = styleChanges.before ?? {};
 972 |         const after = styleChanges.after ?? {};
 973 | 
 974 |         const properties = Array.from(new Set([...Object.keys(before), ...Object.keys(after)]))
 975 |           .map((p) => String(p ?? '').trim())
 976 |           .filter(Boolean);
 977 | 
 978 |         if (properties.length > 0) {
 979 |           const handle = tm.beginMultiStyle(element, properties);
 980 |           if (handle) {
 981 |             handle.set(before);
 982 |             const tx = handle.commit({ merge: false });
 983 |             if (tx) {
 984 |               reverted.style = true;
 985 |               didRevert = true;
 986 |             }
 987 |           }
 988 |         }
 989 |       }
 990 | 
 991 |       if (!didRevert) {
 992 |         return { success: false, error: 'No changes were reverted' };
 993 |       }
 994 | 
 995 |       // Ensure property panel reflects reverted values immediately
 996 |       state.propertyPanel?.refresh();
 997 | 
 998 |       return { success: true, reverted };
 999 |     } catch (error) {
1000 |       console.error(`${WEB_EDITOR_V2_LOG_PREFIX} Revert element failed:`, error);
1001 |       return {
1002 |         success: false,
1003 |         error: error instanceof Error ? error.message : String(error),
1004 |       };
1005 |     }
1006 |   }
1007 | 
1008 |   /**
1009 |    * Clear current selection (called from sidepanel after send).
1010 |    * Triggers handleDeselect which broadcasts null selection to sidepanel.
1011 |    */
1012 |   function clearSelection(): void {
1013 |     if (!state.selectedElement) {
1014 |       // Already deselected
1015 |       return;
1016 |     }
1017 | 
1018 |     // Use EventController to properly transition to hover mode
1019 |     // This triggers onDeselect callback → handleDeselect → broadcastSelectionChanged(null)
1020 |     if (state.eventController) {
1021 |       state.eventController.setMode('hover');
1022 | 
1023 |       // Edge case: if setMode('hover') didn't trigger deselect (e.g., already in hover mode
1024 |       // but selectedElement was set programmatically), manually call handleDeselect
1025 |       if (state.selectedElement) {
1026 |         handleDeselect();
1027 |       }
1028 |     } else {
1029 |       // Fallback if eventController not available: directly call handleDeselect
1030 |       handleDeselect();
1031 |     }
1032 | 
1033 |     console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Selection cleared (from sidepanel)`);
1034 |   }
1035 | 
1036 |   /**
1037 |    * Handle transaction apply errors
1038 |    */
1039 |   function handleTransactionError(error: unknown): void {
1040 |     console.error(`${WEB_EDITOR_V2_LOG_PREFIX} Transaction apply error:`, error);
1041 |   }
1042 | 
1043 |   /**
1044 |    * Start the editor
1045 |    */
1046 |   function start(): void {
1047 |     if (state.active) {
1048 |       console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Already active`);
1049 |       return;
1050 |     }
1051 | 
1052 |     try {
1053 |       // Mount Shadow DOM host
1054 |       state.shadowHost = mountShadowHost({});
1055 | 
1056 |       // Initialize Canvas Overlay
1057 |       const elements = state.shadowHost.getElements();
1058 |       if (!elements?.overlayRoot) {
1059 |         throw new Error('Shadow host overlayRoot not available');
1060 |       }
1061 |       state.canvasOverlay = createCanvasOverlay({
1062 |         container: elements.overlayRoot,
1063 |       });
1064 | 
1065 |       // Initialize Performance Monitor (Phase 5.3) - disabled by default
1066 |       state.perfMonitor = createPerfMonitor({
1067 |         container: elements.overlayRoot,
1068 |         fpsUiIntervalMs: 500,
1069 |         memorySampleIntervalMs: 1000,
1070 |       });
1071 | 
1072 |       // Register hotkey: Ctrl/Cmd + Shift + P toggles perf monitor
1073 |       const perfHotkeyHandler = (event: KeyboardEvent): void => {
1074 |         // Ignore key repeats to avoid rapid toggles when holding the shortcut
1075 |         if (event.repeat) return;
1076 | 
1077 |         const isMod = event.metaKey || event.ctrlKey;
1078 |         if (!isMod) return;
1079 |         if (!event.shiftKey) return;
1080 |         if (event.altKey) return;
1081 | 
1082 |         const key = (event.key || '').toLowerCase();
1083 |         if (key !== 'p') return;
1084 | 
1085 |         const monitor = state.perfMonitor;
1086 |         if (!monitor) return;
1087 | 
1088 |         monitor.toggle();
1089 | 
1090 |         // Prevent browser shortcuts (e.g., print dialog)
1091 |         event.preventDefault();
1092 |         event.stopPropagation();
1093 |         event.stopImmediatePropagation();
1094 |       };
1095 | 
1096 |       const hotkeyOptions: AddEventListenerOptions = { capture: true, passive: false };
1097 |       window.addEventListener('keydown', perfHotkeyHandler, hotkeyOptions);
1098 |       state.perfHotkeyCleanup = () => {
1099 |         window.removeEventListener('keydown', perfHotkeyHandler, hotkeyOptions);
1100 |       };
1101 | 
1102 |       // Initialize Selection Engine for intelligent element picking
1103 |       state.selectionEngine = createSelectionEngine({
1104 |         isOverlayElement: state.shadowHost.isOverlayElement,
1105 |       });
1106 | 
1107 |       // Initialize Position Tracker for scroll/resize synchronization
1108 |       state.positionTracker = createPositionTracker({
1109 |         onPositionUpdate: handlePositionUpdate,
1110 |       });
1111 | 
1112 |       // Initialize Transaction Manager for undo/redo support
1113 |       // Use isEventFromUi (not isOverlayElement) to properly check event source
1114 |       state.transactionManager = createTransactionManager({
1115 |         enableKeyBindings: true,
1116 |         // Include both Shadow UI events and events from editing element
1117 |         // This prevents Ctrl/Cmd+Z from triggering global undo while editing text
1118 |         isEventFromEditorUi: (event) => {
1119 |           if (state.shadowHost?.isEventFromUi(event)) return true;
1120 |           // Also ignore events from the editing element (allow native contentEditable undo)
1121 |           const session = editSession;
1122 |           if (session?.element) {
1123 |             try {
1124 |               const path = typeof event.composedPath === 'function' ? event.composedPath() : null;
1125 |               if (path?.some((node) => node === session.element)) return true;
1126 |             } catch {
1127 |               // Fallback
1128 |               const target = event.target;
1129 |               if (target instanceof Node && session.element.contains(target)) return true;
1130 |             }
1131 |           }
1132 |           return false;
1133 |         },
1134 |         onChange: handleTransactionChange,
1135 |         onApplyError: handleTransactionError,
1136 |       });
1137 | 
1138 |       // Initialize Resize Handles Controller (Phase 4.9)
1139 |       state.handlesController = createHandlesController({
1140 |         container: elements.overlayRoot,
1141 |         canvasOverlay: state.canvasOverlay,
1142 |         transactionManager: state.transactionManager,
1143 |         positionTracker: state.positionTracker,
1144 |       });
1145 | 
1146 |       // Initialize Drag Reorder Controller (Phase 2.4-2.6)
1147 |       state.dragReorderController = createDragReorderController({
1148 |         isOverlayElement: state.shadowHost.isOverlayElement,
1149 |         uiRoot: elements.uiRoot,
1150 |         canvasOverlay: state.canvasOverlay,
1151 |         positionTracker: state.positionTracker,
1152 |         transactionManager: state.transactionManager,
1153 |       });
1154 | 
1155 |       // Initialize Event Controller for interaction handling
1156 |       // Wire up SelectionEngine's findBestTargetFromEvent for Shadow DOM-aware selection (click only)
1157 |       // Hover uses fast elementFromPoint for 60FPS performance
1158 |       state.eventController = createEventController({
1159 |         isOverlayElement: state.shadowHost.isOverlayElement,
1160 |         onHover: handleHover,
1161 |         onSelect: handleSelect,
1162 |         onDeselect: handleDeselect,
1163 |         onStartEdit: startEdit,
1164 |         findTargetForSelect: (_x, _y, modifiers, event) =>
1165 |           state.selectionEngine?.findBestTargetFromEvent(event, modifiers) ?? null,
1166 |         getSelectedElement: () => state.selectedElement,
1167 |         onStartDrag: (ev) => state.dragReorderController?.onDragStart(ev) ?? false,
1168 |         onDragMove: (ev) => state.dragReorderController?.onDragMove(ev),
1169 |         onDragEnd: (ev) => state.dragReorderController?.onDragEnd(ev),
1170 |         onDragCancel: (ev) => state.dragReorderController?.onDragCancel(ev),
1171 |       });
1172 | 
1173 |       // Initialize ExecutionTracker for tracking Agent execution status (Phase 3.10)
1174 |       state.executionTracker = createExecutionTracker({
1175 |         onStatusChange: (execState: ExecutionState) => {
1176 |           // Map execution status to toolbar status (only used when HMR verifier is not active)
1177 |           // When verifier is active, it controls toolbar status after execution completes
1178 |           const verifierPhase = state.hmrConsistencyVerifier?.getSnapshot().phase ?? 'idle';
1179 |           const verifierActive = verifierPhase !== 'idle';
1180 | 
1181 |           // Only update toolbar directly if verifier is not handling it
1182 |           if (!verifierActive || execState.status !== 'completed') {
1183 |             const statusMap: Record<string, string> = {
1184 |               pending: 'applying',
1185 |               starting: 'starting',
1186 |               running: 'running',
1187 |               locating: 'locating',
1188 |               applying: 'applying',
1189 |               completed: 'completed',
1190 |               failed: 'failed',
1191 |               error: 'failed', // Server may return 'error', treat same as 'failed'
1192 |               timeout: 'timeout',
1193 |               cancelled: 'cancelled',
1194 |             };
1195 |             type ToolbarStatusType = Parameters<NonNullable<typeof state.toolbar>['setStatus']>[0];
1196 |             const toolbarStatus = (statusMap[execState.status] ?? 'running') as ToolbarStatusType;
1197 |             state.toolbar?.setStatus(toolbarStatus, execState.message);
1198 |           }
1199 | 
1200 |           // Forward to HMR consistency verifier (Phase 4.8)
1201 |           state.hmrConsistencyVerifier?.onExecutionStatus(execState);
1202 |         },
1203 |       });
1204 | 
1205 |       // Initialize HMR Consistency Verifier (Phase 4.8)
1206 |       state.hmrConsistencyVerifier = createHmrConsistencyVerifier({
1207 |         transactionManager: state.transactionManager,
1208 |         getSelectedElement: () => state.selectedElement,
1209 |         onReselect: (element) => handleSelect(element, DEFAULT_MODIFIERS),
1210 |         onDeselect: handleDeselect,
1211 |         setToolbarStatus: (status, message) => state.toolbar?.setStatus(status, message),
1212 |         isOverlayElement: state.shadowHost?.isOverlayElement,
1213 |         selectionEngine: state.selectionEngine ?? undefined,
1214 |       });
1215 | 
1216 |       // Initialize Toolbar UI
1217 |       state.toolbar = createToolbar({
1218 |         container: elements.uiRoot,
1219 |         dock: 'top',
1220 |         initialPosition: state.toolbarPosition,
1221 |         onPositionChange: (position) => {
1222 |           state.toolbarPosition = position;
1223 |         },
1224 |         getApplyBlockReason: () => {
1225 |           const tm = state.transactionManager;
1226 |           if (!tm) return undefined;
1227 | 
1228 |           const undoStack = tm.getUndoStack();
1229 |           if (undoStack.length === 0) return undefined;
1230 | 
1231 |           // Check all transactions for unsupported types (Phase 1.4)
1232 |           // NOTE: We only do O(n) type checking here, NOT aggregation.
1233 |           // Full net effect check happens in applyAllTransactions() to avoid
1234 |           // performance issues during frequent merge events.
1235 |           for (const tx of undoStack) {
1236 |             if (tx.type === 'move') {
1237 |               return 'Apply does not support reorder operations yet';
1238 |             }
1239 |             if (tx.type === 'structure') {
1240 |               return 'Apply does not support structure operations yet';
1241 |             }
1242 |             if (tx.type !== 'style' && tx.type !== 'text' && tx.type !== 'class') {
1243 |               return `Apply does not support "${tx.type}" transactions`;
1244 |             }
1245 |           }
1246 | 
1247 |           return undefined;
1248 |         },
1249 |         getSelectedElement: () => state.selectedElement,
1250 |         onStructure: (data) => {
1251 |           const target = state.selectedElement;
1252 |           if (!target) return;
1253 | 
1254 |           const tm = state.transactionManager;
1255 |           if (!tm) return;
1256 | 
1257 |           const tx = tm.applyStructure(target, data);
1258 |           if (!tx) return;
1259 | 
1260 |           // Update selection based on action type
1261 |           // For wrap/stack: select the new wrapper
1262 |           // For unwrap: select the unwrapped child
1263 |           // For duplicate: select the clone
1264 |           // For delete: deselect
1265 |           if (data.action === 'delete') {
1266 |             handleDeselect();
1267 |           } else {
1268 |             // The transaction's targetLocator points to the new selection target
1269 |             // For wrap/stack: wrapper
1270 |             // For unwrap: child
1271 |             // For duplicate: clone
1272 |             const newTarget = locateElement(tx.targetLocator);
1273 |             if (newTarget && newTarget.isConnected) {
1274 |               handleSelect(newTarget, DEFAULT_MODIFIERS);
1275 |             }
1276 |           }
1277 |         },
1278 |         onApply: applyAllTransactions,
1279 |         onUndo: () => state.transactionManager?.undo(),
1280 |         onRedo: () => state.transactionManager?.redo(),
1281 |         onRequestClose: () => stop(),
1282 |       });
1283 | 
1284 |       // Initialize toolbar history display
1285 |       state.toolbar.setHistory(
1286 |         state.transactionManager.getUndoStack().length,
1287 |         state.transactionManager.getRedoStack().length,
1288 |       );
1289 | 
1290 |       // Initialize Breadcrumbs UI (shows selected element ancestry)
1291 |       state.breadcrumbs = createBreadcrumbs({
1292 |         container: elements.uiRoot,
1293 |         dock: 'top',
1294 |         onSelect: (element) => {
1295 |           // When a breadcrumb is clicked, select that ancestor element
1296 |           if (element.isConnected) {
1297 |             handleSelect(element, DEFAULT_MODIFIERS);
1298 |           }
1299 |         },
1300 |       });
1301 | 
1302 |       // Initialize Props Bridge (Phase 7)
1303 |       state.propsBridge = createPropsBridge({});
1304 | 
1305 |       // Initialize Design Tokens Service (Phase 5.3)
1306 |       state.tokensService = createDesignTokensService();
1307 | 
1308 |       // Initialize Property Panel (Phase 3)
1309 |       state.propertyPanel = createPropertyPanel({
1310 |         container: elements.uiRoot,
1311 |         transactionManager: state.transactionManager,
1312 |         propsBridge: state.propsBridge,
1313 |         tokensService: state.tokensService,
1314 |         initialPosition: state.propertyPanelPosition,
1315 |         onPositionChange: (position) => {
1316 |           state.propertyPanelPosition = position;
1317 |         },
1318 |         defaultTab: 'design',
1319 |         onSelectElement: (element) => {
1320 |           // When an element is selected from Components tree
1321 |           if (element.isConnected) {
1322 |             handleSelect(element, DEFAULT_MODIFIERS);
1323 |           }
1324 |         },
1325 |         onRequestClose: () => stop(),
1326 |       });
1327 | 
1328 |       // Clamp floating UI positions on window resize (session-only persistence)
1329 |       let uiResizeRafId: number | null = null;
1330 | 
1331 |       const clampFloatingUi = (): void => {
1332 |         const toolbarPos = state.toolbarPosition;
1333 |         const panelPos = state.propertyPanelPosition;
1334 | 
1335 |         if (state.toolbar && toolbarPos) {
1336 |           state.toolbar.setPosition(toolbarPos);
1337 |         }
1338 |         if (state.propertyPanel && panelPos) {
1339 |           state.propertyPanel.setPosition(panelPos);
1340 |         }
1341 |       };
1342 | 
1343 |       const onWindowResize = (): void => {
1344 |         if (!state.active) return;
1345 |         if (uiResizeRafId !== null) return;
1346 |         uiResizeRafId = window.requestAnimationFrame(() => {
1347 |           uiResizeRafId = null;
1348 |           clampFloatingUi();
1349 |         });
1350 |       };
1351 | 
1352 |       window.addEventListener('resize', onWindowResize, { passive: true });
1353 |       state.uiResizeCleanup = () => {
1354 |         window.removeEventListener('resize', onWindowResize);
1355 |         if (uiResizeRafId !== null) {
1356 |           window.cancelAnimationFrame(uiResizeRafId);
1357 |           uiResizeRafId = null;
1358 |         }
1359 |       };
1360 | 
1361 |       // Ensure restored positions are visible on first render
1362 |       clampFloatingUi();
1363 | 
1364 |       state.active = true;
1365 |       console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Started`);
1366 |     } catch (error) {
1367 |       // Cleanup on failure (reverse order)
1368 |       state.uiResizeCleanup?.();
1369 |       state.uiResizeCleanup = null;
1370 |       state.propertyPanel?.dispose();
1371 |       state.propertyPanel = null;
1372 |       state.tokensService?.dispose();
1373 |       state.tokensService = null;
1374 |       state.propsBridge?.dispose();
1375 |       state.propsBridge = null;
1376 |       state.breadcrumbs?.dispose();
1377 |       state.breadcrumbs = null;
1378 |       state.toolbar?.dispose();
1379 |       state.toolbar = null;
1380 |       state.eventController?.dispose();
1381 |       state.eventController = null;
1382 |       state.dragReorderController?.dispose();
1383 |       state.dragReorderController = null;
1384 |       state.handlesController?.dispose();
1385 |       state.handlesController = null;
1386 |       state.transactionManager?.dispose();
1387 |       state.transactionManager = null;
1388 |       state.positionTracker?.dispose();
1389 |       state.positionTracker = null;
1390 |       state.selectionEngine?.dispose();
1391 |       state.selectionEngine = null;
1392 |       state.perfHotkeyCleanup?.();
1393 |       state.perfHotkeyCleanup = null;
1394 |       state.perfMonitor?.dispose();
1395 |       state.perfMonitor = null;
1396 |       state.canvasOverlay?.dispose();
1397 |       state.canvasOverlay = null;
1398 |       state.shadowHost?.dispose();
1399 |       state.shadowHost = null;
1400 |       state.hoveredElement = null;
1401 |       state.selectedElement = null;
1402 |       state.applyingSnapshot = null;
1403 |       state.active = false;
1404 | 
1405 |       console.error(`${WEB_EDITOR_V2_LOG_PREFIX} Failed to start:`, error);
1406 |     }
1407 |   }
1408 | 
1409 |   /**
1410 |    * Stop the editor
1411 |    */
1412 |   function stop(): void {
1413 |     if (!state.active) {
1414 |       return;
1415 |     }
1416 | 
1417 |     state.active = false;
1418 | 
1419 |     // Cancel pending debounced broadcasts (Phase 1.4)
1420 |     if (txChangedBroadcastTimer !== null) {
1421 |       window.clearTimeout(txChangedBroadcastTimer);
1422 |       txChangedBroadcastTimer = null;
1423 |     }
1424 | 
1425 |     try {
1426 |       // Cleanup in reverse order of initialization
1427 | 
1428 |       // Commit any in-progress text edit before cleanup
1429 |       if (editSession) {
1430 |         commitEdit();
1431 |       }
1432 | 
1433 |       // Cleanup resize listener for floating UI
1434 |       state.uiResizeCleanup?.();
1435 |       state.uiResizeCleanup = null;
1436 | 
1437 |       // Cleanup Property Panel (Phase 3)
1438 |       state.propertyPanel?.dispose();
1439 |       state.propertyPanel = null;
1440 | 
1441 |       // Cleanup Design Tokens Service (Phase 5.3)
1442 |       state.tokensService?.dispose();
1443 |       state.tokensService = null;
1444 | 
1445 |       // Cleanup Props Bridge (Phase 7) - best effort cleanup
1446 |       void state.propsBridge?.cleanup();
1447 |       state.propsBridge = null;
1448 | 
1449 |       // Cleanup Breadcrumbs UI
1450 |       state.breadcrumbs?.dispose();
1451 |       state.breadcrumbs = null;
1452 | 
1453 |       // Cleanup Toolbar UI
1454 |       state.toolbar?.dispose();
1455 |       state.toolbar = null;
1456 | 
1457 |       // Cleanup Event Controller (stops event interception)
1458 |       state.eventController?.dispose();
1459 |       state.eventController = null;
1460 | 
1461 |       // Cleanup Drag Reorder Controller
1462 |       state.dragReorderController?.dispose();
1463 |       state.dragReorderController = null;
1464 | 
1465 |       // Cleanup Resize Handles Controller (Phase 4.9)
1466 |       state.handlesController?.dispose();
1467 |       state.handlesController = null;
1468 | 
1469 |       // Cleanup Execution Tracker (Phase 3.10)
1470 |       state.executionTracker?.dispose();
1471 |       state.executionTracker = null;
1472 | 
1473 |       // Cleanup HMR Consistency Verifier (Phase 4.8)
1474 |       state.hmrConsistencyVerifier?.dispose();
1475 |       state.hmrConsistencyVerifier = null;
1476 | 
1477 |       // Cleanup Transaction Manager (clears history)
1478 |       state.transactionManager?.dispose();
1479 |       state.transactionManager = null;
1480 | 
1481 |       // Cleanup Position Tracker (stops scroll/resize monitoring)
1482 |       state.positionTracker?.dispose();
1483 |       state.positionTracker = null;
1484 | 
1485 |       // Cleanup Selection Engine
1486 |       state.selectionEngine?.dispose();
1487 |       state.selectionEngine = null;
1488 | 
1489 |       // Cleanup Performance Monitor (Phase 5.3)
1490 |       state.perfHotkeyCleanup?.();
1491 |       state.perfHotkeyCleanup = null;
1492 |       state.perfMonitor?.dispose();
1493 |       state.perfMonitor = null;
1494 | 
1495 |       // Cleanup Canvas Overlay
1496 |       state.canvasOverlay?.dispose();
1497 |       state.canvasOverlay = null;
1498 | 
1499 |       // Cleanup Shadow DOM host
1500 |       state.shadowHost?.dispose();
1501 |       state.shadowHost = null;
1502 | 
1503 |       // Clear element references and apply state
1504 |       state.hoveredElement = null;
1505 |       state.selectedElement = null;
1506 |       state.applyingSnapshot = null;
1507 | 
1508 |       console.log(`${WEB_EDITOR_V2_LOG_PREFIX} Stopped`);
1509 |     } catch (error) {
1510 |       console.error(`${WEB_EDITOR_V2_LOG_PREFIX} Error during cleanup:`, error);
1511 | 
1512 |       // Force cleanup
1513 |       state.propertyPanel = null;
1514 |       state.propsBridge = null;
1515 |       state.breadcrumbs = null;
1516 |       state.toolbar = null;
1517 |       state.eventController = null;
1518 |       state.dragReorderController = null;
1519 |       state.handlesController = null;
1520 |       state.transactionManager = null;
1521 |       state.positionTracker = null;
1522 |       state.selectionEngine = null;
1523 |       state.perfHotkeyCleanup = null;
1524 |       state.perfMonitor = null;
1525 |       state.canvasOverlay = null;
1526 |       state.shadowHost = null;
1527 |       state.hoveredElement = null;
1528 |       state.selectedElement = null;
1529 |       state.applyingSnapshot = null;
1530 |     } finally {
1531 |       // Always broadcast clear state to sidepanel (removes chips)
1532 |       broadcastEditorCleared();
1533 |     }
1534 |   }
1535 | 
1536 |   /**
1537 |    * Toggle the editor on/off
1538 |    */
1539 |   function toggle(): boolean {
1540 |     if (state.active) {
1541 |       stop();
1542 |     } else {
1543 |       start();
1544 |     }
1545 |     return state.active;
1546 |   }
1547 | 
1548 |   /**
1549 |    * Get current editor state
1550 |    */
1551 |   function getState(): WebEditorState {
1552 |     return {
1553 |       active: state.active,
1554 |       version: WEB_EDITOR_V2_VERSION,
1555 |     };
1556 |   }
1557 | 
1558 |   return {
1559 |     start,
1560 |     stop,
1561 |     toggle,
1562 |     getState,
1563 |     revertElement,
1564 |     clearSelection,
1565 |   };
1566 | }
1567 | 
```
Page 46/60FirstPrevNextLast