# Project export: evolve(browser)

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: The self-modifying browser for maximizing productivity.
- Devpost: https://devpost.com/software/evolve-browser
- GitHub: https://github.com/adeng27/evolve-browser/
- Video: https://www.youtube.com/embed/tJDPK8Kg8XE?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([Y Combinator] Build an Iconic YC Company with AI (1st Place: Guaranteed YC interview 2nd Place: Guaranteed YC Office Hours 3rd Place: Guaranteed YC Office Hours))
- Team: 1 GitHub contributor(s) — Alastair Deng (13 commits)

## Devpost submission (written by the team)

### Inspiration

Web browsing is static. Browsers and websites treat all of their users almost exactly the same, but there isn't a one-size fits all for the billions who use the internet. If a user wants some feature added or changed (e.g. hiding all YouTube shorts when visiting the website to avoid distraction), they must either: (i) hope the browser/website developer can and are willing to make it for them, (ii) hope and trust someone made a Chrome extension for their exact use case (without charging an arm and a leg), or (iii) have the time and expertise to make it themself. We believe we need to stop constraining users with this one-size fits all mindset. As such, we built evolve(browser), a fork of Chromium (and Chrome Extension) which makes customizing your browsing experience as easy as sending a single message.

### What it does

evolve(browser) pairs an advanced coding agent with complete browser context. This AI agent codes extensions which are loaded directly into the user's browser in real-time. Here are some cool extensions we created for ourselves or that our friends requested using evolve(browser) (these all took only 1-2 prompts): Alastair's YouTube filter: When visiting YouTube.com, hide all YouTube shorts and all videos which have any League of Legends keywords in their title between 9 AM - 5 PM. Haley's Email Manager: Stanford students get a lot of spam (and are forced to use Outlook), Haley wanted an extension which will help her quickly delete all her spam emails. Antonio's Ad Blocker: Chrome banned uBlock Origin (Antonio's favorite ad blocker), but Antonio doesn't want to switch browsers. Antonio uses evolve to create a custom ad blocker for his most visited sites (and if he ever wants to extend its ad-blocking capabilities to new sites he can just tell evolve to update the extension).

### How we built it

Very high-level agent breakdown: A primary large model (GPT-5.2 or NVIDIA Nemotron Super 49B) performs the main reasoning, deciding whether to answer directly or call tools. Context tools enhance responses using graph-based RAG over the extension codebase and, when useful, live browser context retrieved via WebSocket. The knowledge graph is built offline by chunking the codebase, extracting entities and relationships with GPT-5-Nano, and generating embeddings with NVIDIA’s embedding model. When actions are required, coding tools (sandboxed terminal, linter, testing sandbox, and code editor) execute tasks, sometimes assisted by a smaller secondary LLM. The primary model synthesizes everything into the final response, which is streamed back to the chat and auto-reloaded browser extension. A secondary model extracts rules / memories from the overall conversation for future conversations about this extension.

### Challenges we ran into

Our fork of Chromium has limited functionality currently because build times take so long (we weren't able to iterate quickly) For the Chrome Extension, auto-loading / auto-reloading chrome extensions from extension code is guarded against by Chrome due to security considerations, so we created a hacky solution (utilizing Apple Events to insert JavaScript directly into Chrome).

### Accomplishments we're proud of

Neither of us are frontend or UI-focused, so we our proud of our (surprisingly) clean UX. Our hacky solutions

### What we learned

Chromium is very large

### What's next

Creating a deployable extension / browser.

## README (from the GitHub repository)

# evolve-browser

## INSTALLATION INSTRUCTIONS (for Chrome extension)

Get backend ready.
1. `cd backend`
2. `uv sync`
3. `uv run main.py`

Get extension ready.
1. `cd evolve-extension`
2. `npm install`
3. `npm run dev`
4. In Chrome, go to `chrome://extensions/`
5. Turn on Developer mode
6. Click `Load Unpacked`
7. Select the `dist` folder inside of `evolve-extension`

NOTE: One-click load for extensions does not work for non-Macs

Mac Instructions:
 - Go to Chrome. Click `View > Developer > Allow JavaScript from Apple Events`

Non-Mac Instructions:
 - Extensions are created inside of `./backend/demo_code`
 - Follow the `Load Unpacked` instructions from above to load in extensions.

## CHROMIUM FORK
The Chromium fork is located in the branch: `chromium`.

## Inspiration
Web browsing is static. Browsers and websites treat all of their users almost exactly the same, but there isn't a one-size fits all for the billions who use the internet. If a user wants some feature added or changed (e.g. hiding all YouTube shorts when visiting the website to avoid distraction), they must either: (i) hope the browser/website developer can and are willing to make it for them, (ii) hope and trust someone made a Chrome extension for their exact use case (without charging an arm and a leg), or (iii) have the time and expertise to make it themself.

We believe we need to stop constraining users with this one-size fits all mindset. As such, we built **evolve(browser)**, a fork of __Chromium__ (and __Chrome Extension__) which makes customizing your browsing experience as easy as sending a single message.

## What it does
**evolve(browser)** pairs an advanced coding agent with complete browser context. This AI agent codes extensions which are loaded directly into the user's browser in real-time. Here are some cool extensions we created for ourselves or that our friends requested using **evolve(browser)** (these all took only 1-2 prompts):

-  Alastair's YouTube filter: When visiting YouTube.com, hide all YouTube shorts and all videos which have any League of Legends keywords in their title between 9 AM - 5 PM.

![youtube_extension_gif](./media/youtube_extension.gif)

- Haley's Email Manager: Stanford students get a lot of spam (and are forced to use Outlook), Haley wanted an extension which will help her quickly delete all her spam emails.

![email_manager_gif](./media/email_manager_extension.gif)

- Antonio's Ad Blocker: Chrome banned uBlock Origin (Antonio's favorite ad blocker), but Antonio doesn't want to switch browsers. Antonio uses **evolve** to create a custom ad blocker for his most visited sites (and if he ever wants to extend its ad-blocking capabilities to new sites he can just tell **evolve** to update the extension).
![adblocker extension gif](./media/adblock_extension.gif)

## How we built it
![Agent Flow Diagram](./media/agent-flow-real.png)

Very high-level agent breakdown:
- A primary large model (GPT-5.2 or NVIDIA Nemotron Super 49B) performs the main reasoning, deciding whether to answer directly or call tools.
- Context tools enhance responses using graph-based RAG over the extension codebase and, when useful, live browser context retrieved via WebSocket.
- The knowledge graph is built offline by chunking the codebase, extracting entities and relationships with GPT-5-Nano, and generating embeddings with NVIDIA’s embedding model.
- When actions are required, coding tools (sandboxed terminal, linter, testing sandbox, and code editor) execute tasks, sometimes assisted by a smaller secondary LLM.
- The primary model synthesizes everything into the final response, which is streamed back to the chat and auto-reloaded browser extension.
- A secondary model extracts rules / memories from the overall conversation for future conversations about this extension.


## Detected evidence (automated analysis)

Indexed codebase: 35 recognized source files, 314 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- LangChain (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (45 of 45)

```
.gitignore
ARCHITECTURE.md
backend/.python-version
backend/main.py
backend/prompt.txt
backend/pyproject.toml
backend/README.md
backend/tests/test_graph_rag.py
backend/utils/agent.py
backend/utils/ai.py
backend/utils/companion.py
backend/utils/config.py
backend/utils/db.py
backend/utils/extension_validator.py
backend/utils/graph_rag.py
backend/utils/memory.py
backend/utils/prompts.py
backend/utils/tools.py
backend/uv.lock
evolve-extension/.gitignore
evolve-extension/manifest.config.ts
evolve-extension/package.json
evolve-extension/README.md
evolve-extension/src/background.ts
evolve-extension/src/components/HelloWorld.tsx
evolve-extension/src/content/highlighter.ts
evolve-extension/src/content/main.tsx
evolve-extension/src/content/views/App.css
evolve-extension/src/content/views/App.tsx
evolve-extension/src/popup/App.css
evolve-extension/src/popup/App.tsx
evolve-extension/src/popup/index.css
evolve-extension/src/popup/index.html
evolve-extension/src/popup/main.tsx
evolve-extension/src/shared/messages.ts
evolve-extension/src/sidepanel/App.css
evolve-extension/src/sidepanel/App.tsx
evolve-extension/src/sidepanel/index.css
evolve-extension/src/sidepanel/index.html
evolve-extension/src/sidepanel/main.tsx
evolve-extension/tsconfig.app.json
evolve-extension/tsconfig.json
evolve-extension/tsconfig.node.json
evolve-extension/vite.config.ts
README.md
```

### Dependencies

- backend/pyproject.toml: aiosqlite@>=0.22.1, dotenv@>=0.9.9, fastapi@>=0.128.7, langchain@>=1.2.10, langchain-core@>=1.2.12, langchain-nvidia-ai-endpoints@>=1.0.4, langchain-openai@>=1.1.9, langsmith@>=0.7.3, networkx@>=3.4, numpy@>=2.0, openai@>=2.20.0, pydantic@>=2.12.5, uvicorn@>=0.40.0, websockets@>=15.0
- evolve-extension/package.json: @crxjs/vite-plugin@^2.0.3, @types/chrome@^0.1.1, @types/node@^24.0.15, @types/react@^19.1.8, @types/react-dom@^19.1.6, @vitejs/plugin-react@^4.7.0, react@^19.1.0, react-dom@^19.1.0, react-markdown@^10.1.0, typescript@~5.8.3, vite@^7.0.5, vite-plugin-zip-pack@^1.2.4

### Recent commits (newest first)

- basics
- without me (readme update)
- basic readme
- mo money mo poroblems (5 nano)
- Merge branch 'main' into alastair
- now or never (nvidia open models)
- Added AI Helper context summarizer
- Merge branch 'main' into alastair
- untitled 05 (new prompt who dis)
- Merge branch 'main' of https://github.com/adeng27/evolve-browser
- Made the UI more slick
- Merge branch 'main' into alastair
- life of the party (memory)
- Fixed merge conflict
- Merge remote-tracking branch 'refs/remotes/origin/main'
- Added basic console context
- kryra (thinking)
- Merge branch 'main' into alastair
- sajed (one click load)
- All the context from elements and from tabs should work fine now

## Key source files (fetched from GitHub, selected and truncated for size)

### ARCHITECTURE.md

```markdown
# Evolve Browser — Agent Architecture

> An AI-powered browser that builds its own Chrome extensions. The agent reasons, writes code, searches semantically, reads your tabs, remembers your preferences, and installs extensions — all in a multi-turn conversational loop.

---

## 1. Full System Architecture

```mermaid
flowchart TD
    classDef openai fill:#10a37f,color:#fff,stroke:#0d8c6d
    classDef nvidia fill:#76b900,color:#fff,stroke:#5a8f00
    classDef browser fill:#4285f4,color:#fff,stroke:#3367d6
    classDef tool fill:#ff6d00,color:#fff,stroke:#e65100
    classDef memory fill:#ab47bc,color:#fff,stroke:#8e24aa
    classDef graphrag fill:#00bcd4,color:#fff,stroke:#0097a7
    classDef storage fill:#78909c,color:#fff,stroke:#546e7a
    classDef agent fill:#e91e63,color:#fff,stroke:#c2185b

    %% ─── BROWSER LAYER ───────────────────────────────────────────────
    subgraph CHROME ["🌐 Chrome Browser"]
        direction LR
        SP["Sidepanel Chat UI<br/><i>React + TypeScript</i>"]:::browser
        CS["Content Scripts<br/><i>DOM extraction · console capture</i>"]:::browser
        TABS["Open Browser Tabs<br/><i>tab metadata → agent context</i>"]:::browser
    end

    %% ─── COMMUNICATION ──────────────────────────────────────────────
    SP <-->|"WebSocket<br/>bidirectional JSON"| WS_EP
    CS <-->|"chrome.tabs.sendMessage"| SP

    %% ─── BACKEND ─────────────────────────────────────────────────────
    subgraph BACKEND ["⚙️ FastAPI Backend"]
        direction TB

        WS_EP["/ws/{project_id}<br/><i>WebSocket endpoint</i>"]
        REST["REST API<br/><i>projects · conversations · rules</i>"]

        %% ─── AGENT CORE ─────────────────────────────────────────────
        subgraph AGENT_CORE ["🤖 EvolveAgent — Agentic Loop"]
            direction TB
            ROUTER{"Provider<br/>Router"}:::agent
            PROMPT["System Prompt Builder<br/><i>+ active tabs + memory rules<br/>+ tool availability</i>"]:::agent
            STREAM["Streaming Tool Loop<br/><i>plan → act → observe → repeat</i>"]:::agent

            ROUTER --> PROMPT --> STREAM
        end

        %% ─── DUAL MODEL STACK ────────────────────────────────────────
        subgraph MODELS ["🧠 Dual-Provider Model Stack"]
            direction LR
            subgraph OAI ["OpenAI"]
                GPT5["<b>GPT-5</b><br/>Primary Agent<br/><i>reasoning · planning<br/>tool orchestration</i>"]:::openai
                GPT4OMINI["<b>GPT-4o-mini</b><br/>Secondary<br/><i>code edits · titles<br/>rules · entities</i>"]:::openai
                EMB_OAI["<b>text-embedding-3-small</b><br/>Embeddings"]:::openai
            end
            subgraph NV ["NVIDIA Nemotron"]
                NEM_SUPER["<b>Nemotron Super 49B</b><br/>Primary Agent<br/><i>reasoning · planning<br/>tool orchestration</i>"]:::nvidia
                NEM_NANO["<b>Nemotron Nano 8B</b><br/>Secondary<br/><i>code edits · titles<br/>rules · entities</i>"]:::nvidia
                EMB_NV["<b>NV-EmbedQA-E5-v5</b><br/>Embeddi
[truncated — 21694 more characters]
```

### backend/pyproject.toml

```
[project]
name = "backend"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "aiosqlite>=0.22.1",
    "dotenv>=0.9.9",
    "fastapi>=0.128.7",
    "langchain>=1.2.10",
    "langchain-core>=1.2.12",
    "langchain-nvidia-ai-endpoints>=1.0.4",
    "langchain-openai>=1.1.9",
    "langsmith>=0.7.3",
    "networkx>=3.4",
    "numpy>=2.0",
    "openai>=2.20.0",
    "pydantic>=2.12.5",
    "uvicorn>=0.40.0",
    "websockets>=15.0",
]

```

### evolve-extension/package.json

```
{
  "name": "evolve-extension",
  "type": "module",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "react-markdown": "^10.1.0"
  },
  "devDependencies": {
    "@crxjs/vite-plugin": "^2.0.3",
    "@types/chrome": "^0.1.1",
    "@types/node": "^24.0.15",
    "@types/react": "^19.1.8",
    "@types/react-dom": "^19.1.6",
    "@vitejs/plugin-react": "^4.7.0",
    "typescript": "~5.8.3",
    "vite": "^7.0.5",
    "vite-plugin-zip-pack": "^1.2.4"
  }
}

```

### backend/main.py

```python
import asyncio
import json
import logging
import shutil
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

from utils.agent import EvolveAgent
from utils.companion import load_extension_via_os
from utils.config import get_secondary_client, get_secondary_model
from utils.db import (
    create_conversation,
    create_project,
    delete_project,
    delete_rule,
    get_history,
    get_messages,
    get_rules,
    init_db,
    list_conversations as db_list_conversations,
    list_projects as db_list_projects,
    save_message,
    save_rules,
    update_conversation_title,
)
from utils.memory import extract_rules
from utils.tools import DEMO_CODE_BASE

logger = logging.getLogger(__name__)

agent = EvolveAgent()


async def generate_conversation_title(
    user_message: str, assistant_message: str, provider: str = "openai"
) -> str:
    """Generate a short conversation title from the first message exchange."""
    client = get_secondary_client(provider)
    model = get_secondary_model(provider)
    response = await client.chat.completions.create(
        model=model,
        messages=[
            {
                "role": "system",
                "content": (
                    "Generate a concise 3-6 word title for this conversation. "
                    "Return only the title text, nothing else. No quotes or punctuation at the end."
                ),
            },
            {
                "role": "user",
                "content": f"User: {user_message[:500]}\n\nAssistant: {assistant_message[:500]}",
            },
        ],
        max_completion_tokens=20,
        temperature=0.5,
    )
    return response.choices[0].message.content.strip()


@asynccontextmanager
async def lifespan(app: FastAPI):
    await init_db()
    DEMO_CODE_BASE.mkdir(parents=True, exist_ok=True)
    yield


app = FastAPI(lifespan=lifespan)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)


# --- Models ---


class CreateProjectRequest(BaseModel):
    name: str


class Project(BaseModel):
    id: str
    name: str
    created_at: str


class ChatRequest(BaseModel):
    query: str
    project_id: str
    conversation_id: str | None = None


class ChatResponse(BaseModel):
    message: str
    conversation_id: str


class Conversation(BaseModel):
    id: str
    title: str | None = None
    created_at: str


class Message(BaseModel):
    role: str
    content: str
    created_at: str


class Rule(BaseModel):
    id: str
    content: str
    created_at: str


# --- Project Routes ---


@app.post("/projects", response_model=Project)
async def create_project_route(request: CreateProjectRequest):
    project_id, created_at = await create_project(request.name)
    (DEMO_CODE_BASE / project_id).mkdir(parents=True, exist_ok=True)
    return Project(id=project_id, name=request.name, created_at=created_at)


@app.get("/projects", response_model=list[Project])
async def list_projects():
    rows = await db_list_projects()
    return [Project(**r) for r in rows]


@app.delete("/projects/{project_id}")
async def delete_project_route(project_id: str):
    deleted = await delete_project(project_id)
    if not deleted:
        raise HTTPException(status_code=404, detail="Project not found")
    workspace = DEMO_CODE_BASE / project_id
    if workspace.exists():
        shutil.rmtree(workspace)
    return {"ok": True}


# --- Extension Loading ---


@app.post("/api/load-extension/{project_id}")
async def api_load_extension(project_id: str):
    """Trigger OS automation to load the extension into Chrome."""
    project_dir = DEMO_CODE_BASE / project_id
    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project not found")
    manifest = project_dir / "manifest.json"
    if not manifest.exists():
        raise HTTPException(
            status_code=400,
            detail="No manifest.json found in the project workspace.",
        )
    extension_path = str(project_dir.resolve())
    result = await load_extension_via_os(extension_path)
    return result


# --- Chat Routes ---


@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
    if request.conversation_id:
        conv_id = request.conversation_id
    else:
        conv_id, _ = await create_conversation(request.project_id)

    await save_message(conv_id, "user", request.query)

    history = await get_history(conv_id)
    rule_rows = await get_rules(request.project_id)
    rules = [r["content"] for r in rule_rows]
    assistant_msg = await agent.get_chat_response(history, project_id=request.project_id, rules=rules)

    await save_message(conv_id, "assistant", assistant_msg)

    # Generate title for new conversations
    if not request.conversation_id:
        try:
            title = await generate_conversation_title(request.query, assistant_msg)
            await update_conversation_title(conv_id, title)
        except Exception:
            logger.exception("Failed to generate conversation title")

    return ChatResponse(message=assistant_msg, conversation_id=conv_id)


@app.websocket("/ws/{project_id}")
async def ws_chat(websocket: WebSocket, project_id: str):
    await websocket.accept()

    # Shared across all chat turns on this connection
    pending_tab_requests: dict[str, asyncio.Future] = {}
    # Queue for incoming FE messages (tab_content_response, etc.) while the
    # agent is streaming.  A background listener task fills this queue.
    incoming: asyncio.Queue[dict] = asyncio.Queue()

    async def _listen_for_responses():
        """Read WS messages and route them: chat messages go on `incoming`,
        tab_content_response resolves the matching Future directly."""
        try:
            while True:
                data = await websocket.receive_json()
         
[truncated — 6550 more characters]
```

### evolve-extension/src/popup/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### evolve-extension/src/sidepanel/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### evolve-extension/src/popup/App.tsx

```typescript
import { useState } from 'react'
import './App.css'

const API_URL = 'http://localhost:8000'

export default function App() {
  const [query, setQuery] = useState('')
  const [response, setResponse] = useState('')
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!query.trim()) return

    setLoading(true)
    setError('')
    setResponse('')

    try {
      const res = await fetch(`${API_URL}/chat`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query }),
      })

      if (!res.ok) {
        throw new Error(`Server error: ${res.status}`)
      }

      const data = await res.json()
      setResponse(data.message)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Something went wrong')
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="chat-container">
      <h1>AI Chat</h1>

      <form onSubmit={handleSubmit} className="chat-form">
        <textarea
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Ask anything..."
          rows={3}
          disabled={loading}
        />
        <button type="submit" disabled={loading || !query.trim()}>
          {loading ? 'Thinking...' : 'Send'}
        </button>
      </form>

      {error && <div className="error">{error}</div>}

      {response && (
        <div className="response">
          <h2>Response</h2>
          <p>{response}</p>
        </div>
      )}
    </div>
  )
}

```

### evolve-extension/src/content/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { setHoverHighlighterEnabled, startHoverHighlighter } from './highlighter'
import { MESSAGE_TYPES } from '../shared/messages'
import App from './views/App.tsx'

console.log('[Evolve] Hello world from content script!')

const CONSOLE_LEVELS = ['log', 'info', 'warn', 'error', 'debug'] as const
type ConsoleLevel = (typeof CONSOLE_LEVELS)[number]

const safeStringify = (value: unknown) => {
  const seen = new WeakSet()
  try {
    return JSON.stringify(value, (_key, val) => {
      if (typeof val === 'object' && val !== null) {
        if (seen.has(val)) return '[Circular]'
        seen.add(val)
      }
      if (val instanceof Error) {
        return {
          name: val.name,
          message: val.message,
          stack: val.stack,
        }
      }
      return val
    })
  } catch {
    return String(value)
  }
}

const formatConsoleArgs = (args: unknown[]) =>
  args
    .map((arg) => {
      if (typeof arg === 'string') return arg
      if (arg instanceof Error) return arg.stack ?? arg.message
      return safeStringify(arg)
    })
    .join(' ')

const sendConsoleLog = (level: ConsoleLevel, args: unknown[]) => {
  const message = formatConsoleArgs(args)
  void chrome.runtime.sendMessage({
    type: MESSAGE_TYPES.storeConsoleLog,
    payload: {
      level,
      timestamp: Date.now(),
      message,
      url: window.location.href,
    },
  })
}

const hookConsole = () => {
  const win = window as unknown as { __evolveConsoleHooked?: boolean }
  if (win.__evolveConsoleHooked) return
  win.__evolveConsoleHooked = true

  CONSOLE_LEVELS.forEach((level) => {
    const consoleRef = console as unknown as Record<string, (...args: unknown[]) => void>
    const original = consoleRef[level].bind(console)
    consoleRef[level] = (...args: unknown[]) => {
      sendConsoleLog(level, args)
      original(...args)
    }
  })

  window.addEventListener('error', (event) => {
    sendConsoleLog('error', [event.message, event.error])
  })

  window.addEventListener('unhandledrejection', (event) => {
    sendConsoleLog('error', ['Unhandled promise rejection', event.reason])
  })
}

hookConsole()

startHoverHighlighter()

const MAX_CONTEXT_HTML_CHARS = 1000

const sanitizeElementTree = (root: Element) => {
  const selectors = [
    'script',
    'style',
    'noscript',
    'svg',
    'header',
    'nav',
    'footer',
    'iframe',
    'canvas',
    'template',
  ]

  selectors.forEach((selector) => {
    root.querySelectorAll(selector).forEach((el) => el.remove())
  })

  root.querySelectorAll('[hidden], [aria-hidden="true"]').forEach((el) => el.remove())

  root.querySelectorAll<HTMLElement>('[style]').forEach((el) => {
    const style = el.getAttribute('style')?.toLowerCase() || ''
    if (style.includes('display:none') || style.includes('visibility:hidden')) {
      el.remove()
    }
  })
}

const pruneLargeHtml = (root: Element) => {
  const selectors = [
    'script',
    'style',
    'noscript',
    'svg',
    'header',
    'nav',
    'footer',
    'iframe',
    'canvas',
    'template',
    'meta',
    'link',
    'form',
    'input',
    'textarea',
    'select',
    'button',
    'video',
    'audio',
    'picture',
    'source',
    'object',
    'embed',
  ]

  selectors.forEach((selector) => {
    root.querySelectorAll(selector).forEach((el) => el.remove())
  })

  root.querySelectorAll<HTMLElement>('*').forEach((el) => {
    Array.from(el.attributes).forEach((attr) => {
      const name = attr.name.toLowerCase()
      if (
        name.startsWith('on') ||
        name === 'style' ||
        name === 'srcset' ||
        name === 'src' ||
        name === 'href' ||
        name === 'integrity' ||
        name === 'nonce' ||
        name === 'crossorigin' ||
        name === 'referrerpolicy'
      ) {
        el.removeAttribute(attr.name)
      }
    })
  })
}

// Listen for sidepanel lifecycle messages to toggle the highlighter
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  if (message?.type === MESSAGE_TYPES.sidepanelOpen) {
    setHoverHighlighterEnabled(true)
    return
  }

  if (message?.type === MESSAGE_TYPES.sidepanelClose) {
    setHoverHighlighterEnabled(false)
    return
  }

  if (message?.type === MESSAGE_TYPES.getPageContent) {
    // Return full page content — raw HTML or visible text depending on the flag.
    // Pagination / chunking is handled server-side.
    const raw = message.includeHtml
      ? (document.body?.innerHTML ?? '')
      : (document.body?.innerText ?? '')
    sendResponse({ content: raw })
    return // synchronous response
  }

  if (message?.type === MESSAGE_TYPES.getElementHtml) {
    const selector = message?.selector
    const stripHighlighting = (root: Element) => {
      const idsToRemove = [
        'evolve-hover-highlight-style',
        'evolve-hover-highlight-overlay',
        'evolve-hover-highlight-label',
        'evolve-click-highlight-overlay',
        'evolve-click-highlight-label',
      ]

      if (root.classList.contains('evolve-clicked-highlight')) {
        root.classList.remove('evolve-clicked-highlight')
      }

      root.querySelectorAll('.evolve-clicked-highlight').forEach((el) => {
        el.classList.remove('evolve-clicked-highlight')
      })

      idsToRemove.forEach((id) => {
        root.querySelectorAll(`#${id}`).forEach((el) => el.remove())
      })
    }
    if (typeof selector === 'string' && selector.length > 0) {
      const el = document.querySelector(selector)
      if (!el) {
        sendResponse({ html: '' })
        return true
      }
      const clone = el.cloneNode(true) as Element
      stripHighlighting(clone)
      sanitizeElementTree(clone)
      let html = clone.outerHTML
      if (html.length > MAX_CONTEXT_HTML_CHARS) {
        pruneLargeHtml(clone)
        html = clone.outerHTML
        if (html.length > MAX_CONTEXT_HTML_CHARS) {
          html = html.slice(0, MAX_CONTEXT_HTML_CHAR
[truncated — 558 more characters]
```

### evolve-extension/src/content/views/App.tsx

```typescript
function App() {
  return null
}

export default App

```

### evolve-extension/vite.config.ts

```typescript
import path from 'node:path'
import { crx } from '@crxjs/vite-plugin'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import zip from 'vite-plugin-zip-pack'
import manifest from './manifest.config.js'
import { name, version } from './package.json'

export default defineConfig({
  resolve: {
    alias: {
      '@': `${path.resolve(__dirname, 'src')}`,
    },
  },
  plugins: [
    react(),
    crx({ manifest }),
    zip({ outDir: 'release', outFileName: `${name}-${version}.zip` }),
  ],
  server: {
    cors: {
      origin: [
        /chrome-extension:\/\//,
      ],
    },
  },
})

```

[23 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]