[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: Report a bug\ntitle: '[Bug] '\nlabels: ['bug']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for your interest in improving the project! Before submitting, please read our guidelines.\n        感谢您对改进项目的兴趣！提交前请阅读我们的指南。\n\n        - [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)\n        - [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)\n\n  - type: textarea\n    id: description\n    attributes:\n      label: What happened?\n      placeholder: Describe the bug and expected behavior\n    validations:\n      required: true\n\n  - type: textarea\n    id: code\n    attributes:\n      label: Code\n      render: typescript\n      placeholder: Minimal reproduction code\n    validations:\n      required: false\n\n  - type: input\n    id: browser\n    attributes:\n      label: Browser\n      placeholder: 'Chrome 120, Firefox 119, etc.'\n    validations:\n      required: false\n\n  - type: input\n    id: version\n    attributes:\n      label: version\n      placeholder: '0.0.0'\n    validations:\n      required: false\n\n  - type: checkboxes\n    id: community\n    attributes:\n      label: Community Communication / 社区沟通\n      description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通\n      options:\n        - label: I will be polite and respectful. / 我会保持礼貌与尊重。\n          required: true\n        - label: I will share constructive, actionable suggestions. / 我会提供建设性、可行动的建议。\n          required: true\n        - label: I have read the Code of Conduct. / 我已阅读行为准则。\n          required: true\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Questions & Ideas / 问题与想法（Discussions）\n    url: https://github.com/alibaba/page-agent/discussions\n    about: Use Discussions for Q&A and ideation. 使用 Discussions 进行问答与想法交流。\n  - name: Security Report / 安全问题报告\n    url: https://github.com/alibaba/page-agent/security/policy\n    about: Report security vulnerabilities responsibly. 通过安全页面报告漏洞。\n  - name: Contributing Guide / 贡献指南\n    url: https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md\n    about: How to contribute code and ideas. 如何进行贡献与提交代码。\n  - name: Code of Conduct / 行为准则\n    url: https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md\n    about: Community expectations and standards. 社区行为期望与标准。\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request\ndescription: Suggest a feature\ntitle: '[Feature] '\nlabels: ['enhancement']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for your interest in improving the project! Before submitting, please read our guidelines.\n        感谢您对改进项目的兴趣！提交前请阅读我们的指南。\n\n        - [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)\n        - [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Feature Description / 功能描述\n      description: Describe the problem, solution, and any API changes. / 描述问题、解决方案以及 API 变更。\n      placeholder: |\n        **Problem**:\n        What problem does this solve?\n\n        **Solution**:\n        How should this work?\n\n        **Proposed API**:\n        ```typescript\n        // code here\n        ```\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: community\n    attributes:\n      label: Community Communication / 社区沟通\n      description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通\n      options:\n        - label: I will be polite and respectful. / 我会保持礼貌与尊重。\n          required: true\n        - label: I will share constructive, actionable suggestions. / 我会提供建设性、可行动的建议。\n          required: true\n        - label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。\n          required: true\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## What\n\nBrief description of changes.\n\n## Type\n\n- [ ] Bug fix\n- [ ] Feature / Improvement\n- [ ] Refactor\n- [ ] Documentation\n- [ ] Website\n- [ ] Demo / Testing\n- [ ] Breaking change\n\n## Testing\n\n- [ ] Tested in modern browsers\n- [ ] No console errors\n- [ ] Types/doc added\n\nCloses #(issue)\n\n## Requirements / 要求\n\n- [ ] I have read and follow the [Code of Conduct](../docs/CODE_OF_CONDUCT.md) and [Contributing Guide](../CONTRIBUTING.md) . / 我已阅读并遵守行为准则。\n- [ ] This PR is NOT generated by a bot or AI agent acting autonomously. I have authored or meaningfully reviewed every change. / 此 PR 不是由 bot 或 AI 自主生成的，我已亲自编写或充分审查了每一处变更。\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: 'npm'\n    directory: '/'\n    schedule:\n      interval: 'weekly'\n    groups:\n      # 生产依赖 - 小版本更新\n      production-dependencies:\n        dependency-type: 'production'\n        update-types:\n          - 'minor'\n          - 'patch'\n\n      # 开发依赖 - 小版本更新\n      development-dependencies:\n        dependency-type: 'development'\n        update-types:\n          - 'minor'\n          - 'patch'\n\n      # Major 更新单独处理（不分组，需要人工审查）\n      # 安全更新也不分组，Dependabot 会自动优先创建独立 PR\n\n  - package-ecosystem: 'github-actions'\n    directory: '/'\n    schedule:\n      interval: 'weekly'\n    groups:\n      github-actions:\n        patterns:\n          - '*'\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "permissions:\n  contents: read\nname: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node-version: [24]\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: 'npm'\n\n      # test on default version of npm\n      # - 9.6~10.8 on node@20\n      # - 11.3~11.6 on node@24\n\n      - name: Node and NPM version\n        run: node --version && npm --version\n\n      - name: Install dependencies\n        run: npm install\n\n      - name: Lint\n        run: npx eslint . && npx prettier --check **/*.ts\n\n      - name: Build\n        run: npm run build\n"
  },
  {
    "path": ".github/workflows/deploy-demo.yml",
    "content": "name: Deploy Demo\n\non:\n  push:\n    branches: [main]\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pages: write\n      id-token: write\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          cache: 'npm'\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Build demo\n        run: npm run build:website\n\n      - name: Setup Pages\n        uses: actions/configure-pages@v5\n\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v4\n        with:\n          path: './packages/website/dist'\n\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n\npermissions:\n  id-token: write # Required for OIDC\n  contents: read\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          registry-url: 'https://registry.npmjs.org'\n\n      # Ensure npm 11.5.1 or later is installed\n      - name: Update npm\n        run: npm install -g npm@latest\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Build\n        run: npm run build:libs\n\n      - name: Publish all public packages\n        run: |\n          VERSION=${GITHUB_REF#refs/tags/v}\n          if [[ \"$VERSION\" == *\"-\"* ]]; then\n            # Prerelease version (e.g., 0.3.0-beta.1) -> extract tag name before the dot\n            TAG=$(echo \"$VERSION\" | sed 's/.*-\\([a-zA-Z]*\\).*/\\1/')\n            npm publish --workspaces --access public --tag \"$TAG\"\n          else\n            npm publish --workspaces --access public\n          fi\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\n# /lib\ndist-ssr\n*.local\n\n# Editor directories and files\n# .vscode/*\n# !.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n.qoder\n\n# env files\n.env\n.env.*\n\n# extension\n.output\n.wxt\n\n# AI\n.agent\n.claude\n.cursor\n.gemini\nCLAUDE.md"
  },
  {
    "path": ".husky/commit-msg",
    "content": "npx --no -- commitlint --edit $1\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "npx lint-staged --allow-empty"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n    \"recommendations\": [\"dbaeumer.vscode-eslint\", \"esbenp.prettier-vscode\"]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"cSpell.words\": [\n        \"contenteditable\",\n        \"deepseek\",\n        \"historychange\",\n        \"HITL\",\n        \"innerhtml\",\n        \"languagedetector\",\n        \"llms\",\n        \"magicui\",\n        \"npmmirror\",\n        \"onwarn\",\n        \"opensource\",\n        \"qwen\",\n        \"retryable\",\n        \"shadcn\",\n        \"sidepanel\",\n        \"statuschange\",\n        \"wouter\"\n    ],\n    \"files.exclude\": {\n        \"packages/*/node_modules\": true\n    },\n    \"markdownlint.config\": {\n        // \"comment\": \"Relaxed rules\",\n        \"default\": true,\n        \"whitespace\": false,\n        \"line_length\": false,\n        \"ul-indent\": false,\n        \"no-inline-html\": false,\n        \"no-bare-urls\": false,\n        \"fenced-code-language\": false,\n        \"first-line-h1\": false,\n        \"block-spacing\": false,\n        \"blanks-around-lists\": false,\n        \"ol-prefix\": false,\n        \"no-duplicate-heading\": false\n    }\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# Instructions for Coding Assistants\n\n## Project Overview\n\nThis is a **monorepo** with npm workspaces:\n\n- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm\n- **Extension** (`packages/extension/`) - Browser extension (WXT + React) 🚧 WIP\n- **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**\n\nInternal packages:\n\n- **Core** (`packages/core/`) - PageAgentCore without UI (npm: `@page-agent/core`)\n- **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model\n- **Page Controller** (`packages/page-controller/`) - DOM operations and visual feedback (SimulatorMask), independent of LLM\n- **UI** (`packages/ui/`) - Panel and i18n. Decoupled from PageAgent\n\n## Development Commands\n\n```bash\nnpm start                    # Start website dev server\nnpm run build                # Build all packages\nnpm run build:libs           # Build all libraries\nnpm run lint                 # ESLint with TypeScript strict rules\nnpm run zip -w @page-agent/ext # Zip the extension package\n```\n\n## Architecture\n\n### Monorepo Structure\n\nSimple monorepo solution: TypeScript references + Vite aliases. Update tsconfig and vite config when adding/removing packages.\n\n```\npackages/\n├── core/                    # npm: \"@page-agent/core\" ⭐ Core agent logic (headless)\n├── page-agent/              # npm: \"page-agent\" entry class (with UI + controller + demo builds)\n├── website/                 # @page-agent/website (private)\n├── llms/                    # @page-agent/llms\n├── extension/               # Browser extension (WXT + React)\n├── page-controller/         # @page-agent/page-controller\n└── ui/                      # @page-agent/ui\n```\n\n`workspaces` in `package.json` must be in topological order.\n\n### Module Boundaries\n\n- **Page Agent**: Main entry with UI. Extends PageAgentCore and adds Panel. Imports from `@page-agent/core`, `@page-agent/ui`\n- **Core**: PageAgentCore without UI. Imports from `@page-agent/llms`, `@page-agent/page-controller`\n- **LLMs**: LLM client with MacroToolInput contract. No dependency on page-agent\n- **UI**: Panel and i18n. Decoupled from PageAgent via PanelAgentAdapter interface\n- **Page Controller**: DOM operations with optional visual feedback (SimulatorMask). No LLM dependency. Enable mask via `enableMask: true` config\n\n### PageController ↔ PageAgent Communication\n\nAll communication is async and isolated:\n\n```typescript\n// PageAgent delegates DOM operations to PageController\nawait this.pageController.updateTree()\nawait this.pageController.clickElement(index)\nawait this.pageController.inputText(index, text)\nawait this.pageController.scroll({ down: true, numPages: 1 })\n\n// PageController exposes state via async methods\nconst simplifiedHTML = await this.pageController.getSimplifiedHTML()\nconst pageInfo = await this.pageController.getPageInfo()\n```\n\n### DOM Pipeline\n\n1. **DOM Extraction**: Live DOM → `FlatDomTree` via `page-controller/src/dom/dom_tree/`\n2. **Dehydration**: DOM tree → simplified text for LLM\n3. **LLM Processing**: AI returns action plans (page-agent)\n4. **Indexed Operations**: PageAgent calls PageController by element index\n\n## Key Files Reference\n\n### Page Agent (`packages/page-agent/`)\n\n| File               | Description                                  |\n| ------------------ | -------------------------------------------- |\n| `src/PageAgent.ts` | ⭐ Main class with UI, extends PageAgentCore |\n| `src/demo.ts`      | IIFE demo entry (auto-init with demo API)    |\n\n### Core (`packages/core/`)\n\n| File                   | Description                             |\n| ---------------------- | --------------------------------------- |\n| `src/PageAgentCore.ts` | ⭐ Core agent class without UI          |\n| `src/tools/`           | Tool definitions calling PageController |\n| `src/config/`          | Configuration types and constants       |\n| `src/prompts/`         | System prompt templates                 |\n\n### LLMs (`packages/llms/`)\n\n| File                  | Description                           |\n| --------------------- | ------------------------------------- |\n| `src/index.ts`        | ⭐ LLM class with retry logic         |\n| `src/types.ts`        | MacroToolInput, AgentBrain, LLMConfig |\n| `src/OpenAIClient.ts` | OpenAI-compatible client              |\n\n### Page Controller (`packages/page-controller/`)\n\n| File                        | Description                                                |\n| --------------------------- | ---------------------------------------------------------- |\n| `src/PageController.ts`     | ⭐ Main controller class with optional mask support        |\n| `src/SimulatorMask.ts`      | Visual overlay blocking user interaction during automation |\n| `src/actions.ts`            | Element interactions (click, input, scroll)                |\n| `src/dom/dom_tree/index.js` | Core DOM extraction engine                                 |\n\n## Adding New Features\n\n### New Agent Tool\n\n1. Implement in `packages/core/src/tools/index.ts`\n2. If tool needs DOM ops, add method to PageController first\n3. Tool calls `this.pageController.methodName()` for DOM interactions\n\n### New PageController Action\n\n1. Add implementation in `packages/page-controller/src/actions.ts`\n2. Expose via async method in `PageController.ts`\n3. Export from `packages/page-controller/src/index.ts`\n\n## Code Standards\n\n- Explicit typing for exported/public APIs\n- ESLint relaxes some unsafe rules for rapid iteration\n- Every change you make should not only implement the desired functionality but also improve the quality of the codebase\n- All code and comments must be in English.\n- Do not try to hide errors or risks. They are valuable feedbacks for developers and users. Make them visible and actionable.\n- Traceability and predictability is more important than success rate.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to PageAgent\n\n♥️ We welcome contributions from everyone.\n\n## 🚀 Quick Start\n\n### Development Setup\n\n1. **Prerequisites**\n    - `macOS` / `Linux` / `WSL`\n    - `node.js >= 20` with `npm >= 10`\n    - An editor that supports `ts/eslint/prettier`\n    - Make sure `eslint`, `prettier` and `commitlint` work well. Un-linted code won't pass the CI.\n\n2. **Setup**\n\n    ```bash\n    npm i\n    npm start          # Start demo and documentation site\n    npm run build      # Build libs and website\n    ```\n\n### Project Structure\n\nThis is a **monorepo** with npm workspaces containing **4 main packages**:\n\n- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm\n- **Core** (`packages/core/`) - Core agent logic without UI (npm: `@page-agent/core`)\n- **Extension** (`packages/extension/`) - Chrome extension for multi-page tasks and browser-level automation\n- **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`\n\n> We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.\n> \n> - When developing. Use alias so that we don't have to pre-build.\n> - When bundling. Use external and disable ts `paths` alias.\n> - When bundling `IIFE` and `Website`. Bundle everything together.\n\n## 🤝 How to Contribute\n\n### Reporting Issues\n\n- Use the GitHub issue tracker to report bugs or request features\n- Search existing issues before creating new ones\n- Provide clear reproduction steps for bugs\n- Include browser version and environment details\n\n### Code Contributions\n\n1. **Fork and Clone**\n\n    ```bash\n    git clone https://github.com/your-username/page-agent.git\n    cd page-agent\n    ```\n\n2. **Create Feature Branch**\n\n    ```bash\n    git checkout -b feat/your-feature-name\n    ```\n\n3. **Make Changes**\n    - Follow existing code style and patterns\n    - Add tests for new functionality\n    - Update documentation as needed\n\n4. **Test Your Changes**\n    - Build and lint everything.\n    - Test in our demo website\n    - Test it on other websites if applicable\n    - `@TODO: test suite`\n\n5. **Commit and Push**\n\n    ```bash\n    git add .\n    git commit -m \"feat: add awesome feature\"\n    git push origin feat/your-feature-name\n    ```\n\n6. **Create Pull Request**\n    - Provide clear description of changes\n    - Link related issues\n    - Include screenshots for UI changes\n\n## 📝 Code Style\n\n### General Guidelines\n\n- Use TypeScript for type safety\n- Follow existing naming conventions\n- Write meaningful commit messages\n- Keep functions small and focused\n- Add JSDoc for public APIs\n\n### Vibe Coding with AI\n\n> [Vibe coding](https://en.wikipedia.org/wiki/Vibe_coding)\n\n- Vibe coding is **RECOMMENDED** when maintaining **the demo, the website, the UI and tests**.\n    - We have a [website/AGENTS.md](packages/website/AGENTS.md) for that.\n- Vibe coding is **NOT** allowed for the core lib!!!\n- NEVER try to vibe coding the MV3 extension!!! It is HELL.\n- Review anything AI wrote before make a commit. You are the author of anything you commit. NOT AI.\n\nIf your AI assistant does not support [AGENTS.md](https://agents.md/). Add an alias for it:\n\n- claude-code (`CLAUDE.md`)\n\n    ```markdown\n    @AGENTS.md\n    ```\n\n- antigravity (`.agent/rules/alias.md`)\n\n    ```markdown\n    ---\n    trigger: always_on\n    ---\n\n    @../../AGENTS.md\n    ```\n\n## 🔧 Development Workflows\n\n### Test With Your Own LLM API\n\n- Create a `.env` file in the repo root with your LLM API config\n\n    ```env\n    LLM_MODEL_NAME=gpt-5.2\n    LLM_API_KEY=your-api-key\n    LLM_BASE_URL=https://api.your-llm-provider.com/v1\n    ```\n\n- **Ollama example** (tested on 0.15 + qwen3:14b, RTX3090 24GB):\n\n    ```env\n    LLM_BASE_URL=\"http://localhost:11434/v1\"\n    LLM_API_KEY=\"NA\"\n    LLM_MODEL_NAME=\"qwen3:14b\"\n    ```\n\n    > @see https://alibaba.github.io/page-agent/docs/features/models#ollama for configuration\n\n- **Restart the dev server** to load new env vars\n- If not provided, the demo will use the free testing proxy by default. By using it, you agree to its [terms](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md).\n\n### Extension Development\n\n```bash\n# make sure you ran `npm run build:libs` first\n# and every time you changed the core libs\nnpm run dev -w @page-agent/ext\nnpm run zip -w @page-agent/ext\n```\n\n- Update `packages/extension/docs/extension_api.md` for API integration details\n\n### Testing on Other Websites\n\n- Start and serve a local `iife` script\n\n    ```bash\n    npm run dev:demo # Serving IIFE with auto rebuild at http://localhost:5174/page-agent.demo.js\n    ```\n\n- Add a new bookmark\n\n    ```javascript\n    javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5174/page-agent.demo.js?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();\n    ```\n\n- Click the bookmark on any page to load Page-Agent\n\n> Warning: AK in your local `.env` will be inlined in the iife script. Be very careful when you distribute the script.\n\n### Adding Documentation\n\nAsk an AI to help you add documentation to the `website/` package. Follow the existing style.\n\n> Our AGENTS.md file and guardrails are designed for this purpose. But please be careful and review anything AI generated.\n\n## 🚫 What We Don't Accept\n\n- Breaking changes and large PRs without prior discussion\n- Heavy dependencies to core libs\n- Contributions without proper testing\n- Code that doesn't follow project conventions\n- Dependencies or code with licenses incompatible with MIT\n- Bot or AI-generated pull requests without meaningful human involvement\n\n## 📄 Legal\n\nBy contributing to this project, you agree that your contributions will be licensed under the MIT License.\n\n> CLA is optional.\n\n## 💬 Questions?\n\n- Open a GitHub issue for technical questions\n- Check existing documentation and issues first\n- Be respectful and constructive in discussions\n\nThank you for helping make PageAgent better! 🎉\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2026 SimonLuvRamen\nCopyright (c) 2026 Alibaba Group Holding Limited\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Page Agent\n\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png\">\n  <img alt=\"Page Agent Banner\" src=\"https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png\">\n</picture>\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-auto.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)\n\nThe GUI Agent Living in Your Webpage. Control web interfaces with natural language.\n\n🌐 **English** | [中文](./docs/README-zh.md)\n\n<a href=\"https://alibaba.github.io/page-agent/\" target=\"_blank\"><b>🚀 Demo</b></a> | <a href=\"https://alibaba.github.io/page-agent/docs/introduction/overview\" target=\"_blank\"><b>📖 Docs</b></a> | <a href=\"https://news.ycombinator.com/item?id=47264138\" target=\"_blank\"><b>📢 HN Discussion</b></a> | <a href=\"https://x.com/simonluvramen\" target=\"_blank\"><b>𝕏 Follow on X</b></a>\n\n<video id=\"demo-video\" src=\"https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2\" controls crossorigin muted></video>\n\n---\n\n## ✨ Features\n\n- **🎯 Easy integration**\n    - No need for `browser extension` / `python` / `headless browser`.\n    - Just in-page javascript. Everything happens in your web page.\n- **📖 Text-based DOM manipulation**\n    - No screenshots. No multi-modal LLMs or special permissions needed.\n- **🧠 Bring your own LLMs**\n- **🎨 Pretty UI with human-in-the-loop**\n- **🐙 Optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension) for multi-page tasks.**\n\n## 💡 Use Cases\n\n- **SaaS AI Copilot** — Ship an AI copilot in your product in lines of code. No backend rewrite.\n- **Smart Form Filling** — Turn 20-click workflows into one sentence. Perfect for ERP, CRM, and admin systems.\n- **Accessibility** — Make any web app accessible through natural language. Voice commands, screen readers, zero barrier.\n- **Multi-page Agent** — Extend your own agent's reach across browser tabs with the optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension).\n\n## 🚀 Quick Start\n\n### One-line integration\n\nFastest way to try PageAgent with our free Demo LLM:\n\n```html\n<script src=\"{URL}\" crossorigin=\"true\"></script>\n```\n\n> **⚠️ For technical evaluation only.** This demo CDN uses our free [testing LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api). By using it, you agree to its [terms](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md).\n\n| Mirrors | URL                                                                                |\n| ------- | ---------------------------------------------------------------------------------- |\n| Global  | https://cdn.jsdelivr.net/npm/page-agent@1.6.0/dist/iife/page-agent.demo.js         |\n| China   | https://registry.npmmirror.com/page-agent/1.6.0/files/dist/iife/page-agent.demo.js |\n\n### NPM Installation\n\n```bash\nnpm install page-agent\n```\n\n```javascript\nimport { PageAgent } from 'page-agent'\n\nconst agent = new PageAgent({\n    model: 'qwen3.5-plus',\n    baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n    apiKey: 'YOUR_API_KEY',\n    language: 'en-US',\n})\n\nawait agent.execute('Click the login button')\n```\n\nFor more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/docs/introduction/overview).\n\n## 🤝 Contributing\n\nWe welcome contributions from the community! Follow our instructions in [CONTRIBUTING.md](CONTRIBUTING.md) for setup and guidelines.\n\nPlease read [Code of Conduct](docs/CODE_OF_CONDUCT.md) before contributing.\n\nContributions generated entirely by bots or agents without substantial human involvement will not be accepted, and bot accounts may be blocked.\n\n## 👏 Acknowledgments\n\nThis project builds upon the excellent work of **[`browser-use`](https://github.com/browser-use/browser-use)**.\n\n`PageAgent` is designed for **client-side web enhancement**, not server-side automation.\n\n```\nDOM processing components and prompt are derived from browser-use:\n\nBrowser Use <https://github.com/browser-use/browser-use>\nCopyright (c) 2024 Gregor Zunic\nLicensed under the MIT License\n\nWe gratefully acknowledge the browser-use project and its contributors for their\nexcellent work on web automation and DOM interaction patterns that helped make\nthis project possible.\n\nThird-party dependencies and their licenses can be found in the package.json\nfile and in the node_modules directory after installation.\n```\n\n## 📄 License\n\n[MIT License](LICENSE)\n\n---\n\n**⭐ Star this repo if you find PageAgent helpful!**\n\n<a href=\"https://www.star-history.com/?repos=alibaba%2Fpage-agent&type=date\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/image?repos=alibaba/page-agent&type=date&theme=dark&legend=top-left&v=7\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7\" />\n </picture>\n</a>\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nWe provide security fixes on a best-effort basis for:\n\n| Version                                                   | Supported |\n| --------------------------------------------------------- | --------- |\n| `main`                                                    | Yes       |\n| Latest npm release of `page-agent` and workspace packages | Yes       |\n| Older releases                                            | No        |\n\nPlease upgrade to the latest release before reporting an issue against an older build.\n\n## Reporting a Vulnerability\n\nPlease do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.\n\nUse GitHub's private vulnerability reporting flow:\n\n- Open https://github.com/alibaba/page-agent/security/policy\n- Click `Report a vulnerability`\n\nIf private reporting is unavailable, open a minimal public issue only to request a private contact channel. Do not include exploit details.\n\n## What to Include\n\n- Affected package or feature\n- Exact version, commit, or build\n- Browser, OS, and runtime environment\n- Reproduction steps or a proof of concept\n- Expected impact\n\n## Scope\n\nWe prioritize reports that show a real security boundary failure, such as:\n\n- Unauthorized access to data, tokens, or extension capabilities\n- Bypassing explicit safety constraints\n- Sensitive data exposure caused by default behavior\n\nThe following usually do not qualify by themselves:\n\n- Unsafe custom integrations that ignore documented safeguards\n- Intentionally embedding secrets into client-side builds\n- Reports against unsupported older versions\n\n## Disclosure\n\nPlease avoid public disclosure until maintainers have had a reasonable chance to investigate and ship a fix.\n"
  },
  {
    "path": "docs/CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [1.6.0] - 2026-03-21\n\n### Features\n\n- **Beta MCP support** - New `@page-agent/mcp` package lets MCP clients such as Claude Desktop and Copilot control the browser through the Page Agent extension\n- **Better iframe handling** - Same-origin iframe elements are handled more reliably during DOM extraction and actions\n- **Extension history workflows** - Users can rerun past tasks, export history sessions as JSON, and approve MCP-triggered tasks before execution\n\n### Improvements\n\n- **Unified versioning across packages** - The extension now follows the root workspace version. Changelog entries are no longer split into a separate extension version section\n- **Configurable `stepDelay`** - Agent pacing between steps is now configurable via `stepDelay`\n- **Optional API key** - `apiKey` can now be omitted for compatible deployments that do not require one\n- **Optional named tool choice** - Tool invocation can disable named tool choice for providers that behave better without it\n- **Better rich-text input support** - Improved `contenteditable` handling with better event dispatching and `execCommand` fallback for more editors\n- **More flexible DOM extraction** - `includeAttributes` now supports wildcards, `contenteditable` is included by default, and heuristically interactive elements expose more useful attributes\n- **MiniMax model support** - Added MiniMax compatibility, with the default recommendation updated to `MiniMax-M2.7`\n\n### Bug Fixes\n\n- Fixed Safari issues when `requestIdleCallback` is unavailable\n- Avoid throwing when `webgl2` initialization fails\n- Improved OpenAI-compatible request patches for GPT-5.4 chat tools and MiniMax temperature/tool-call compatibility\n- Fixed several UI polish issues in the extension and website, including cursor and layout regressions\n\n## [1.5.1] - 2026-03-05\n\n### Breaking Changes\n\n- **`data-browser-use-ignore` → `data-page-agent-ignore`** - DOM ignore attribute renamed to match the project identity\n- **Config types restructured** - `PageAgentConfig` split into `AgentConfig` + `PageAgentCoreConfig`; config definitions moved from `config/index.ts` to `types.ts`\n- **Zod v3/v4 dual support** - Libraries now accept both `zod@^3.25` and `zod@^4.0` as peer dependencies\n\n### Features\n\n- **Experimental `llms.txt` support** - Agent can fetch and include a site's `llms.txt` in context. Enable via `experimentalLlmsTxt: true`\n\n### Improvements\n\n- Default `maxSteps` changed from 20 to 40 for better for complex tasks out of the box\n- Added 400ms wait between agent steps for page reactions\n- Increased click wait time (100ms → 200ms) for more reliable interactions\n- Removed debug `console.log` statements from scroll actions\n- Reset observations on new task start\n- Improved logging across packages\n\n### Extension v0.1.9\n\n> PageAgent 1.5.1\n\n- **Advanced config panel** - New collapsible section exposing Max Steps, System Instruction, and experimental `llms.txt` toggle\n- Streamlined User Auth Token description\n- Moved testing API notice below auth token section\n\n---\n\n## [1.4.0] - 2026-02-27\n\n### Features\n\n- Update Terms of Use and Privacy Policy\n- **Robust tool-call validation** - Action inputs are now validated against tool schemas individually, producing clear error messages (e.g. `Invalid input for action \"click_element_by_index\"`) instead of unreadable union parse errors\n- **Primitive action input coercion** - Small models that output `{\"click_element_by_index\": 2}` instead of `{\"click_element_by_index\": {\"index\": 2}}` are now auto-corrected using tool schemas\n- **Qwen model updates** - Added `qwen3.5-plus` as the default free testing model; disabled `enable_thinking` for Qwen models to avoid incompatible responses\n- **Updated default LLM endpoint** - Migrated demo and extension to a new testing endpoint with legacy endpoint auto-migration\n\n### Improvements\n\n- Unified zod imports (`* as z`) across all packages for consistency\n- Better Zod error formatting with `z.prettifyError()` in LLM client\n- Exported `InvokeError` and `InvokeErrorType` as values (not just types) from `@page-agent/llms`\n- Exported `SupportedLanguage` type from `@page-agent/core`\n\n### Extension v0.1.8\n\n- **Language setting** - Added language selector (System / English / 中文) in config panel\n- **UI makeover** - New empty state with breathing glow and typing animation; ai-motion glow overlay while running; refined focus styles\n- **Testing endpoint notice** - Shows terms of use notice when using the free testing API\n- **Legacy endpoint migration** - Auto-migrates old Supabase testing endpoint to new endpoint on startup\n\n---\n\n## [1.3.0] - 2026-02-13\n\n### Breaking Changes\n\n- **Lifecycle: `stop()` vs `dispose()`** - New `stop()` method to cancel the current task while keeping the agent reusable. `dispose()` is now terminal — a disposed agent cannot be reused. This affects both `PageAgentCore` and `PanelAgentAdapter`.\n\n### Features\n\n- **Panel action button** - The panel button now morphs between Stop (■) and Close (X) based on agent status\n- **Error history** - Errors and max-step failures are now recorded in `history` as `AgentErrorEvent`, making post-task analysis more complete\n\n### Bug Fixes\n\n- **AbortError handling** - `AbortError` is no longer retried by the LLM client, and shows a clean \"Task stopped\" message instead of a raw error stack\n\n---\n\n## [1.2.0] - 2026-02-11\n\n### Features\n\n- **Observe Phase** - Agent now observes the page before each action, improving decision accuracy on dynamic pages\n- **Better Abort Handling** - Improved `abortSignal` support for cleaner task cancellation\n\n### Improvements\n\n- Pruned system prompts for lower token usage and faster responses\n- Improved error handling during agent steps with better error messages\n- Zod tree-shaking for smaller bundle size\n\n### Bug Fixes\n\n- Fixed indentation lost in DOM extraction caused by `trimLines`\n- Fixed `gpt-5-mini` temperature configuration\n\n---\n\n## [1.1.0] - 2026-02-02\n\n### Features\n\n- **Custom System Prompt** - New `systemPrompt` config option to customize or extend the default system prompt\n- **Chrome Extension** - Extension with multi-tab control, main-world API with token auth, and tab lifecycle management\n\n### Improvements\n\n- Renamed `include_attributes` to `includeAttributes` in PageController config (camelCase consistency)\n- Lazy-loaded mask module for faster initialization\n- Better date formatting and error messages from LLM client\n- Added `rawRequest` to step history for easier debugging\n\n### Bug Fixes\n\n- Fixed CSP errors by using local SVGs for cursor mask instead of inline styles\n- Fixed `AbortError` being incorrectly retried and shown to users\n- Fixed mask not working correctly when starting a new task after stopping a previous one\n\n---\n\n## [1.0.0] - 2026-01-19\n\n### 🎉 First Stable Release\n\nPageAgent is now ready for production use. The API is stable and breaking changes will follow semantic versioning.\n\n### Features\n\n#### Core\n\n- **PageAgent** - Main entry class with built-in UI Panel\n- **PageAgentCore** - Headless agent class for custom UI or programmatic use\n- **DOM Analysis** - Text-based DOM extraction with high-intensity dehydration\n- **LLM Support** - Works with OpenAI, Claude, DeepSeek, Qwen, and other OpenAI-compatible APIs\n- **Tool System** - Built-in tools for click, input, scroll, select, and more\n- **Custom Tools** - Extend agent capabilities with your own tools (experimental)\n- **Lifecycle Hooks** - Hook into agent execution (experimental)\n- **Instructions System** - System-level and page-level instructions to guide agent behavior\n- **Data Masking** - Transform page content before sending to LLM\n\n#### Page Controller\n\n- **Element Interactions** - Click, input text, select options, scroll\n- **Visual Mask** - Blocks user interaction during automation\n- **DOM Tree Extraction** - Efficient page structure extraction for LLM consumption\n\n#### UI\n\n- **Interactive Panel** - Real-time task progress and agent thinking display\n- **Ask User Tool** - Agent can ask users for clarification\n- **i18n Support** - English and Chinese localization\n\n### Packages\n\n| Package                       | Description                        |\n| ----------------------------- | ---------------------------------- |\n| `page-agent`                  | Main entry with UI Panel           |\n| `@page-agent/core`            | Core agent logic without UI        |\n| `@page-agent/llms`            | LLM client with retry logic        |\n| `@page-agent/page-controller` | DOM operations and visual feedback |\n| `@page-agent/ui`              | Panel and i18n                     |\n\n### Known Limitations\n\n- Single-page application only (cannot navigate across pages)\n- No visual recognition (relies on DOM structure)\n- Limited interaction support (no hover, drag-drop, canvas operations)\n- See [Limitations](https://alibaba.github.io/page-agent/docs/introduction/limitations) for details\n\n### Acknowledgments\n\nThis project builds upon the excellent work of [browser-use](https://github.com/browser-use/browser-use). DOM processing components and prompts are adapted from browser-use (MIT License).\n"
  },
  {
    "path": "docs/CODE_OF_CONDUCT.md",
    "content": "# Alibaba Open Source Code of Conduct\n\n[¶中文版](#我们的保证)\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at opensource@alibaba-inc.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n---\n\n> Chinese Version\n> 《阿里巴巴开源行为准则》\n\n## 我们的保证\n\n为了促进一个开放透明且友好的环境，我们作为贡献者和维护者保证：无论年龄、种族、民族、性别认同和表达（方式）、体型、身体健全与否、经验水平、国籍、个人表现、宗教或性别取向，参与者在我们项目和社区中都免于骚扰。\n\n## 我们的标准\n\n有助于创造正面环境的行为包括但不限于：\n\n* 使用友好和包容性语言\n* 尊重不同的观点和经历\n* 耐心地接受建设性批评\n* 关注对社区最有利的事情\n* 友善对待其他社区成员\n\n身为参与者不能接受的行为包括但不限于：\n\n* 使用与性有关的言语或是图像，以及不受欢迎的性骚扰\n* 捣乱/煽动/造谣的行为或进行侮辱/贬损的评论，人身攻击及政治攻击\n* 公开或私下的骚扰\n* 未经许可地发布他人的个人资料，例如住址或是电子地址\n* 其他可以被合理地认定为不恰当或者违反职业操守的行为\n\n## 我们的责任\n\n项目维护者有责任为「可接受的行为」标准做出诠释，以及对已发生的不被接受的行为采取恰当且公平的纠正措施。\n\n项目维护者有权利及责任去删除、编辑、拒绝与本行为标准有所违背的评论 (comments)、提交 (commits)、代码、wiki 编辑、问题 (issues) 和其他贡献，以及项目维护者可暂时或永久性的禁止任何他们认为有不适当、威胁、冒犯、有害行为的贡献者。\n\n## 使用范围\n\n当一个人代表该项目或是其社区时，本行为标准适用于其项目平台和公共平台。\n\n代表项目或是社区的情况，举例来说包括使用官方项目的电子邮件地址、通过官方的社区媒体账号发布或线上或线下事件中担任指定代表。\n\n该项目的呈现方式可由其项目维护者进行进一步的定义及解释。\n\n## 强制执行\n\n可以通过 opensource@alibaba-inc.com 来联系项目团队来举报滥用、骚扰或其他不被接受的行为。\n\n任何维护团队认为有必要且适合的所有投诉都将进行审查及调查，并做出相对应的回应。项目小组有对事件回报者有保密的义务。具体执行的方针近一步细节可能会单独公布。\n\n没有切实地遵守或是执行本行为标准的项目维护人员，可能会因项目领导人或是其他成员的决定，暂时或是永久地取消其参与资格。\n\n## 来源\n\n本行为标准改编自[贡献者公约](https://www.contributor-covenant.org)，版本 1.4\n可在此查看[https://www.contributor-covenant.org/zh-cn/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/zh-cn/version/1/4/code-of-conduct.html)\n"
  },
  {
    "path": "docs/README-zh.md",
    "content": "# Page Agent\n\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png\">\n  <img alt=\"Page Agent Banner\" src=\"https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png\">\n</picture>\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-auto.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)\n\n纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。\n\n🌐 [English](../README.md) | **中文**\n\n<a href=\"https://alibaba.github.io/page-agent/\" target=\"_blank\"><b>🚀 Demo</b></a> | <a href=\"https://alibaba.github.io/page-agent/docs/introduction/overview\" target=\"_blank\"><b>📖 Docs</b></a> | <a href=\"https://news.ycombinator.com/item?id=47264138\" target=\"_blank\"><b>📢 HN Discussion</b></a> | <a href=\"https://x.com/simonluvramen\" target=\"_blank\"><b>𝕏 Follow on X</b></a>\n\n<video id=\"demo-video\" src=\"https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2\" controls crossorigin muted></video>\n\n---\n\n## ✨ Features\n\n- **🎯 轻松集成**\n    - 无需 `浏览器插件` / `Python` / `无头浏览器`。\n    - 纯页面内 JavaScript，一切都在你的网页中完成。\n- **📖 基于文本的 DOM 操作**\n    - 无需截图，无需多模态模型或特殊权限。\n- **🧠 用你自己的 LLM**\n- **🎨 精美 UI，支持人机协同**\n- **🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension)，支持跨页面任务。**\n\n## 💡 应用场景\n\n- **SaaS AI 副驾驶** — 几行代码为你的产品加上 AI 副驾驶，无需重写后端。\n- **智能表单填写** — 把 20 次点击变成一句话。ERP、CRM、管理后台的最佳拍档。\n- **无障碍增强** — 用自然语言让任何网页无障碍。语音指令、屏幕阅读器，零门槛。\n- **跨页面 Agent** — 通过可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension)，让你自己的 Agent 跨标签页工作。\n\n## 🚀 快速开始\n\n### 一行代码集成\n\n通过我们免费的 Demo LLM 快速体验 PageAgent：\n\n```html\n<script src=\"{URL}\" crossorigin=\"true\"></script>\n```\n\n> **⚠️ 仅用于技术评估。** 该 Demo CDN 使用了免费的[测试 LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api)，使用即表示您同意其[条款](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md)。\n\n| Mirrors | URL                                                                                |\n| ------- | ---------------------------------------------------------------------------------- |\n| Global  | https://cdn.jsdelivr.net/npm/page-agent@1.6.0/dist/iife/page-agent.demo.js         |\n| China   | https://registry.npmmirror.com/page-agent/1.6.0/files/dist/iife/page-agent.demo.js |\n\n### NPM 安装\n\n```bash\nnpm install page-agent\n```\n\n```javascript\nimport { PageAgent } from 'page-agent'\n\nconst agent = new PageAgent({\n    model: 'qwen3.5-plus',\n    baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n    apiKey: 'YOUR_API_KEY',\n    language: 'zh-CN',\n})\n\nawait agent.execute('点击登录按钮')\n```\n\n更多编程用法，请参阅 [📖 文档](https://alibaba.github.io/page-agent/docs/introduction/overview)。\n\n## 🤝 贡献\n\n欢迎社区贡献！请参阅 [CONTRIBUTING.md](../CONTRIBUTING.md) 了解安装与贡献指南。请在贡献前阅读[行为准则](CODE_OF_CONDUCT.md)。\n\n我们不接受未经实质性人类参与、完全由 Bot 或 Agent 自动生成的代码，机器人账号可能被禁止参与互动。\n\n## 👏 致谢\n\n本项目基于 **[`browser-use`](https://github.com/browser-use/browser-use)** 的优秀工作构建。\n\n`PageAgent` 专为**客户端网页增强**设计，不是服务端自动化工具。\n\n```\nDOM processing components and prompt are derived from browser-use:\n\nBrowser Use <https://github.com/browser-use/browser-use>\nCopyright (c) 2024 Gregor Zunic\nLicensed under the MIT License\n\nWe gratefully acknowledge the browser-use project and its contributors for their\nexcellent work on web automation and DOM interaction patterns that helped make\nthis project possible.\n\nThird-party dependencies and their licenses can be found in the package.json\nfile and in the node_modules directory after installation.\n```\n\n## 📄 许可证\n\n[MIT License](../LICENSE)\n\n---\n\n**⭐ 如果觉得 PageAgent 有用或有趣，请给项目点个星！**\n\n<a href=\"https://www.star-history.com/?repos=alibaba%2Fpage-agent&type=date\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/image?repos=alibaba/page-agent&type=date&theme=dark&legend=top-left&v=7\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7\" />\n </picture>\n</a>\n"
  },
  {
    "path": "docs/terms-and-privacy.md",
    "content": "# Terms of Use & Privacy\n\n**Last updated:** March 2026\n\n\"We\" in this document refers to the maintainers of the open-source Page Agent project (https://github.com/alibaba/page-agent). \"The software\" refers to Page Agent (the JavaScript library) and Page Agent Ext (the browser extension). This document covers the software itself and the testing API we provide — **not** any third-party product or service built with it.\n\n---\n\n## 1. Open Source Software Privacy\n\nThe software is a **client-side only** tool with a \"Bring Your Own Key\" (BYOK) architecture. The software itself does **not** include any backend service. The software does **not** collect or transmit any user data on its own, and we do **not** have access to your browsing activity, page content, or task instructions through the software.\n\nAll data transmission occurs **only** between your browser and the LLM provider you configure. You are in full control of which provider receives your data.\n\nThe project is open source under the [MIT License](https://github.com/alibaba/page-agent/blob/main/LICENSE) and can be audited at: https://github.com/alibaba/page-agent\n\n---\n\n## 2. Testing API and Demo Disclaimer & Terms of Use\n\nTo facilitate easy testing and technical evaluation, we provide a free testing LLM API. This API is used in the project homepage's live demo, the pre-built demo CDN bundles, and the browser extension's default configuration. Users may also use it independently for their own technical evaluation of the software.\n\nThis free testing API is provided **strictly for technical evaluation and R&D purposes only**. It must not be used in any production environment. By using this API, you agree to the following terms:\n\n- **Permitted Use Only**: This API must be used solely for technical evaluation of the software. Any other use — including integration into other products or services, unlawful activities, violation of the underlying LLM provider's usage policies, or automated scraping at scale — is strictly prohibited.\n\n- **No Sensitive Data**: You are strictly prohibited from inputting any Personal Identifiable Information (PII), confidential business data, financial/medical records, or using this agent on web pages containing such sensitive information.\n\n- **Data Processing**: We do not store or log your prompts, webpage data (HTML), or any submitted content, nor do we use such data for model training. All data is processed in-transit and immediately discarded. We perform in-memory request validation to prevent abuse of the testing API, and temporarily process IP addresses for rate-limiting purposes. No data from these processes is retained. Data is processed through Alibaba Cloud infrastructure, which is subject to its own privacy policy.\n\n- **Independent Infrastructure**: The software is completely frontend-based with a \"Bring Your Own Key\" (BYOK) architecture and **no built-in backend**. To facilitate easy testing, the maintainers have purchased public cloud services from Alibaba Cloud China ([aliyun.com](https://www.aliyun.com) Function Compute and BaiLian Qwen models). This project is not a product of, nor endorsed by, Alibaba Cloud.\n\n- **No Guaranteed Availability**: This testing API may be rate-limited, degraded, or discontinued at any time without prior notice.\n\n- **\"AS IS\" & Limitation of Liability**: This service is provided strictly on an \"AS IS\" and \"AS AVAILABLE\" basis, without any warranties. The maintainers bear no liability for any data loss, service interruption, or legal consequences arising from your use of this service.\n\n- **Recommendation for Real Usage**: For secure and continuous usage, we strongly advise using the BYOK mode with your own legally compliant commercial LLM API keys, or connecting to local, offline models (e.g., Ollama).\n\n**Note**: This free testing LLM API processes data via servers located in Mainland China. If you are located in a region with strict data localization laws (such as the EU/EEA), please do not use this API.\n\n**Age Requirement**: The software and testing API are not intended for use by individuals under the age of 13 (or the minimum age of digital consent in your jurisdiction).\n\n---\n\n## 3. Browser Extension (Page Agent Ext)\n\n### Data Processing\n\nThe extension performs DOM analysis and automation actions **locally in your browser**. Your browsing history, passwords, and form data are not accessed or collected by the extension developer.\n\nData is transmitted to external servers **only when you initiate an automation task**. When this occurs:\n\n- Your task instructions (natural language commands)\n- Simplified page structure (cleaned HTML) of all pages under the extension's control\n\nare sent to the LLM API endpoint configured in **your settings**.\n\n> **Note:** The HTML cleaning process simplifies page structure for AI readability but **does not guarantee removal of sensitive information** (e.g., visible text, form values, or personal data on the page). Please be mindful of the page content when initiating tasks.\n\n**If you configure a third-party LLM provider** (e.g., OpenAI, Anthropic, or others), data is sent directly to that provider. Their privacy policies apply.\n\n**If you use the testing API**, the terms in [Section 2](#2-testing-api-and-demo-disclaimer--terms-of-use) apply. By using the extension with the default testing API, you agree to those terms.\n\n### Data Storage\n\n- **Local storage only**: Your configuration (API endpoint, API key, model selection) is stored in your browser via `chrome.storage.local` (or equivalent browser storage APIs)\n- **No cloud sync**: Configuration is not synced to any external server\n- **No analytics**: The extension does not include any analytics or tracking code\n\n### Your Control\n\n- The extension is open source and can be audited by anyone\n- You choose which LLM provider to use\n- You may configure your own API endpoint at any time\n- You can clear all stored data by removing the extension\n\n---\n\n## Changes\n\nWe may update these terms at our discretion.\n\n## Contact\n\nhttps://github.com/alibaba/page-agent/issues\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import js from '@eslint/js'\nimport reactDom from 'eslint-plugin-react-dom'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport reactX from 'eslint-plugin-react-x'\nimport { defineConfig, globalIgnores } from 'eslint/config'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\nexport default defineConfig([\n\tglobalIgnores([\n\t\t'**/dist',\n\t\t'**/node_modules',\n\t\t'packages/*/src/components/ui',\n\t\t'**/.wxt',\n\t\t'**/.output',\n\t]),\n\t{\n\t\tplugins: {\n\t\t\t'react-hooks': reactHooks,\n\t\t},\n\t\trules: reactHooks.configs.recommended.rules,\n\t},\n\t{\n\t\tfiles: ['**/*.{ts,tsx}'],\n\t\textends: [\n\t\t\tjs.configs.recommended,\n\t\t\ttseslint.configs.recommended,\n\t\t\t// reactHooks.configs['recommended-latest'],\n\t\t\treactRefresh.configs.vite,\n\n\t\t\t// Remove tseslint.configs.recommended and replace with this\n\t\t\t...tseslint.configs.recommendedTypeChecked,\n\t\t\t// Alternatively, use this for stricter rules\n\t\t\t...tseslint.configs.strictTypeChecked,\n\t\t\t// Optionally, add this for stylistic rules\n\t\t\t...tseslint.configs.stylisticTypeChecked,\n\n\t\t\t// Enable lint rules for React\n\t\t\treactX.configs['recommended-typescript'],\n\t\t\t// Enable lint rules for React DOM\n\t\t\treactDom.configs.recommended,\n\t\t],\n\t\tlanguageOptions: {\n\t\t\tparserOptions: {\n\t\t\t\t// project: ['./tsconfig.json'],\n\t\t\t\t// project: ['./packages/*/tsconfig.json'],\n\t\t\t\t// tsconfigRootDir: import.meta.dirname,\n\t\t\t\tprojectService: true,\n\t\t\t},\n\t\t\tecmaVersion: 2020,\n\t\t\tglobals: globals.browser,\n\t\t},\n\t\trules: {\n\t\t\t// Add any additional rules here\n\t\t\t'@typescript-eslint/no-non-null-assertion': 'off',\n\t\t\t'@typescript-eslint/no-unsafe-assignment': 'off',\n\t\t\t'@typescript-eslint/no-unsafe-member-access': 'off',\n\t\t\t'@typescript-eslint/no-unsafe-call': 'off',\n\t\t\t'@typescript-eslint/no-explicit-any': 'off',\n\t\t\t'@typescript-eslint/no-empty-function': 'off',\n\t\t\t'@typescript-eslint/no-floating-promises': 'off',\n\t\t\t'@typescript-eslint/no-confusing-void-expression': 'off',\n\t\t\t'@typescript-eslint/no-unused-vars': 'off',\n\t\t\t'@typescript-eslint/no-inferrable-types': 'off',\n\t\t\t'@typescript-eslint/restrict-template-expressions': 'off',\n\t\t\t'@typescript-eslint/no-dynamic-delete': 'off',\n\t\t\t'@typescript-eslint/no-unnecessary-condition': 'off',\n\t\t\t'@typescript-eslint/prefer-nullish-coalescing': 'off',\n\t\t\t'@typescript-eslint/no-unnecessary-type-assertion': 'off',\n\t\t\t'@typescript-eslint/no-misused-promises': 'off',\n\t\t\t'@typescript-eslint/no-unsafe-argument': 'off',\n\t\t\t'@typescript-eslint/no-unsafe-return': 'off',\n\t\t\t'@typescript-eslint/restrict-plus-operands': 'off',\n\t\t\t'react-dom/no-missing-button-type': 'off',\n\t\t\t'react-x/no-nested-component-definitions': 'off',\n\t\t\t'@typescript-eslint/prefer-optional-chain': 'off',\n\t\t\t'@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',\n\t\t\t'@typescript-eslint/no-unnecessary-type-parameters': 'off',\n\n\t\t\t// 'require-await': 'off',\n\t\t\t'@typescript-eslint/require-await': 'off',\n\t\t},\n\t},\n])\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"root\",\n    \"private\": true,\n    \"version\": \"1.6.0\",\n    \"type\": \"module\",\n    \"workspaces\": [\n        \"packages/page-controller\",\n        \"packages/ui\",\n        \"packages/llms\",\n        \"packages/core\",\n        \"packages/page-agent\",\n        \"packages/mcp\",\n        \"packages/extension\",\n        \"packages/website\"\n    ],\n    \"description\": \"AI-powered UI agent for web applications\",\n    \"author\": \"Simon<gaomeng1900>\",\n    \"license\": \"MIT\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/alibaba/page-agent.git\"\n    },\n    \"homepage\": \"https://alibaba.github.io/page-agent/\",\n    \"engines\": {\n        \"node\": \"^20.19.0 || ^22.13.0 || >=24\"\n    },\n    \"scripts\": {\n        \"start\": \"npm run dev --workspace=@page-agent/website\",\n        \"dev:ext\": \"npm run dev -w @page-agent/ext\",\n        \"dev:demo\": \"npm run dev:demo --workspace=page-agent\",\n        \"build\": \"npm run build:libs && npm run build:website\",\n        \"build:libs\": \"npm run build --workspaces --if-present\",\n        \"build:website\": \"npm run build:website --workspace=@page-agent/website\",\n        \"build:ext\": \"npm run build:libs && npm run zip -w @page-agent/ext\",\n        \"version\": \"node scripts/sync-version.js\",\n        \"lint\": \"eslint .\",\n        \"cleanup\": \"rm -rf packages/*/dist\",\n        \"prepare\": \"husky\"\n    },\n    \"devDependencies\": {\n        \"@commitlint/cli\": \"^20.5.0\",\n        \"@commitlint/config-conventional\": \"^20.5.0\",\n        \"@eslint/js\": \"^9.39.2\",\n        \"@microsoft/api-extractor\": \"^7.57.7\",\n        \"@tailwindcss/vite\": \"^4.2.1\",\n        \"@trivago/prettier-plugin-sort-imports\": \"^6.0.2\",\n        \"@types/node\": \"^25.5.0\",\n        \"@vitejs/plugin-react-swc\": \"^4.3.0\",\n        \"chalk\": \"^5.6.2\",\n        \"concurrently\": \"^9.2.1\",\n        \"dotenv\": \"^17.3.1\",\n        \"eslint\": \"^9.39.2\",\n        \"eslint-config-prettier\": \"^10.1.8\",\n        \"eslint-plugin-react-dom\": \"^2.13.0\",\n        \"eslint-plugin-react-hooks\": \"^7.0.1\",\n        \"eslint-plugin-react-refresh\": \"^0.5.2\",\n        \"eslint-plugin-react-x\": \"^2.13.0\",\n        \"globals\": \"^17.4.0\",\n        \"husky\": \"^9.1.7\",\n        \"lint-staged\": \"^16.4.0\",\n        \"prettier\": \"^3.8.0\",\n        \"typescript\": \"^5.9.3\",\n        \"typescript-eslint\": \"^8.57.1\",\n        \"unplugin-dts\": \"^1.0.0-beta.6\",\n        \"vite\": \"^7.3.1\",\n        \"vite-plugin-css-injected-by-js\": \"^4.0.1\",\n        \"vite-bundle-analyzer\": \"^1.3.6\"\n    },\n    \"overrides\": {\n        \"typescript\": \"^5.9.3\"\n    },\n    \"lint-staged\": {\n        \"*.{js,ts,cjs,cts,mjs,mts}\": [\n            \"npx prettier --write --ignore-unknown\",\n            \"npx eslint --quiet\"\n        ],\n        \"*.{jsx,tsx}\": [\n            \"npx prettier --write --ignore-unknown\",\n            \"npx eslint --quiet\"\n        ],\n        \"*.css\": [\n            \"npx prettier --write --ignore-unknown\"\n        ]\n    },\n    \"commitlint\": {\n        \"extends\": [\n            \"@commitlint/config-conventional\"\n        ],\n        \"rules\": {\n            \"subject-case\": [\n                0,\n                \"never\"\n            ]\n        }\n    },\n    \"prettier\": {\n        \"singleQuote\": true,\n        \"semi\": false,\n        \"useTabs\": true,\n        \"printWidth\": 100,\n        \"trailingComma\": \"es5\",\n        \"plugins\": [\n            \"@trivago/prettier-plugin-sort-imports\"\n        ],\n        \"importOrder\": [\n            \"<THIRD_PARTY_MODULES>\",\n            \"^(@/).*(?<!css)$\",\n            \"^[./].*(?<!css)$\",\n            \".css$\"\n        ],\n        \"importOrderSeparation\": true,\n        \"importOrderSortSpecifiers\": true,\n        \"overrides\": [\n            {\n                \"files\": \"*.md\",\n                \"options\": {\n                    \"useTabs\": false,\n                    \"tabWidth\": 4\n                }\n            },\n            {\n                \"files\": \"*.json\",\n                \"options\": {\n                    \"useTabs\": false,\n                    \"tabWidth\": 4\n                }\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "packages/core/package.json",
    "content": "{\n    \"name\": \"@page-agent/core\",\n    \"private\": false,\n    \"version\": \"1.6.0\",\n    \"type\": \"module\",\n    \"main\": \"./dist/esm/page-agent-core.js\",\n    \"module\": \"./dist/esm/page-agent-core.js\",\n    \"types\": \"./dist/esm/PageAgentCore.d.ts\",\n    \"exports\": {\n        \".\": {\n            \"types\": \"./dist/esm/PageAgentCore.d.ts\",\n            \"import\": \"./dist/esm/page-agent-core.js\",\n            \"default\": \"./dist/esm/page-agent-core.js\"\n        }\n    },\n    \"files\": [\n        \"dist/\"\n    ],\n    \"description\": \"GUI agent for web applications - add intelligent automation to any webpage with a single script\",\n    \"keywords\": [\n        \"ai\",\n        \"automation\",\n        \"ui-agent\",\n        \"GUI-agent\",\n        \"browser-automation\",\n        \"web-agent\",\n        \"llm\",\n        \"dom-interaction\",\n        \"web-automation\",\n        \"GUI-simulation\"\n    ],\n    \"author\": \"Simon<gaomeng1900>\",\n    \"license\": \"MIT\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/alibaba/page-agent.git\"\n    },\n    \"homepage\": \"https://alibaba.github.io/page-agent/\",\n    \"scripts\": {\n        \"build\": \"vite build\",\n        \"dev:iife\": \"concurrently \\\"vite build --config vite.iife.config.js --watch\\\" \\\"npx serve dist/iife -p 5174\\\"\",\n        \"prepublishOnly\": \"node -e \\\"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\\\"\",\n        \"postpublish\": \"node -e \\\"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\\\"\"\n    },\n    \"dependencies\": {\n        \"chalk\": \"^5.6.2\",\n        \"@page-agent/llms\": \"1.6.0\",\n        \"@page-agent/page-controller\": \"1.6.0\"\n    },\n    \"peerDependencies\": {\n        \"zod\": \"^3.25.0 || ^4.0.0\"\n    },\n    \"devDependencies\": {\n        \"zod\": \"^4.3.5\"\n    }\n}\n"
  },
  {
    "path": "packages/core/src/PageAgentCore.ts",
    "content": "/**\n * Copyright (C) 2025 Alibaba Group Holding Limited\n * Copyright (C) 2026 SimonLuvRamen\n * All rights reserved.\n */\nimport { InvokeError, LLM, type Tool } from '@page-agent/llms'\nimport type { BrowserState, PageController } from '@page-agent/page-controller'\nimport chalk from 'chalk'\nimport * as z from 'zod/v4'\n\nimport SYSTEM_PROMPT from './prompts/system_prompt.md?raw'\nimport { tools } from './tools'\nimport type {\n\tAgentActivity,\n\tAgentConfig,\n\tAgentReflection,\n\tAgentStatus,\n\tAgentStepEvent,\n\tExecutionResult,\n\tHistoricalEvent,\n\tMacroToolInput,\n\tMacroToolResult,\n} from './types'\nimport { assert, fetchLlmsTxt, normalizeResponse, uid, waitFor } from './utils'\n\nexport { tool, type PageAgentTool } from './tools'\nexport type * from './types'\n\nexport type PageAgentCoreConfig = AgentConfig & { pageController: PageController }\n\n/**\n * AI agent for browser automation.\n *\n * @remarks\n * ## Re-act Agent Loop\n * - step\n *    - observe (gather information about current environment and context)\n *    - think (LLM calling)\n *      - reflection (evaluate history, generate memory, short-term planning)\n *      - action (give the action to approach the next goal)\n *    - act (execute the action)\n * - loop\n *\n * ## Event System\n * - `statuschange` - Agent status transitions (idle → running → completed/error)\n * - `historychange` - History events updated (persistent, part of agent memory)\n * - `activity` - Real-time activity feedback (transient, for UI only)\n * - `dispose` - Agent cleanup triggered\n *\n * ## Information Streams\n * 1. **History Events** (`history` array)\n *    - Persistent event stream that forms agent's memory\n *    - Included in LLM context across steps\n *    - Types: steps, observations, user takeovers, llm errors\n *\n * 2. **Activity Events** (via `activity` event)\n *    - Transient UI feedback during task execution\n *    - NOT included in LLM context\n *    - Types: thinking, executing, executed, retrying, error\n */\nexport class PageAgentCore extends EventTarget {\n\treadonly id = uid()\n\treadonly config: PageAgentCoreConfig & { maxSteps: number }\n\treadonly tools: typeof tools\n\t/** PageController for DOM operations */\n\treadonly pageController: PageController\n\n\ttask = ''\n\ttaskId = ''\n\t/** History events */\n\thistory: HistoricalEvent[] = []\n\t/** Whether this agent has been disposed */\n\tdisposed = false\n\n\t/**\n\t * Callback for when agent needs user input (ask_user tool)\n\t * If not set, ask_user tool will be disabled\n\t * @example onAskUser: (q) => window.prompt(q) || ''\n\t */\n\tonAskUser?: (question: string) => Promise<string>\n\n\t#status: AgentStatus = 'idle'\n\t#llm: LLM\n\t#abortController = new AbortController()\n\t#observations: string[] = []\n\n\t/** internal states during a single task execution */\n\t#states = {\n\t\t/** Accumulated wait time in seconds */\n\t\ttotalWaitTime: 0,\n\t\t/** For detecting navigation */\n\t\tlastURL: '',\n\t\t/** Browser state */\n\t\tbrowserState: null as BrowserState | null,\n\t}\n\n\tconstructor(config: PageAgentCoreConfig) {\n\t\tsuper()\n\n\t\tthis.config = { ...config, maxSteps: config.maxSteps ?? 40 }\n\n\t\tthis.#llm = new LLM(this.config)\n\t\tthis.tools = new Map(tools)\n\t\tthis.pageController = config.pageController\n\n\t\t// Listen to LLM retry events\n\t\tthis.#llm.addEventListener('retry', (e) => {\n\t\t\tconst { attempt, maxAttempts } = (e as CustomEvent).detail\n\t\t\tthis.#emitActivity({ type: 'retrying', attempt, maxAttempts })\n\t\t\t// Also push to history for panel rendering\n\t\t\tthis.history.push({\n\t\t\t\ttype: 'retry',\n\t\t\t\tmessage: `LLM retry attempt ${attempt} of ${maxAttempts}`,\n\t\t\t\tattempt,\n\t\t\t\tmaxAttempts,\n\t\t\t})\n\t\t\tthis.#emitHistoryChange()\n\t\t})\n\t\tthis.#llm.addEventListener('error', (e) => {\n\t\t\tconst error = (e as CustomEvent).detail.error as Error | InvokeError\n\t\t\tif ((error as any)?.rawError?.name === 'AbortError') return\n\t\t\tconst message = String(error)\n\t\t\tthis.#emitActivity({ type: 'error', message })\n\t\t\t// Also push to history for panel rendering\n\t\t\tthis.history.push({\n\t\t\t\ttype: 'error',\n\t\t\t\tmessage,\n\t\t\t\trawResponse: (error as InvokeError).rawResponse,\n\t\t\t})\n\t\t\tthis.#emitHistoryChange()\n\t\t})\n\n\t\tif (this.config.customTools) {\n\t\t\tfor (const [name, tool] of Object.entries(this.config.customTools)) {\n\t\t\t\tif (tool === null) {\n\t\t\t\t\tthis.tools.delete(name)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tthis.tools.set(name, tool)\n\t\t\t}\n\t\t}\n\n\t\tif (!this.config.experimentalScriptExecutionTool) {\n\t\t\tthis.tools.delete('execute_javascript')\n\t\t}\n\t}\n\n\t/** Get current agent status */\n\tget status(): AgentStatus {\n\t\treturn this.#status\n\t}\n\n\t/** Emit statuschange event */\n\t#emitStatusChange(): void {\n\t\tthis.dispatchEvent(new Event('statuschange'))\n\t}\n\n\t/** Emit historychange event */\n\t#emitHistoryChange(): void {\n\t\tthis.dispatchEvent(new Event('historychange'))\n\t}\n\n\t/**\n\t * Emit activity event - for transient UI feedback\n\t * @param activity - Current agent activity\n\t */\n\t#emitActivity(activity: AgentActivity): void {\n\t\tthis.dispatchEvent(new CustomEvent('activity', { detail: activity }))\n\t}\n\n\t/** Update status and emit event */\n\t#setStatus(status: AgentStatus): void {\n\t\tif (this.#status !== status) {\n\t\t\tthis.#status = status\n\t\t\tthis.#emitStatusChange()\n\t\t}\n\t}\n\n\t/**\n\t * Push an observation message to the history event stream.\n\t * This will be visible in <agent_history> and remain persistent in memory across steps.\n\t * @experimental @internal\n\t * @note history change will be emitted before next step starts\n\t */\n\tpushObservation(content: string): void {\n\t\tthis.#observations.push(content)\n\t}\n\n\t/** Stop the current task. Agent remains reusable. */\n\tstop() {\n\t\tthis.pageController.cleanUpHighlights()\n\t\tthis.pageController.hideMask()\n\t\tthis.#abortController.abort()\n\t}\n\n\tasync execute(task: string): Promise<ExecutionResult> {\n\t\tif (this.disposed) throw new Error('PageAgent has been disposed. Create a new instance.')\n\t\tif (!task) throw new Error('Task is required')\n\t\tthis.task = task\n\t\tthis.taskId = uid()\n\n\t\t// Disable ask_user tool if onAskUser is not set\n\t\tif (!this.onAskUser) {\n\t\t\tthis.tools.delete('ask_user')\n\t\t}\n\n\t\tconst onBeforeStep = this.config.onBeforeStep\n\t\tconst onAfterStep = this.config.onAfterStep\n\t\tconst onBeforeTask = this.config.onBeforeTask\n\t\tconst onAfterTask = this.config.onAfterTask\n\n\t\tawait onBeforeTask?.(this)\n\n\t\t// Show mask\n\t\tawait this.pageController.showMask()\n\n\t\tif (this.#abortController) {\n\t\t\tthis.#abortController.abort()\n\t\t\tthis.#abortController = new AbortController()\n\t\t}\n\n\t\tthis.history = []\n\t\tthis.#setStatus('running')\n\t\tthis.#emitHistoryChange()\n\t\tthis.#observations = []\n\n\t\t// Reset internal states\n\t\tthis.#states = { totalWaitTime: 0, lastURL: '', browserState: null }\n\n\t\tlet step = 0\n\n\t\twhile (true) {\n\t\t\ttry {\n\t\t\t\tconsole.group(`step: ${step}`)\n\n\t\t\t\tawait onBeforeStep?.(this, step)\n\n\t\t\t\t// observe\n\n\t\t\t\tconsole.log(chalk.blue.bold('👀 Observing...'))\n\n\t\t\t\tthis.#states.browserState = await this.pageController.getBrowserState()\n\t\t\t\tawait this.#handleObservations(step)\n\n\t\t\t\t// assemble prompts\n\n\t\t\t\tconst messages = [\n\t\t\t\t\t{ role: 'system' as const, content: this.#getSystemPrompt() },\n\t\t\t\t\t{ role: 'user' as const, content: await this.#assembleUserPrompt() },\n\t\t\t\t]\n\n\t\t\t\tconst macroTool = { AgentOutput: this.#packMacroTool() }\n\n\t\t\t\t// invoke LLM\n\n\t\t\t\tconsole.log(chalk.blue.bold('🧠 Thinking...'))\n\t\t\t\tthis.#emitActivity({ type: 'thinking' })\n\n\t\t\t\tconst result = await this.#llm.invoke(messages, macroTool, this.#abortController.signal, {\n\t\t\t\t\ttoolChoiceName: 'AgentOutput',\n\t\t\t\t\tnormalizeResponse: (res) => normalizeResponse(res, this.tools),\n\t\t\t\t})\n\n\t\t\t\t// assemble history\n\n\t\t\t\tconst macroResult = result.toolResult as MacroToolResult\n\t\t\t\tconst input = macroResult.input\n\t\t\t\tconst output = macroResult.output\n\t\t\t\tconst reflection: Partial<AgentReflection> = {\n\t\t\t\t\tevaluation_previous_goal: input.evaluation_previous_goal,\n\t\t\t\t\tmemory: input.memory,\n\t\t\t\t\tnext_goal: input.next_goal,\n\t\t\t\t}\n\t\t\t\tconst actionName = Object.keys(input.action)[0]\n\t\t\t\tconst action: AgentStepEvent['action'] = {\n\t\t\t\t\tname: actionName,\n\t\t\t\t\tinput: input.action[actionName],\n\t\t\t\t\toutput: output,\n\t\t\t\t}\n\n\t\t\t\tthis.history.push({\n\t\t\t\t\ttype: 'step',\n\t\t\t\t\tstepIndex: step,\n\t\t\t\t\treflection,\n\t\t\t\t\taction,\n\t\t\t\t\tusage: result.usage,\n\t\t\t\t\trawResponse: result.rawResponse,\n\t\t\t\t\trawRequest: result.rawRequest,\n\t\t\t\t} as AgentStepEvent)\n\t\t\t\tthis.#emitHistoryChange()\n\n\t\t\t\t//\n\n\t\t\t\tawait onAfterStep?.(this, this.history)\n\n\t\t\t\tconsole.groupEnd()\n\n\t\t\t\t// finish task if done\n\n\t\t\t\tif (actionName === 'done') {\n\t\t\t\t\tconst success = action.input?.success ?? false\n\t\t\t\t\tconst text = action.input?.text || 'no text provided'\n\t\t\t\t\tconsole.log(chalk.green.bold('Task completed'), success, text)\n\t\t\t\t\tthis.#onDone(success)\n\t\t\t\t\tconst result: ExecutionResult = {\n\t\t\t\t\t\tsuccess,\n\t\t\t\t\t\tdata: text,\n\t\t\t\t\t\thistory: this.history,\n\t\t\t\t\t}\n\t\t\t\t\tawait onAfterTask?.(this, result)\n\t\t\t\t\treturn result\n\t\t\t\t}\n\t\t\t} catch (error: unknown) {\n\t\t\t\tconsole.groupEnd() // to prevent nested groups\n\t\t\t\tconst isAbortError = (error as any)?.rawError?.name === 'AbortError'\n\n\t\t\t\tconsole.error('Task failed', error)\n\t\t\t\tconst errorMessage = isAbortError ? 'Task stopped' : String(error)\n\t\t\t\tthis.#emitActivity({ type: 'error', message: errorMessage })\n\t\t\t\tthis.history.push({ type: 'error', message: errorMessage, rawResponse: error })\n\t\t\t\tthis.#emitHistoryChange()\n\t\t\t\tthis.#onDone(false)\n\t\t\t\tconst result: ExecutionResult = {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\tdata: errorMessage,\n\t\t\t\t\thistory: this.history,\n\t\t\t\t}\n\t\t\t\tawait onAfterTask?.(this, result)\n\t\t\t\treturn result\n\t\t\t}\n\n\t\t\tstep++\n\t\t\tif (step > this.config.maxSteps) {\n\t\t\t\tconst errorMessage = 'Step count exceeded maximum limit'\n\t\t\t\tthis.history.push({ type: 'error', message: errorMessage })\n\t\t\t\tthis.#emitHistoryChange()\n\t\t\t\tthis.#onDone(false)\n\t\t\t\tconst result: ExecutionResult = {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\tdata: errorMessage,\n\t\t\t\t\thistory: this.history,\n\t\t\t\t}\n\t\t\t\tawait onAfterTask?.(this, result)\n\t\t\t\treturn result\n\t\t\t}\n\n\t\t\tawait waitFor(this.config.stepDelay ?? 0.4)\n\t\t}\n\t}\n\n\t/**\n\t * Merge all tools into a single MacroTool with the following input:\n\t * - thinking: string\n\t * - evaluation_previous_goal: string\n\t * - memory: string\n\t * - next_goal: string\n\t * - action: { toolName: toolInput }\n\t * where action must be selected from tools defined in this.tools\n\t */\n\t#packMacroTool(): Tool<MacroToolInput, MacroToolResult> {\n\t\tconst tools = this.tools\n\n\t\tconst actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {\n\t\t\treturn z.object({ [toolName]: tool.inputSchema }).describe(tool.description)\n\t\t})\n\n\t\tconst actionSchema = z.union(actionSchemas as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]])\n\n\t\tconst macroToolSchema = z.object({\n\t\t\t// thinking: z.string().optional(),\n\t\t\tevaluation_previous_goal: z.string().optional(),\n\t\t\tmemory: z.string().optional(),\n\t\t\tnext_goal: z.string().optional(),\n\t\t\taction: actionSchema,\n\t\t})\n\n\t\treturn {\n\t\t\tdescription: 'You MUST call this tool every step!',\n\t\t\tinputSchema: macroToolSchema as z.ZodType<MacroToolInput>,\n\t\t\texecute: async (input: MacroToolInput): Promise<MacroToolResult> => {\n\t\t\t\t// abort\n\t\t\t\tif (this.#abortController.signal.aborted) throw new Error('AbortError')\n\n\t\t\t\tconsole.log(chalk.blue.bold('MacroTool input'), input)\n\t\t\t\tconst action = input.action\n\n\t\t\t\tconst toolName = Object.keys(action)[0]\n\t\t\t\tconst toolInput = action[toolName]\n\n\t\t\t\t// Build reflection text, only include non-empty fields\n\t\t\t\tconst reflectionLines: string[] = []\n\t\t\t\tif (input.evaluation_previous_goal)\n\t\t\t\t\treflectionLines.push(`✅: ${input.evaluation_previous_goal}`)\n\t\t\t\tif (input.memory) reflectionLines.push(`💾: ${input.memory}`)\n\t\t\t\tif (input.next_goal) reflectionLines.push(`🎯: ${input.next_goal}`)\n\n\t\t\t\tconst reflectionText = reflectionLines.length > 0 ? reflectionLines.join('\\n') : ''\n\n\t\t\t\tif (reflectionText) {\n\t\t\t\t\tconsole.log(reflectionText)\n\t\t\t\t}\n\n\t\t\t\t// Find the corresponding tool\n\t\t\t\tconst tool = tools.get(toolName)\n\t\t\t\tassert(tool, `Tool ${toolName} not found`)\n\n\t\t\t\tconsole.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)\n\n\t\t\t\t// Emit executing activity\n\t\t\t\tthis.#emitActivity({ type: 'executing', tool: toolName, input: toolInput })\n\n\t\t\t\tconst startTime = Date.now()\n\n\t\t\t\t// Execute tool, bind `this` to PageAgent\n\t\t\t\tconst result = await tool.execute.bind(this)(toolInput)\n\n\t\t\t\tconst duration = Date.now() - startTime\n\t\t\t\tconsole.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result)\n\n\t\t\t\t// Emit executed activity\n\t\t\t\tthis.#emitActivity({\n\t\t\t\t\ttype: 'executed',\n\t\t\t\t\ttool: toolName,\n\t\t\t\t\tinput: toolInput,\n\t\t\t\t\toutput: result,\n\t\t\t\t\tduration,\n\t\t\t\t})\n\n\t\t\t\t// counting wait time\n\t\t\t\tif (toolName === 'wait') {\n\t\t\t\t\tthis.#states.totalWaitTime += toolInput?.seconds || 0\n\t\t\t\t} else {\n\t\t\t\t\tthis.#states.totalWaitTime = 0\n\t\t\t\t}\n\n\t\t\t\t// Return structured result\n\t\t\t\treturn {\n\t\t\t\t\tinput,\n\t\t\t\t\toutput: result,\n\t\t\t\t}\n\t\t\t},\n\t\t}\n\t}\n\n\t/**\n\t * Get system prompt, dynamically replace language settings based on configured language\n\t */\n\t#getSystemPrompt(): string {\n\t\tif (this.config.customSystemPrompt) {\n\t\t\treturn this.config.customSystemPrompt\n\t\t}\n\n\t\tconst targetLanguage = this.config.language === 'zh-CN' ? '中文' : 'English'\n\t\tconst systemPrompt = SYSTEM_PROMPT.replace(\n\t\t\t/Default working language: \\*\\*.*?\\*\\*/,\n\t\t\t`Default working language: **${targetLanguage}**`\n\t\t)\n\n\t\treturn systemPrompt\n\t}\n\n\t/**\n\t * Get instructions from config\n\t */\n\tasync #getInstructions(): Promise<string> {\n\t\tconst { instructions, experimentalLlmsTxt } = this.config\n\n\t\tconst systemInstructions = instructions?.system?.trim()\n\t\tlet pageInstructions: string | undefined\n\n\t\tconst url = this.#states.browserState?.url || ''\n\t\tif (instructions?.getPageInstructions && url) {\n\t\t\ttry {\n\t\t\t\tpageInstructions = instructions.getPageInstructions(url)?.trim()\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\n\t\t\t\t\tchalk.red('[PageAgent] Failed to execute getPageInstructions callback:'),\n\t\t\t\t\terror\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\tconst llmsTxt = experimentalLlmsTxt && url ? await fetchLlmsTxt(url) : undefined\n\n\t\tif (!systemInstructions && !pageInstructions && !llmsTxt) return ''\n\n\t\tlet result = '<instructions>\\n'\n\n\t\tif (systemInstructions) {\n\t\t\tresult += `<system_instructions>\\n${systemInstructions}\\n</system_instructions>\\n`\n\t\t}\n\n\t\tif (pageInstructions) {\n\t\t\tresult += `<page_instructions>\\n${pageInstructions}\\n</page_instructions>\\n`\n\t\t}\n\n\t\tif (llmsTxt) {\n\t\t\tresult += `<llms_txt>\\n${llmsTxt}\\n</llms_txt>\\n`\n\t\t}\n\n\t\tresult += '</instructions>\\n\\n'\n\n\t\treturn result\n\t}\n\n\t/**\n\t * Generate system observations before each step\n\t * @todo loop detection\n\t * @todo console error\n\t */\n\tasync #handleObservations(step: number): Promise<void> {\n\t\t// Accumulated wait time warning\n\t\tif (this.#states.totalWaitTime >= 3) {\n\t\t\tthis.pushObservation(\n\t\t\t\t`You have waited ${this.#states.totalWaitTime} seconds accumulatively. ` +\n\t\t\t\t\t`DO NOT wait any longer unless you have a good reason.`\n\t\t\t)\n\t\t}\n\n\t\t// Detect URL change\n\t\tconst currentURL = this.#states.browserState?.url || ''\n\t\tif (currentURL !== this.#states.lastURL) {\n\t\t\tthis.pushObservation(`Page navigated to → ${currentURL}`)\n\t\t\tthis.#states.lastURL = currentURL\n\t\t\tawait waitFor(0.5) // wait for page to stabilize\n\t\t}\n\n\t\t// Remaining steps warning\n\t\tconst remaining = this.config.maxSteps - step\n\t\tif (remaining === 5) {\n\t\t\tthis.pushObservation(\n\t\t\t\t`⚠️ Only ${remaining} steps remaining. ` +\n\t\t\t\t\t`Consider wrapping up or calling done with partial results.`\n\t\t\t)\n\t\t} else if (remaining === 2) {\n\t\t\tthis.pushObservation(\n\t\t\t\t`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`\n\t\t\t)\n\t\t}\n\n\t\t// Push observations to history and emit\n\t\tif (this.#observations.length > 0) {\n\t\t\tfor (const content of this.#observations) {\n\t\t\t\tthis.history.push({ type: 'observation', content })\n\t\t\t\tconsole.log(chalk.cyan('Observation:'), content)\n\t\t\t}\n\t\t\tthis.#observations = []\n\t\t\tthis.#emitHistoryChange()\n\t\t}\n\t}\n\n\tasync #assembleUserPrompt(): Promise<string> {\n\t\tconst browserState = this.#states.browserState!\n\n\t\tlet prompt = ''\n\n\t\t// <instructions> (optional)\n\n\t\tprompt += await this.#getInstructions()\n\n\t\t// <agent_state>\n\t\t//  - <user_request>\n\t\t//  - <step_info>\n\t\t// <agent_state>\n\n\t\tconst stepCount = this.history.filter((e) => e.type === 'step').length\n\n\t\tprompt += '<agent_state>\\n'\n\t\tprompt += '<user_request>\\n'\n\t\tprompt += `${this.task}\\n`\n\t\tprompt += '</user_request>\\n'\n\t\tprompt += '<step_info>\\n'\n\t\tprompt += `Step ${stepCount + 1} of ${this.config.maxSteps} max possible steps\\n`\n\t\tprompt += `Current time: ${new Date().toLocaleString()}\\n`\n\t\tprompt += '</step_info>\\n'\n\t\tprompt += '</agent_state>\\n\\n'\n\n\t\t// <agent_history>\n\t\t//  - <step_N> for steps\n\t\t//  - <sys> for observations and system messages\n\n\t\tprompt += '<agent_history>\\n'\n\n\t\tlet stepIndex = 0\n\t\tfor (const event of this.history) {\n\t\t\tif (event.type === 'step') {\n\t\t\t\tstepIndex++\n\t\t\t\tprompt += `<step_${stepIndex}>\\n`\n\t\t\t\tprompt += `Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}\\n`\n\t\t\t\tprompt += `Memory: ${event.reflection.memory}\\n`\n\t\t\t\tprompt += `Next Goal: ${event.reflection.next_goal}\\n`\n\t\t\t\tprompt += `Action Results: ${event.action.output}\\n`\n\t\t\t\tprompt += `</step_${stepIndex}>\\n`\n\t\t\t} else if (event.type === 'observation') {\n\t\t\t\tprompt += `<sys>${event.content}</sys>\\n`\n\t\t\t} else if (event.type === 'user_takeover') {\n\t\t\t\tprompt += `<sys>User took over control and made changes to the page</sys>\\n`\n\t\t\t} else if (event.type === 'error') {\n\t\t\t\t// Error events are mainly for panel rendering, not included in LLM context\n\t\t\t\t// to avoid polluting the agent's reasoning with transient errors\n\t\t\t}\n\t\t}\n\n\t\tprompt += '</agent_history>\\n\\n'\n\n\t\t// <browser_state>\n\n\t\tlet pageContent = browserState.content\n\t\tif (this.config.transformPageContent) {\n\t\t\tpageContent = await this.config.transformPageContent(pageContent)\n\t\t}\n\n\t\tprompt += '<browser_state>\\n'\n\t\tprompt += browserState.header + '\\n'\n\t\tprompt += pageContent + '\\n'\n\t\tprompt += browserState.footer + '\\n\\n'\n\t\tprompt += '</browser_state>\\n\\n'\n\n\t\treturn prompt\n\t}\n\n\t#onDone(success = true) {\n\t\tthis.pageController.cleanUpHighlights()\n\t\tthis.pageController.hideMask() // No await - fire and forget\n\t\tthis.#setStatus(success ? 'completed' : 'error')\n\t\tthis.#abortController.abort()\n\t}\n\n\tdispose() {\n\t\tconsole.log('Disposing PageAgent...')\n\t\tthis.disposed = true\n\t\tthis.pageController.dispose()\n\t\t// this.history = []\n\t\tthis.#abortController.abort()\n\n\t\t// Emit dispose event for UI cleanup\n\t\tthis.dispatchEvent(new Event('dispose'))\n\n\t\tthis.config.onDispose?.(this)\n\t}\n}\n"
  },
  {
    "path": "packages/core/src/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\ndeclare module '*.md?raw' {\n\tconst content: string\n\texport default content\n}\n"
  },
  {
    "path": "packages/core/src/prompts/.prettierignore",
    "content": "system_prompt.md"
  },
  {
    "path": "packages/core/src/prompts/system_prompt.md",
    "content": "You are an AI agent designed to operate in an iterative loop to automate browser tasks. Your ultimate goal is accomplishing the task provided in <user_request>.\n\n<intro>\nYou excel at following tasks:\n1. Navigating complex websites and extracting precise information\n2. Automating form submissions and interactive web actions\n3. Gathering and saving information \n4. Operate effectively in an agent loop\n5. Efficiently performing diverse web tasks\n</intro>\n\n<language_settings>\n- Default working language: **English**\n- Use the language that user is using. Return in user's language.\n</language_settings>\n\n<input>\nAt every step, your input will consist of: \n1. <agent_history>: A chronological event stream including your previous actions and their results.\n2. <agent_state>: Current <user_request> and <step_info>.\n3. <browser_state>: Current URL, interactive elements indexed for actions, and visible page content.\n</input>\n\n<agent_history>\nAgent history will be given as a list of step information as follows:\n\n<step_{step_number}>:\nEvaluation of Previous Step: Assessment of last action\nMemory: Your memory of this step\nNext Goal: Your goal for this step\nAction Results: Your actions and their results\n</step_{step_number}>\n\nand system messages wrapped in <sys> tag.\n</agent_history>\n\n<user_request>\nUSER REQUEST: This is your ultimate objective and always remains visible.\n- This has the highest priority. Make the user happy.\n- If the user request is very specific - then carefully follow each step and dont skip or hallucinate steps.\n- If the task is open ended you can plan yourself how to get it done.\n</user_request>\n\n<browser_state>\n1. Browser State will be given as:\n\nCurrent URL: URL of the page you are currently viewing.\nInteractive Elements: All interactive elements will be provided in format as [index]<type>text</type> where\n- index: Numeric identifier for interaction\n- type: HTML element type (button, input, etc.)\n- text: Element description\n\nExamples:\n[33]<div>User form</div>\n\\t*[35]<button aria-label='Submit form'>Submit</button>\n\nNote that:\n- Only elements with numeric indexes in [] are interactive\n- (stacked) indentation (with \\t) is important and means that the element is a (html) child of the element above (with a lower index)\n- Elements tagged with `*[` are the new clickable elements that appeared on the website since the last step - if url has not changed.\n- Pure text elements without [] are not interactive.\n</browser_state>\n\n<browser_rules>\nStrictly follow these rules while using the browser and navigating the web:\n- Only interact with elements that have a numeric [index] assigned.\n- Only use indexes that are explicitly provided.\n- If the page changes after, for example, an input text action, analyze if you need to interact with new elements, e.g. selecting the right option from the list.\n- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.\n- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).\n- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.\n- If a captcha appears, tell user you can not solve captcha. Finish the task and ask user to solve it.\n- If expected elements are missing, try scrolling, or navigating back.\n- If the page is not fully loaded, use the `wait` action.\n- Do not repeat one action for more than 3 times unless some conditions changed.\n- If you fill an input field and your action sequence is interrupted, most often something changed e.g. suggestions popped up under the field.\n- If the <user_request> includes specific page information such as product type, rating, price, location, etc., try to apply filters to be more efficient.\n- The <user_request> is the ultimate goal. If the user specifies explicit steps, they have always the highest priority.\n- If you input_text into a field, you might need to press enter, click the search button, or select from dropdown for completion.\n- Don't login into a page if you don't have to. Don't login if you don't have the credentials. \n- There are 2 types of tasks always first think which type of request you are dealing with:\n1. Very specific step by step instructions:\n- Follow them as very precise and don't skip steps. Try to complete everything as requested.\n2. Open ended tasks. Plan yourself, be creative in achieving them.\n- If you get stuck e.g. with logins or captcha in open-ended tasks you can re-evaluate the task and try alternative ways, e.g. sometimes accidentally login pops up, even though there some part of the page is accessible or you get some information via web search.\n</browser_rules>\n\n<capability>\n- You can only handle single page app. Do not jump out of current page.\n- Do not click on link if it will open in a new page (e.g., <a target=\"_blank\">)\n- It is ok to fail the task.\n\t- User can be wrong. If the request of user is not achievable, inappropriate or you do not have enough information or tools to achieve it. Tell user to make a better request.\n\t- Webpage can be broken. All webpages or apps have bugs. Some bug will make it hard for your job. It's encouraged to tell user the problem of current page. Your feedbacks (including failing) are valuable for user.\n\t- Trying too hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted results and harmful side-effects. User would rather you complete the task with a fail.\n- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.\n</capability>\n\n<task_completion_rules>\nYou must call the `done` action in one of three cases:\n- When you have fully completed the USER REQUEST.\n- When you reach the final allowed step (`max_steps`), even if the task is incomplete.\n- When you feel stuck or unable to solve user request. Or user request is not clear or contains inappropriate content.\n- If it is ABSOLUTELY IMPOSSIBLE to continue.\n\nThe `done` action is your opportunity to terminate and share your findings with the user.\n- Set `success` to `true` only if the full USER REQUEST has been completed with no missing components.\n- If any part of the request is missing, incomplete, or uncertain, set `success` to `false`.\n- You can use the `text` field of the `done` action to communicate your findings and to provide a coherent reply to the user and fulfill the USER REQUEST.\n- You are ONLY ALLOWED to call `done` as a single action. Don't call it together with other actions.\n- If the user asks for specified format, such as \"return JSON with following structure\", \"return a list of format...\", MAKE sure to use the right format in your answer.\n- If the user asks for a structured output, your `done` action's schema may be modified. Take this schema into account when solving the task!\n</task_completion_rules>\n\n<reasoning_rules>\nExhibit the following reasoning patterns to successfully achieve the <user_request>:\n\n- Reason about <agent_history> to track progress and context toward <user_request>.\n- Analyze the most recent \"Next Goal\" and \"Action Result\" in <agent_history> and clearly state what you previously tried to achieve.\n- Analyze all relevant items in <agent_history> and <browser_state> to understand your state.\n- Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because it appears to be executed in your last step in <agent_history>. If the expected change is missing, mark the last action as failed (or uncertain) and plan a recovery.\n- Analyze whether you are stuck, e.g. when you repeat the same actions multiple times without any progress. Then consider alternative approaches e.g. scrolling for more context or ask user for help.\n- Ask user for help if you have any difficulty. Keep user in the loop.\n- If you see information relevant to <user_request>, plan saving the information to memory.\n- Always reason about the <user_request>. Make sure to carefully analyze the specific steps and information required. E.g. specific filters, specific form fields, specific information to search. Make sure to always compare the current trajectory with the user request and think carefully if thats how the user requested it.\n</reasoning_rules>\n\n<examples>\nHere are examples of good output patterns. Use them as reference but never copy them directly.\n\n<evaluation_examples>\n\"evaluation_previous_goal\": \"Successfully navigated to the product page and found the target information. Verdict: Success\"\n\"evaluation_previous_goal\": \"Clicked the login button and user authentication form appeared. Verdict: Success\"\n</evaluation_examples>\n\n<memory_examples>\n\"memory\": \"Found many pending reports that need to be analyzed in the main page. Successfully processed the first 2 reports on quarterly sales data and moving on to inventory analysis and customer feedback reports.\"\n</memory_examples>\n\n<next_goal_examples>\n\"next_goal\": \"Click on the 'Add to Cart' button to proceed with the purchase flow.\"\n</next_goal_examples>\n</examples>\n\n<output>\n{\n  \"evaluation_previous_goal\": \"Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.\",\n  \"memory\": \"1-3 concise sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.\",\n  \"next_goal\": \"State the next immediate goal and action to achieve it, in one clear sentence.\",\n  \"action\":{\n    \"Action name\": {// Action parameters}\n  }\n}\n</output>\n"
  },
  {
    "path": "packages/core/src/tools/index.ts",
    "content": "/**\n * Internal tools for PageAgent.\n * @note Adapted from browser-use\n */\nimport * as z from 'zod/v4'\n\nimport type { PageAgentCore } from '../PageAgentCore'\nimport { waitFor } from '../utils'\n\n/**\n * Internal tool definition that has access to PageAgent `this` context\n */\nexport interface PageAgentTool<TParams = any> {\n\t// name: string\n\tdescription: string\n\tinputSchema: z.ZodType<TParams>\n\texecute: (this: PageAgentCore, args: TParams) => Promise<string>\n}\n\nexport function tool<TParams>(options: PageAgentTool<TParams>): PageAgentTool<TParams> {\n\treturn options\n}\n\n/**\n * Internal tools for PageAgent.\n * Note: Using any to allow different parameter types for each tool\n */\nexport const tools = new Map<string, PageAgentTool>()\n\ntools.set(\n\t'done',\n\ttool({\n\t\tdescription:\n\t\t\t'Complete task. Text is your final response to the user — keep it concise unless the user explicitly asks for detail.',\n\t\tinputSchema: z.object({\n\t\t\ttext: z.string(),\n\t\t\tsuccess: z.boolean().default(true),\n\t\t}),\n\t\texecute: async function (this: PageAgentCore, input) {\n\t\t\t// @note main loop will handle this one\n\t\t\treturn Promise.resolve('Task completed')\n\t\t},\n\t})\n)\n\ntools.set(\n\t'wait',\n\ttool({\n\t\tdescription: 'Wait for x seconds. Can be used to wait until the page or data is fully loaded.',\n\t\tinputSchema: z.object({\n\t\t\tseconds: z.number().min(1).max(10).default(1),\n\t\t}),\n\t\texecute: async function (this: PageAgentCore, input) {\n\t\t\t// try to subtract LLM calling time from the actual wait time\n\t\t\tconst lastTimeUpdate = await this.pageController.getLastUpdateTime()\n\t\t\tconst actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)\n\t\t\tconsole.log(`actualWaitTime: ${actualWaitTime} seconds`)\n\t\t\tawait waitFor(actualWaitTime)\n\n\t\t\treturn `✅ Waited for ${input.seconds} seconds.`\n\t\t},\n\t})\n)\n\ntools.set(\n\t'ask_user',\n\ttool({\n\t\tdescription:\n\t\t\t'Ask the user a question and wait for their answer. Use this if you need more information or clarification.',\n\t\tinputSchema: z.object({\n\t\t\tquestion: z.string(),\n\t\t}),\n\t\texecute: async function (this: PageAgentCore, input) {\n\t\t\tif (!this.onAskUser) {\n\t\t\t\tthrow new Error('ask_user tool requires onAskUser callback to be set')\n\t\t\t}\n\t\t\tconst answer = await this.onAskUser(input.question)\n\t\t\treturn `User answered: ${answer}`\n\t\t},\n\t})\n)\n\ntools.set(\n\t'click_element_by_index',\n\ttool({\n\t\tdescription: 'Click element by index',\n\t\tinputSchema: z.object({\n\t\t\tindex: z.int().min(0),\n\t\t}),\n\t\texecute: async function (this: PageAgentCore, input) {\n\t\t\tconst result = await this.pageController.clickElement(input.index)\n\t\t\treturn result.message\n\t\t},\n\t})\n)\n\ntools.set(\n\t'input_text',\n\ttool({\n\t\tdescription: 'Click and type text into an interactive input element',\n\t\tinputSchema: z.object({\n\t\t\tindex: z.int().min(0),\n\t\t\ttext: z.string(),\n\t\t}),\n\t\texecute: async function (this: PageAgentCore, input) {\n\t\t\tconst result = await this.pageController.inputText(input.index, input.text)\n\t\t\treturn result.message\n\t\t},\n\t})\n)\n\ntools.set(\n\t'select_dropdown_option',\n\ttool({\n\t\tdescription:\n\t\t\t'Select dropdown option for interactive element index by the text of the option you want to select',\n\t\tinputSchema: z.object({\n\t\t\tindex: z.int().min(0),\n\t\t\ttext: z.string(),\n\t\t}),\n\t\texecute: async function (this: PageAgentCore, input) {\n\t\t\tconst result = await this.pageController.selectOption(input.index, input.text)\n\t\t\treturn result.message\n\t\t},\n\t})\n)\n\n/**\n * @note Reference from browser-use\n */\ntools.set(\n\t'scroll',\n\ttool({\n\t\tdescription: 'Scroll the page vertically. Use index for scroll elements (dropdowns/custom UI).',\n\t\tinputSchema: z.object({\n\t\t\tdown: z.boolean().default(true),\n\t\t\tnum_pages: z.number().min(0).max(10).optional().default(0.1),\n\t\t\tpixels: z.number().int().min(0).optional(),\n\t\t\tindex: z.number().int().min(0).optional(),\n\t\t}),\n\t\texecute: async function (this: PageAgentCore, input) {\n\t\t\tconst result = await this.pageController.scroll({\n\t\t\t\t...input,\n\t\t\t\tnumPages: input.num_pages,\n\t\t\t})\n\t\t\treturn result.message\n\t\t},\n\t})\n)\n\n/**\n * @todo Tables need a dedicated parser to extract structured data. This tool is useless.\n */\ntools.set(\n\t'scroll_horizontally',\n\ttool({\n\t\tdescription:\n\t\t\t'Scroll the page horizontally, or within a specific element by index. Useful for wide tables.',\n\t\tinputSchema: z.object({\n\t\t\tright: z.boolean().default(true),\n\t\t\tpixels: z.number().int().min(0),\n\t\t\tindex: z.number().int().min(0).optional(),\n\t\t}),\n\t\texecute: async function (this: PageAgentCore, input) {\n\t\t\tconst result = await this.pageController.scrollHorizontally(input)\n\t\t\treturn result.message\n\t\t},\n\t})\n)\n\ntools.set(\n\t'execute_javascript',\n\ttool({\n\t\tdescription:\n\t\t\t'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution!',\n\t\tinputSchema: z.object({\n\t\t\tscript: z.string(),\n\t\t}),\n\t\texecute: async function (this: PageAgentCore, input) {\n\t\t\tconst result = await this.pageController.executeJavascript(input.script)\n\t\t\treturn result.message\n\t\t},\n\t})\n)\n\n// @todo send_keys\n// @todo upload_file\n// @todo go_back\n// @todo extract_structured_data\n"
  },
  {
    "path": "packages/core/src/types.ts",
    "content": "import type { LLMConfig } from '@page-agent/llms'\n\n// @note circular dependency but okay\nimport type { PageAgentCore } from './PageAgentCore'\nimport type { PageAgentTool } from './tools'\n\n/** Supported UI languages */\nexport type SupportedLanguage = 'en-US' | 'zh-CN'\n\nexport interface AgentConfig extends LLMConfig {\n\tlanguage?: SupportedLanguage\n\n\t/**\n\t * Maximum number of steps the agent can take per task.\n\t * @default 40\n\t */\n\tmaxSteps?: number\n\n\t/**\n\t * Custom tools to extend PageAgent capabilities\n\t * @experimental\n\t * @note You can also override or remove internal tools by using the same name.\n\t * @see PageAgentTool\n\t *\n\t * @example\n\t * // override internal tool\n\t * import { z } from 'zod/v4'\n\t * import { tool } from 'page-agent'\n\t * const customTools = {\n\t * ask_user: tool({\n\t * \tdescription:\n\t * \t\t'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',\n\t * \tinputSchema: z.object({\n\t * \t\tquestion: z.string(),\n\t * \t}),\n\t * \texecute: async function (this: PageAgent, input) {\n\t * \t\tconst answer = await do_some_thing(input.question)\n\t * \t\treturn \"✅ Received user answer: \" + answer\n\t * \t},\n\t * })\n\t * }\n\t *\n\t * @example\n\t * // remove internal tool\n\t * const customTools = {\n\t * \task_user: null // never ask user questions\n\t * }\n\t */\n\tcustomTools?: Record<string, PageAgentTool | null>\n\n\t/**\n\t * Instructions to guide the agent's behavior\n\t */\n\tinstructions?: {\n\t\t/**\n\t\t * Global system-level instructions, applied to all tasks\n\t\t */\n\t\tsystem?: string\n\n\t\t/**\n\t\t * Dynamic page-level instructions callback\n\t\t * Called before each step to get instructions for the current page\n\t\t * @param url - Current page URL (window.location.href)\n\t\t * @returns Instructions string, or undefined/null to skip\n\t\t */\n\t\tgetPageInstructions?: (url: string) => string | undefined | null\n\t}\n\n\t/**\n\t * Lifecycle hooks for task execution.\n\t * @experimental API may change in future versions.\n\t *\n\t * All hooks receive the agent instance as first parameter.\n\t */\n\n\t/**\n\t * Called before each step execution.\n\t * @experimental\n\t * @param agent - The PageAgentCore instance\n\t * @param stepCount - Current step number (0-indexed)\n\t */\n\tonBeforeStep?: (agent: PageAgentCore, stepCount: number) => Promise<void> | void\n\n\t/**\n\t * Called after each step execution.\n\t * @experimental\n\t * @param agent - The PageAgentCore instance\n\t * @param history - Current history of events\n\t */\n\tonAfterStep?: (agent: PageAgentCore, history: HistoricalEvent[]) => Promise<void> | void\n\n\t/**\n\t * Called before task execution starts.\n\t * @experimental\n\t * @param agent - The PageAgentCore instance\n\t */\n\tonBeforeTask?: (agent: PageAgentCore) => Promise<void> | void\n\n\t/**\n\t * Called after task execution completes (success or failure).\n\t * @experimental\n\t * @param agent - The PageAgentCore instance\n\t * @param result - The execution result\n\t */\n\tonAfterTask?: (agent: PageAgentCore, result: ExecutionResult) => Promise<void> | void\n\n\t/**\n\t * Called when the agent is disposed.\n\t * @experimental\n\t * @note This hook can block the disposal process if it's async.\n\t * @param agent - The PageAgentCore instance\n\t * @param reason - Optional reason for disposal\n\t */\n\tonDispose?: (agent: PageAgentCore, reason?: string) => void\n\n\t// page behavior hooks\n\n\t/**\n\t * @experimental\n\t * Enable the experimental script execution tool that allows executing generated JavaScript code on the page.\n\t * @note Can cause unpredictable side effects.\n\t * @note May bypass some safe guards and data-masking mechanisms.\n\t */\n\texperimentalScriptExecutionTool?: boolean\n\n\t/**\n\t * @experimental\n\t * Fetch /llms.txt from current site origin and include as context.\n\t * Only fetched once per origin per task.\n\t * @default false\n\t */\n\texperimentalLlmsTxt?: boolean\n\n\t/**\n\t * Transform page content before sending to LLM.\n\t * Called after DOM extraction and simplification, before LLM invocation.\n\t * Use cases: inspect extraction results, modify page info, mask sensitive data.\n\t *\n\t * @param content - Simplified page content that will be sent to LLM\n\t * @returns Transformed content\n\t *\n\t * @example\n\t * // Mask phone numbers\n\t * transformPageContent: async (content) => {\n\t *   return content.replace(/1[3-9]\\d{9}/g, '***********')\n\t * }\n\t */\n\ttransformPageContent?: (content: string) => Promise<string> | string\n\n\t/**\n\t * Completely override the default system prompt.\n\t * @experimental Use with caution - incorrect prompts may break agent behavior.\n\t */\n\tcustomSystemPrompt?: string\n\n\t/**\n\t * Delay between steps in seconds.\n\t * @default 0.4\n\t */\n\tstepDelay?: number\n}\n\n/**\n * Agent reflection state - the reflection-before-action model\n *\n * Every tool call must first reflect on:\n * - evaluation_previous_goal: How well did the previous action achieve its goal?\n * - memory: Key information to remember for future steps\n * - next_goal: What should be accomplished in the next action?\n */\nexport interface AgentReflection {\n\tevaluation_previous_goal: string\n\tmemory: string\n\tnext_goal: string\n}\n\n/**\n * MacroTool input structure\n *\n * This is the core abstraction that enforces the \"reflection-before-action\" mental model.\n * Before executing any action, the LLM must output its reasoning state.\n */\nexport interface MacroToolInput extends Partial<AgentReflection> {\n\taction: Record<string, any>\n}\n\n/**\n * MacroTool output structure\n */\nexport interface MacroToolResult {\n\tinput: MacroToolInput\n\toutput: string\n}\n\n/**\n * A single agent step with reflection and action\n */\nexport interface AgentStepEvent {\n\ttype: 'step'\n\tstepIndex: number\n\treflection: Partial<AgentReflection>\n\taction: {\n\t\tname: string\n\t\tinput: any\n\t\toutput: string\n\t}\n\tusage: {\n\t\tpromptTokens: number\n\t\tcompletionTokens: number\n\t\ttotalTokens: number\n\t\tcachedTokens?: number\n\t\treasoningTokens?: number\n\t}\n\t/** Raw LLM response for debugging */\n\trawResponse?: unknown\n\t/** Raw LLM request for debugging */\n\trawRequest?: unknown\n}\n\n/**\n * Persistent observation event (stays in memory)\n */\nexport interface ObservationEvent {\n\ttype: 'observation'\n\tcontent: string\n}\n\n/**\n * User takeover event\n */\nexport interface UserTakeoverEvent {\n\ttype: 'user_takeover'\n}\n\n/**\n * Retry event - LLM call is being retried\n */\nexport interface RetryEvent {\n\ttype: 'retry'\n\tmessage: string\n\tattempt: number\n\tmaxAttempts: number\n}\n\n/**\n * Error event - fatal error from LLM or execution\n */\nexport interface AgentErrorEvent {\n\ttype: 'error'\n\tmessage: string\n\trawResponse?: unknown\n}\n\n/**\n * Union type for all history events\n */\nexport type HistoricalEvent =\n\t| AgentStepEvent\n\t| ObservationEvent\n\t| UserTakeoverEvent\n\t| RetryEvent\n\t| AgentErrorEvent\n\n/**\n * Agent execution status\n */\nexport type AgentStatus = 'idle' | 'running' | 'completed' | 'error'\n\n/**\n * Agent activity - transient state for immediate UI feedback.\n *\n * Unlike historical events (which are persisted), activities are ephemeral\n * and represent \"what the agent is doing right now\". UI components should\n * listen to 'activity' events to show real-time feedback.\n *\n * Note: There is no 'idle' activity - absence of activity events means idle.\n */\nexport type AgentActivity =\n\t| { type: 'thinking' }\n\t| { type: 'executing'; tool: string; input: unknown }\n\t| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }\n\t| { type: 'retrying'; attempt: number; maxAttempts: number }\n\t| { type: 'error'; message: string }\n\nexport interface ExecutionResult {\n\tsuccess: boolean\n\tdata: string\n\thistory: HistoricalEvent[]\n}\n"
  },
  {
    "path": "packages/core/src/utils/autoFixer.ts",
    "content": "import { InvokeError, InvokeErrorType } from '@page-agent/llms'\nimport chalk from 'chalk'\nimport * as z from 'zod/v4'\n\nimport type { PageAgentTool } from '../tools'\n\nconst log = console.log.bind(console, chalk.yellow('[autoFixer]'))\n\n/**\n * Normalize LLM response and fix common format issues.\n *\n * Handles:\n * - No tool_calls but JSON in message.content (fallback)\n * - Model returns action name as tool call instead of AgentOutput\n * - Arguments wrapped as double JSON string\n * - Nested function call format\n * - Missing action field (fallback to wait)\n * - Primitive action input for single-field tools (e.g. `{\"click_element_by_index\": 2}`)\n * - etc.\n */\nexport function normalizeResponse(response: any, tools?: Map<string, PageAgentTool>): any {\n\tlet resolvedArguments = null as any\n\n\tconst choice = (response as { choices?: Choice[] }).choices?.[0]\n\tif (!choice) throw new Error('No choices in response')\n\n\tconst message = choice.message\n\tif (!message) throw new Error('No message in choice')\n\n\tconst toolCall = message.tool_calls?.[0]\n\n\t// fix level and location of arguments\n\n\tif (toolCall?.function?.arguments) {\n\t\tresolvedArguments = safeJsonParse(toolCall.function.arguments)\n\n\t\t// case: sometimes the model only returns the action level\n\t\tif (toolCall.function.name && toolCall.function.name !== 'AgentOutput') {\n\t\t\tlog(`#1: fixing tool_call`)\n\t\t\tresolvedArguments = { action: safeJsonParse(resolvedArguments) }\n\t\t}\n\t} else {\n\t\t// case: sometimes the model returns json in content instead of tool_calls\n\t\tif (message.content) {\n\t\t\tconst content = message.content.trim()\n\t\t\tconst jsonInContent = retrieveJsonFromString(content)\n\t\t\tif (jsonInContent) {\n\t\t\t\tresolvedArguments = safeJsonParse(jsonInContent)\n\n\t\t\t\t// case: sometimes the content json includes upper level wrapper\n\t\t\t\tif (resolvedArguments?.name === 'AgentOutput') {\n\t\t\t\t\tlog(`#2: fixing tool_call`)\n\t\t\t\t\tresolvedArguments = safeJsonParse(resolvedArguments.arguments)\n\t\t\t\t}\n\n\t\t\t\t// case: sometimes even 2-levels of wrapping\n\t\t\t\tif (resolvedArguments?.type === 'function') {\n\t\t\t\t\tlog(`#3: fixing tool_call`)\n\t\t\t\t\tresolvedArguments = safeJsonParse(resolvedArguments.function.arguments)\n\t\t\t\t}\n\n\t\t\t\t// case: and sometimes action level only\n\t\t\t\t// todo: needs better detection logic\n\t\t\t\tif (\n\t\t\t\t\t!resolvedArguments?.action &&\n\t\t\t\t\t!resolvedArguments?.evaluation_previous_goal &&\n\t\t\t\t\t!resolvedArguments?.memory &&\n\t\t\t\t\t!resolvedArguments?.next_goal &&\n\t\t\t\t\t!resolvedArguments?.thinking\n\t\t\t\t) {\n\t\t\t\t\tlog(`#4: fixing tool_call`)\n\t\t\t\t\tresolvedArguments = { action: safeJsonParse(resolvedArguments) }\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthrow new Error('No tool_call and the message content does not contain valid JSON')\n\t\t\t}\n\t\t} else {\n\t\t\tthrow new Error('No tool_call nor message content is present')\n\t\t}\n\t}\n\n\t// fix double stringified arguments\n\tresolvedArguments = safeJsonParse(resolvedArguments)\n\tif (resolvedArguments.action) {\n\t\tresolvedArguments.action = safeJsonParse(resolvedArguments.action)\n\t}\n\n\t// validate and fix action input using tool schemas\n\tif (resolvedArguments.action && tools) {\n\t\tresolvedArguments.action = validateAction(resolvedArguments.action, tools)\n\t}\n\n\t// fix incomplete formats\n\tif (!resolvedArguments.action) {\n\t\tlog(`#5: fixing tool_call`)\n\t\tresolvedArguments.action = { name: 'wait', input: { seconds: 1 } }\n\t}\n\n\t// pack back to standard format\n\treturn {\n\t\t...response,\n\t\tchoices: [\n\t\t\t{\n\t\t\t\t...choice,\n\t\t\t\tmessage: {\n\t\t\t\t\t...message,\n\t\t\t\t\ttool_calls: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t...(toolCall || {}),\n\t\t\t\t\t\t\tfunction: {\n\t\t\t\t\t\t\t\t...(toolCall?.function || {}),\n\t\t\t\t\t\t\t\tname: 'AgentOutput',\n\t\t\t\t\t\t\t\targuments: JSON.stringify(resolvedArguments),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t}\n}\n\n/**\n * Validate action against tool schemas. Provides clear error messages\n * instead of letting the union schema produce unreadable errors.\n *\n * Also coerces primitive inputs for single-field tools:\n * e.g. `{\"click_element_by_index\": 2}` → `{\"click_element_by_index\": {\"index\": 2}}`\n */\nfunction validateAction(action: any, tools: Map<string, PageAgentTool>): any {\n\tif (typeof action !== 'object' || action === null) return action\n\n\tconst toolName = Object.keys(action)[0]\n\tif (!toolName) return action\n\n\tconst tool = tools.get(toolName)\n\tif (!tool) {\n\t\tconst available = Array.from(tools.keys()).join(', ')\n\t\tthrow new InvokeError(\n\t\t\tInvokeErrorType.INVALID_TOOL_ARGS,\n\t\t\t`Unknown action \"${toolName}\". Available: ${available}`\n\t\t)\n\t}\n\n\tlet value = action[toolName]\n\tconst schema = tool.inputSchema\n\n\t// coerce primitive input for single-field tools\n\tif (schema instanceof z.ZodObject && value !== null && typeof value !== 'object') {\n\t\tconst requiredKey = Object.keys(schema.shape).find(\n\t\t\t(k) => !(schema.shape as Record<string, z.ZodType>)[k].safeParse(undefined).success\n\t\t)\n\t\tif (requiredKey) {\n\t\t\tlog(`coercing primitive action input for \"${toolName}\"`)\n\t\t\tvalue = { [requiredKey]: value }\n\t\t}\n\t}\n\n\tconst result = schema.safeParse(value)\n\tif (!result.success) {\n\t\tthrow new InvokeError(\n\t\t\tInvokeErrorType.INVALID_TOOL_ARGS,\n\t\t\t`Invalid input for action \"${toolName}\": ${z.prettifyError(result.error)}`\n\t\t)\n\t}\n\n\treturn { [toolName]: result.data }\n}\n\n/**\n * Safely parse JSON, return original input if not json.\n */\nfunction safeJsonParse(input: any): any {\n\tif (typeof input === 'string') {\n\t\ttry {\n\t\t\treturn JSON.parse(input.trim())\n\t\t} catch {\n\t\t\treturn input\n\t\t}\n\t}\n\treturn input\n}\n\n/**\n * Extract and parse JSON from a string.\n * - Treat content between the first `{` and the last `}` as JSON.\n * - Try to parse that content as JSON and return the parsed value (object/array/primitive) if successful, otherwise return null.\n */\nfunction retrieveJsonFromString(str: string): any {\n\ttry {\n\t\tconst json = /({[\\s\\S]*})/.exec(str) ?? []\n\t\tif (json.length === 0) {\n\t\t\treturn null\n\t\t}\n\t\treturn JSON.parse(json[0]!)\n\t} catch {\n\t\treturn null\n\t}\n}\n\ninterface Choice {\n\tmessage?: {\n\t\trole?: 'assistant'\n\t\tcontent?: string\n\t\ttool_calls?: {\n\t\t\tid?: string\n\t\t\ttype?: 'function'\n\t\t\tfunction?: {\n\t\t\t\tname?: string\n\t\t\t\targuments?: string\n\t\t\t}\n\t\t}[]\n\t}\n\tindex?: 0\n\tfinish_reason?: 'tool_calls'\n}\n"
  },
  {
    "path": "packages/core/src/utils/index.ts",
    "content": "import chalk from 'chalk'\n\nexport * from './autoFixer'\n\nexport async function waitFor(seconds: number): Promise<void> {\n\tawait new Promise((resolve) => setTimeout(resolve, seconds * 1000))\n}\n\n//\n\nexport function truncate(text: string, maxLength: number): string {\n\tif (text.length > maxLength) {\n\t\treturn text.substring(0, maxLength) + '...'\n\t}\n\treturn text\n}\n\n//\n\nexport function randomID(existingIDs?: string[]): string {\n\tlet id = Math.random().toString(36).substring(2, 11)\n\n\tif (!existingIDs) {\n\t\treturn id\n\t}\n\n\tconst MAX_TRY = 1000\n\tlet tryCount = 0\n\n\twhile (existingIDs.includes(id)) {\n\t\tid = Math.random().toString(36).substring(2, 11)\n\t\ttryCount++\n\t\tif (tryCount > MAX_TRY) {\n\t\t\tthrow new Error('randomID: too many tries')\n\t\t}\n\t}\n\n\treturn id\n}\n\n//\nconst _global = globalThis as any\n\nif (!_global.__PAGE_AGENT_IDS__) {\n\t_global.__PAGE_AGENT_IDS__ = []\n}\n\nconst ids = _global.__PAGE_AGENT_IDS__\n\n/**\n * Generate a random ID.\n * @note Unique within this window.\n */\nexport function uid() {\n\tconst id = randomID(ids)\n\tids.push(id)\n\treturn id\n}\n\nconst llmsTxtCache = new Map<string, string | null>()\n\n/** Fetch /llms.txt for a URL's origin. Cached per origin, `null` = tried and not found. */\nexport async function fetchLlmsTxt(url: string): Promise<string | null> {\n\tlet origin: string\n\ttry {\n\t\torigin = new URL(url).origin\n\t} catch {\n\t\treturn null // Invalid URL\n\t}\n\t// about:blank, data:, file:\n\tif (origin === 'null') return null\n\n\tif (llmsTxtCache.has(origin)) return llmsTxtCache.get(origin)!\n\n\tconst endpoint = `${origin}/llms.txt`\n\tlet result: string | null = null\n\ttry {\n\t\tconsole.log(chalk.gray(`[llms.txt] Fetching ${endpoint}`))\n\t\tconst res = await fetch(endpoint, { signal: AbortSignal.timeout(3000) })\n\t\tif (res.ok) {\n\t\t\tresult = await res.text()\n\t\t\tconsole.log(chalk.green(`[llms.txt] Found (${result.length} chars)`))\n\t\t\tif (result.length > 1000) {\n\t\t\t\tconsole.log(chalk.yellow(`[llms.txt] Truncating to 1000 chars`))\n\t\t\t\tresult = truncate(result, 1000)\n\t\t\t}\n\t\t} else {\n\t\t\tconsole.debug(chalk.gray(`[llms.txt] ${res.status} for ${endpoint}`))\n\t\t}\n\t} catch (e) {\n\t\tconsole.debug(chalk.gray(`[llms.txt] not found for ${endpoint}`), e)\n\t}\n\tllmsTxtCache.set(origin, result)\n\treturn result\n}\n\n/**\n * Simple assertion function that throws an error if the condition is falsy\n * @param condition - The condition to assert\n * @param message - Optional error message\n * @throws Error if condition is falsy\n */\nexport function assert(condition: unknown, message?: string, silent?: boolean): asserts condition {\n\tif (!condition) {\n\t\tconst errorMessage = message ?? 'Assertion failed'\n\n\t\tif (!silent) console.error(chalk.red(`❌ assert: ${errorMessage}`))\n\n\t\tthrow new Error(errorMessage)\n\t}\n}\n"
  },
  {
    "path": "packages/core/tsconfig.dts.json",
    "content": "{\n    \"extends\": \"./tsconfig.json\",\n    \"compilerOptions\": {\n        // @workaround DTS bug\n        // dts do not work with monorepo path mapping\n        // disable path mapping for it\n        \"paths\": {}\n    }\n}\n"
  },
  {
    "path": "packages/core/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.base.json\",\n    \"compilerOptions\": {\n        \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.tsbuildinfo\",\n        \"noEmit\": false,\n        \"allowImportingTsExtensions\": false,\n        \"baseUrl\": \".\",\n        \"outDir\": \"dist\",\n        \"paths\": {\n            //\n            \"@page-agent/llms\": [\"../llms/src/index.ts\"],\n            \"@page-agent/page-controller\": [\"../page-controller/src/PageController.ts\"]\n        }\n    },\n    \"include\": [\"**/*.ts\"],\n    \"exclude\": [\"dist\", \"node_modules\"],\n    \"references\": [\n        //\n        { \"path\": \"../llms\" },\n        { \"path\": \"../page-controller\" }\n    ]\n}\n"
  },
  {
    "path": "packages/core/vite.config.js",
    "content": "// @ts-check\nimport { dirname, resolve } from 'path'\nimport dts from 'unplugin-dts/vite'\nimport { fileURLToPath } from 'url'\nimport { defineConfig } from 'vite'\nimport cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n// ES Module for NPM Package\nexport default defineConfig({\n\tclearScreen: false,\n\tplugins: [\n\t\tdts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),\n\t\tcssInjectedByJsPlugin({ relativeCSSInjection: true }),\n\t],\n\tpublicDir: false,\n\tesbuild: {\n\t\tkeepNames: true,\n\t},\n\tbuild: {\n\t\tlib: {\n\t\t\tentry: resolve(__dirname, 'src/PageAgentCore.ts'),\n\t\t\tname: 'PageAgentCore',\n\t\t\tfileName: 'page-agent-core',\n\t\t\tformats: ['es'],\n\t\t},\n\t\toutDir: resolve(__dirname, 'dist', 'esm'),\n\t\trollupOptions: {\n\t\t\texternal: [\n\t\t\t\t'chalk',\n\t\t\t\t'zod',\n\t\t\t\t'zod/v4',\n\t\t\t\t// all the internal packages\n\t\t\t\t/^@page-agent\\//,\n\t\t\t],\n\t\t},\n\t\tminify: false,\n\t\tsourcemap: true,\n\t\tcssCodeSplit: true,\n\t},\n\tdefine: {\n\t\t'process.env.NODE_ENV': '\"production\"',\n\t},\n})\n"
  },
  {
    "path": "packages/extension/.prettierignore",
    "content": ".wxt\nsrc/components/ui"
  },
  {
    "path": "packages/extension/PRIVACY.md",
    "content": "# Privacy Policy for Page Agent Extension\n\nThis document has moved. Please see our full **[Terms of Use & Privacy](../../docs/terms-and-privacy.md)**.\n\nOnline: https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md\n"
  },
  {
    "path": "packages/extension/components.json",
    "content": "{\n    \"$schema\": \"https://ui.shadcn.com/schema.json\",\n    \"style\": \"new-york\",\n    \"rsc\": false,\n    \"tsx\": true,\n    \"tailwind\": {\n        \"config\": \"\",\n        \"css\": \"src/assets/index.css\",\n        \"baseColor\": \"neutral\",\n        \"cssVariables\": true,\n        \"prefix\": \"\"\n    },\n    \"iconLibrary\": \"lucide\",\n    \"aliases\": {\n        \"components\": \"@/components\",\n        \"utils\": \"@/lib/utils\",\n        \"ui\": \"@/components/ui\",\n        \"lib\": \"@/lib\",\n        \"hooks\": \"@/lib/hooks\"\n    },\n    \"registries\": {\n        \"@magicui\": \"https://magicui.design/r/{name}.json\"\n    }\n}\n"
  },
  {
    "path": "packages/extension/docs/extension_api.md",
    "content": "# Page Agent Extension API\n\nIntegrate the Page Agent extension into your web app and trigger multi-page browser tasks from page JavaScript.\n\n## Installation\n\n### 1. Install the browser extension\n\nPrimary channel:\n\n- Chrome Web Store: https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj\n\nLatest updates are often published earlier on:\n\n- GitHub Releases: https://github.com/alibaba/page-agent/releases\n\n### 2. Install type definitions (recommended)\n\n```bash\nnpm install @page-agent/core --save-dev\n```\n\n### 3. Authorization (Token)\n\nThe token allows your page JS to call the extension API (`window.PAGE_AGENT_EXT`) and execute multi-page tasks.\n\nWhy token-based access is required:\n\n- The extension has broad browser permissions (page access, navigation, multi-tab control).\n- If abused, it can harm user privacy and security.\n- Users must explicitly provide the token only to applications they trust.\n\nSetup:\n\n1. Open the extension side panel and copy your auth token.\n2. Set the token in your page:\n\n```typescript\nlocalStorage.setItem('PageAgentExtUserAuthToken', 'your-token')\n```\n\n## Quick Start\n\n```typescript\nimport type {\n  AgentActivity,\n  AgentStatus,\n  ExecutionResult,\n  HistoricalEvent,\n} from '@page-agent/core'\n\n// Wait for extension injection (up to 1 second)\nasync function waitForExtension(timeout = 1000): Promise<boolean> {\n  const start = Date.now()\n  while (Date.now() - start < timeout) {\n    if (window.PAGE_AGENT_EXT) return true\n    await new Promise((r) => setTimeout(r, 100))\n  }\n  return false\n}\n\n// Usage\nif (await waitForExtension()) {\n  const result = await window.PAGE_AGENT_EXT!.execute('Click the login button', {\n    baseURL: 'https://api.openai.com/v1',\n    apiKey: 'your-api-key',\n    model: 'gpt-5.2',\n    onStatusChange: (status) => console.log('Status:', status),\n    onActivity: (activity) => console.log('Activity:', activity),\n  })\n  console.log('Result:', result)\n}\n```\n\n## Global API\n\nAfter token match, the extension injects APIs into `window`.\n\n### `window.PAGE_AGENT_EXT_VERSION`\n\nExtension version string (for capability checks before using the main API).\n\n### `window.PAGE_AGENT_EXT`\n\nMain namespace object.\n\n#### `PAGE_AGENT_EXT.execute(task, config)`\n\nExecute one agent task.\n\nParameters:\n\n| Name | Type | Required | Description |\n| ---- | ---- | -------- | ----------- |\n| `task` | `string` | Yes | Task description |\n| `config` | `ExecuteConfig` | Yes | LLM settings, options, and callbacks |\n\nReturns: `Promise<ExecutionResult>`\n\n#### `PAGE_AGENT_EXT.stop()`\n\nStop the current task.\n\n## Types\n\nInstall `@page-agent/core` for complete types:\n\n```typescript\nimport type {\n  AgentActivity,\n  AgentStatus,\n  ExecutionResult,\n  HistoricalEvent,\n} from '@page-agent/core'\n\nexport interface ExecuteConfig {\n  baseURL: string\n  model: string\n  apiKey?: string\n\n  // Include the initial tab where page JS starts. Default: true.\n  includeInitialTab?: boolean\n\n  onStatusChange?: (status: AgentStatus) => void\n  onActivity?: (activity: AgentActivity) => void\n  onHistoryUpdate?: (history: HistoricalEvent[]) => void\n}\n\nexport type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>\n```\n\n`AgentStatus`\n\n```typescript\ntype AgentStatus = 'idle' | 'running' | 'completed' | 'error'\n```\n\n`AgentActivity`\n\n```typescript\ntype AgentActivity =\n  | { type: 'thinking' }\n  | { type: 'executing'; tool: string; input: unknown }\n  | { type: 'executed'; tool: string; input: unknown; output: string; duration: number }\n  | { type: 'retrying'; attempt: number; maxAttempts: number }\n  | { type: 'error'; message: string }\n```\n\n`HistoricalEvent`\n\n```typescript\ntype HistoricalEvent =\n  | { type: 'step'; stepIndex: number; reflection: AgentReflection; action: Action }\n  | { type: 'observation'; content: string }\n  | { type: 'user_takeover' }\n  | { type: 'retry'; message: string; attempt: number; maxAttempts: number }\n  | { type: 'error'; message: string; rawResponse?: unknown }\n```\n\n`ExecutionResult`\n\n```typescript\ninterface ExecutionResult {\n  success: boolean\n  data: string\n  history: HistoricalEvent[]\n}\n```\n\n## Usage Examples\n\n### Basic Execution\n\n```typescript\nconst result = await window.PAGE_AGENT_EXT!.execute(\n  'Fill in the email field with test@example.com and click Submit',\n  {\n    baseURL: 'https://api.openai.com/v1',\n    apiKey: process.env.OPENAI_API_KEY!,\n    model: 'gpt-5.2',\n    includeInitialTab: false, // Optional: exclude current tab\n    onStatusChange: (status) => console.log(status),\n    onActivity: (activity) => console.log(activity),\n  }\n)\n```\n\n### Stop the Current Task\n\n```typescript\nwindow.PAGE_AGENT_EXT!.stop()\n```\n\n## Window Type Declaration\n\nIf you are not importing `@page-agent/core`, add:\n\n```typescript\nimport type {\n  AgentActivity,\n  AgentStatus,\n  ExecutionResult,\n  HistoricalEvent,\n} from '@page-agent/core'\n\ninterface ExecuteConfig {\n  baseURL: string\n  model: string\n  apiKey?: string\n  includeInitialTab?: boolean\n  onStatusChange?: (status: AgentStatus) => void\n  onActivity?: (activity: AgentActivity) => void\n  onHistoryUpdate?: (history: HistoricalEvent[]) => void\n}\n\ndeclare global {\n  interface Window {\n    PAGE_AGENT_EXT_VERSION?: string\n    PAGE_AGENT_EXT?: {\n      version: string\n      execute: Execute\n      stop: () => void\n    }\n  }\n}\n```\n"
  },
  {
    "path": "packages/extension/package.json",
    "content": "{\n    \"name\": \"@page-agent/ext\",\n    \"private\": true,\n    \"version\": \"1.6.0\",\n    \"type\": \"module\",\n    \"scripts\": {\n        \"dev\": \"wxt\",\n        \"build:ext\": \"wxt build\",\n        \"zip\": \"wxt zip\",\n        \"postinstall\": \"wxt prepare\"\n    },\n    \"devDependencies\": {\n        \"@radix-ui/react-hover-card\": \"^1.1.15\",\n        \"@radix-ui/react-icons\": \"^1.3.2\",\n        \"@radix-ui/react-label\": \"^2.1.8\",\n        \"@radix-ui/react-separator\": \"^1.1.8\",\n        \"@radix-ui/react-slot\": \"^1.2.4\",\n        \"@radix-ui/react-switch\": \"^1.2.6\",\n        \"@types/chrome\": \"^0.1.37\",\n        \"@types/react\": \"^19.2.14\",\n        \"@types/react-dom\": \"^19.2.1\",\n        \"@wxt-dev/module-react\": \"^1.2.2\",\n        \"class-variance-authority\": \"^0.7.1\",\n        \"clsx\": \"^2.1.1\",\n        \"idb\": \"^8.0.3\",\n        \"lucide-react\": \"^0.577.0\",\n        \"motion\": \"^12.37.0\",\n        \"next-themes\": \"^0.4.6\",\n        \"react\": \"^19.2.4\",\n        \"react-dom\": \"^19.2.4\",\n        \"rough-notation\": \"^0.5.1\",\n        \"simple-icons\": \"^16.12.0\",\n        \"sonner\": \"^2.0.7\",\n        \"tailwind-merge\": \"^3.5.0\",\n        \"tailwindcss\": \"^4.1.14\",\n        \"tw-animate-css\": \"^1.4.0\",\n        \"wxt\": \"^0.20.19\"\n    },\n    \"dependencies\": {\n        \"@page-agent/core\": \"1.6.0\",\n        \"@page-agent/llms\": \"1.6.0\",\n        \"@page-agent/page-controller\": \"1.6.0\",\n        \"@page-agent/ui\": \"1.6.0\",\n        \"ai-motion\": \"^0.4.8\",\n        \"chalk\": \"^5.6.2\"\n    },\n    \"peerDependencies\": {\n        \"zod\": \"^3.25.0 || ^4.0.0\"\n    }\n}\n"
  },
  {
    "path": "packages/extension/public/_locales/en/messages.json",
    "content": "{\n\t\"extName\": {\n\t\t\"message\": \"Page Agent Ext\"\n\t},\n\t\"extDescription\": {\n\t\t\"message\": \"AI-powered browser automation assistant. Control web pages with natural language.\"\n\t},\n\t\"extActionTitle\": {\n\t\t\"message\": \"Open Page Agent\"\n\t}\n}\n"
  },
  {
    "path": "packages/extension/public/_locales/zh_CN/messages.json",
    "content": "{\n\t\"extName\": {\n\t\t\"message\": \"Page Agent Ext\"\n\t},\n\t\"extDescription\": {\n\t\t\"message\": \"AI 驱动的浏览器自动化助手，用自然语言控制网页。\"\n\t},\n\t\"extActionTitle\": {\n\t\t\"message\": \"打开 Page Agent\"\n\t}\n}\n"
  },
  {
    "path": "packages/extension/src/agent/.prettierignore",
    "content": "system_prompt.md"
  },
  {
    "path": "packages/extension/src/agent/MultiPageAgent.ts",
    "content": "import { type AgentConfig, PageAgentCore } from '@page-agent/core'\n\nimport { RemotePageController } from './RemotePageController'\nimport { TabsController } from './TabsController'\nimport SYSTEM_PROMPT from './system_prompt.md?raw'\nimport { createTabTools } from './tabTools'\n\n/** Detect user language from browser settings */\nfunction detectLanguage(): 'en-US' | 'zh-CN' {\n\tconst lang = navigator.language || navigator.languages?.[0] || 'en-US'\n\treturn lang.startsWith('zh') ? 'zh-CN' : 'en-US'\n}\n\n/**\n * MultiPageAgent\n * - use with extension\n * - can be used from a side panel or a content script\n */\nexport class MultiPageAgent extends PageAgentCore {\n\tconstructor(config: AgentConfig & { includeInitialTab?: boolean }) {\n\t\t// multi page controller\n\t\tconst tabsController = new TabsController()\n\t\tconst pageController = new RemotePageController(tabsController)\n\t\tconst customTools = createTabTools(tabsController)\n\n\t\t// system prompt - auto-detect language if not specified\n\t\tconst language = config.language ?? detectLanguage()\n\t\tconst targetLanguage = language === 'zh-CN' ? '中文' : 'English'\n\t\tconst systemPrompt = SYSTEM_PROMPT.replace(\n\t\t\t/Default working language: \\*\\*.*?\\*\\*/,\n\t\t\t`Default working language: **${targetLanguage}**`\n\t\t)\n\n\t\t// include initial tab for controlling\n\t\tconst includeInitialTab = config.includeInitialTab ?? true\n\n\t\t/**\n\t\t * When the agent is in side-panel and user closed the side-panel.\n\t\t * There is no chance for isAgentRunning to be set false.\n\t\t * (unload event doesn't work well in side panel.)\n\t\t * (I'm trying not to use long-lived connection because the lifecycle of a sw is hard to predict.)\n\t\t * This heartbeat mechanism acts as a backup.\n\t\t */\n\t\tlet heartBeatInterval: null | number = null\n\n\t\tsuper({\n\t\t\t...config,\n\t\t\tpageController: pageController as any,\n\t\t\tcustomTools: customTools,\n\t\t\tcustomSystemPrompt: systemPrompt,\n\n\t\t\tonBeforeTask: async (agent) => {\n\t\t\t\tawait tabsController.init(agent.task, includeInitialTab)\n\n\t\t\t\theartBeatInterval = window.setInterval(() => {\n\t\t\t\t\tchrome.storage.local.set({\n\t\t\t\t\t\tagentHeartbeat: Date.now(),\n\t\t\t\t\t})\n\t\t\t\t}, 1_000)\n\n\t\t\t\tawait chrome.storage.local.set({\n\t\t\t\t\tisAgentRunning: true,\n\t\t\t\t})\n\t\t\t},\n\n\t\t\tonAfterTask: async () => {\n\t\t\t\tif (heartBeatInterval) {\n\t\t\t\t\twindow.clearInterval(heartBeatInterval)\n\t\t\t\t\theartBeatInterval = null\n\t\t\t\t}\n\n\t\t\t\tawait chrome.storage.local.set({\n\t\t\t\t\tisAgentRunning: false,\n\t\t\t\t})\n\t\t\t},\n\n\t\t\tonBeforeStep: async (agent) => {\n\t\t\t\tif (!tabsController.currentTabId) return\n\t\t\t\t// make sure the current tab is loaded before the step starts\n\t\t\t\tawait tabsController.waitUntilTabLoaded(tabsController.currentTabId!)\n\t\t\t},\n\n\t\t\tonDispose: () => {\n\t\t\t\tif (heartBeatInterval) {\n\t\t\t\t\twindow.clearInterval(heartBeatInterval)\n\t\t\t\t\theartBeatInterval = null\n\t\t\t\t}\n\n\t\t\t\tchrome.storage.local.set({\n\t\t\t\t\tisAgentRunning: false,\n\t\t\t\t})\n\n\t\t\t\ttabsController.dispose()\n\t\t\t},\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "packages/extension/src/agent/RemotePageController.background.ts",
    "content": "/**\n * background logics for RemotePageController\n * - redirect messages from RemotePageController(Agent, extension pages) to ContentScript\n */\n\nexport function handlePageControlMessage(\n\tmessage: { type: 'PAGE_CONTROL'; action: string; payload: any; targetTabId: number },\n\tsender: chrome.runtime.MessageSender,\n\tsendResponse: (response: unknown) => void\n): true | undefined {\n\tconst PREFIX = '[RemotePageController.background]'\n\n\tfunction debug(...messages: any[]) {\n\t\tconsole.debug(`\\x1b[90m${PREFIX}\\x1b[0m`, ...messages)\n\t}\n\n\tconst { action, payload, targetTabId } = message\n\n\tif (action === 'get_my_tab_id') {\n\t\tdebug('get_my_tab_id', sender.tab?.id)\n\t\tsendResponse({ tabId: sender.tab?.id || null })\n\t\treturn\n\t}\n\n\t// proxy to content script\n\tchrome.tabs\n\t\t.sendMessage(targetTabId, {\n\t\t\ttype: 'PAGE_CONTROL',\n\t\t\taction,\n\t\t\tpayload,\n\t\t})\n\t\t.then((result) => {\n\t\t\tsendResponse(result)\n\t\t})\n\t\t.catch((error) => {\n\t\t\tconsole.error(PREFIX, error)\n\t\t\tsendResponse({\n\t\t\t\tsuccess: false,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t})\n\t\t})\n\n\treturn true // async response\n}\n"
  },
  {
    "path": "packages/extension/src/agent/RemotePageController.content.ts",
    "content": "/**\n * content script for RemotePageController\n */\nimport { PageController } from '@page-agent/page-controller'\n\nexport function initPageController() {\n\tlet pageController: PageController | null = null\n\tlet intervalID: number | null = null\n\n\tconst myTabIdPromise = chrome.runtime\n\t\t.sendMessage({ type: 'PAGE_CONTROL', action: 'get_my_tab_id' })\n\t\t.then((response) => {\n\t\t\treturn (response as { tabId: number | null }).tabId\n\t\t})\n\t\t.catch((error) => {\n\t\t\tconsole.error('[RemotePageController.ContentScript]: Failed to get my tab id', error)\n\t\t\treturn null\n\t\t})\n\n\tfunction getPC(): PageController {\n\t\tif (!pageController) {\n\t\t\tpageController = new PageController({ enableMask: false, viewportExpansion: 400 })\n\t\t}\n\t\treturn pageController\n\t}\n\n\tintervalID = window.setInterval(async () => {\n\t\tconst agentHeartbeat = (await chrome.storage.local.get('agentHeartbeat')).agentHeartbeat\n\t\tconst now = Date.now()\n\t\tconst agentInTouch = typeof agentHeartbeat === 'number' && now - agentHeartbeat < 2_000\n\n\t\tconst isAgentRunning = (await chrome.storage.local.get('isAgentRunning')).isAgentRunning\n\t\tconst currentTabId = (await chrome.storage.local.get('currentTabId')).currentTabId\n\n\t\tconst shouldShowMask = isAgentRunning && agentInTouch && currentTabId === (await myTabIdPromise)\n\n\t\tif (shouldShowMask) {\n\t\t\tconst pc = getPC()\n\t\t\tpc.initMask()\n\t\t\tawait pc.showMask()\n\t\t} else {\n\t\t\t// await getPC().hideMask()\n\t\t\tif (pageController) {\n\t\t\t\tpageController.hideMask()\n\t\t\t\tpageController.cleanUpHighlights()\n\t\t\t}\n\t\t}\n\n\t\tif (!isAgentRunning && agentInTouch) {\n\t\t\tif (pageController) {\n\t\t\t\tpageController.dispose()\n\t\t\t\tpageController = null\n\t\t\t}\n\t\t}\n\t}, 500)\n\n\tchrome.runtime.onMessage.addListener((message, sender, sendResponse): true | undefined => {\n\t\tif (message.type !== 'PAGE_CONTROL') {\n\t\t\t// sendResponse({\n\t\t\t// \tsuccess: false,\n\t\t\t// \terror: `[RemotePageController.ContentScript]: Invalid message type: ${message.type}`,\n\t\t\t// })\n\t\t\treturn\n\t\t}\n\n\t\tconst { action, payload } = message\n\t\tconst methodName = getMethodName(action)\n\n\t\tconst pc = getPC() as any\n\n\t\tswitch (action) {\n\t\t\tcase 'get_last_update_time':\n\t\t\tcase 'get_browser_state':\n\t\t\tcase 'update_tree':\n\t\t\tcase 'clean_up_highlights':\n\t\t\tcase 'click_element':\n\t\t\tcase 'input_text':\n\t\t\tcase 'select_option':\n\t\t\tcase 'scroll':\n\t\t\tcase 'scroll_horizontally':\n\t\t\tcase 'execute_javascript':\n\t\t\t\tpc[methodName](...(payload || []))\n\t\t\t\t\t.then((result: any) => sendResponse(result))\n\t\t\t\t\t.catch((error: any) =>\n\t\t\t\t\t\tsendResponse({\n\t\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\tbreak\n\n\t\t\tdefault:\n\t\t\t\tsendResponse({\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: `Unknown PAGE_CONTROL action: ${action}`,\n\t\t\t\t})\n\t\t}\n\n\t\treturn true\n\t})\n}\n\nfunction getMethodName(action: string): string {\n\tswitch (action) {\n\t\tcase 'get_last_update_time':\n\t\t\treturn 'getLastUpdateTime' as const\n\t\tcase 'get_browser_state':\n\t\t\treturn 'getBrowserState' as const\n\t\tcase 'update_tree':\n\t\t\treturn 'updateTree' as const\n\t\tcase 'clean_up_highlights':\n\t\t\treturn 'cleanUpHighlights' as const\n\n\t\t// DOM actions\n\n\t\tcase 'click_element':\n\t\t\treturn 'clickElement' as const\n\t\tcase 'input_text':\n\t\t\treturn 'inputText' as const\n\t\tcase 'select_option':\n\t\t\treturn 'selectOption' as const\n\t\tcase 'scroll':\n\t\t\treturn 'scroll' as const\n\t\tcase 'scroll_horizontally':\n\t\t\treturn 'scrollHorizontally' as const\n\t\tcase 'execute_javascript':\n\t\t\treturn 'executeJavascript' as const\n\n\t\tdefault:\n\t\t\treturn action\n\t}\n}\n"
  },
  {
    "path": "packages/extension/src/agent/RemotePageController.ts",
    "content": "import type { BrowserState } from '@page-agent/page-controller'\n\nimport type { TabsController } from './TabsController'\n\nconst PREFIX = '[RemotePageController]'\n\nfunction debug(...messages: any[]) {\n\tconsole.debug(`\\x1b[90m${PREFIX}\\x1b[0m`, ...messages)\n}\n\nfunction sendMessage(message: {\n\ttype: 'PAGE_CONTROL'\n\taction: string\n\ttargetTabId: number\n\tpayload?: any\n}): Promise<any> {\n\treturn chrome.runtime.sendMessage(message).catch((error) => {\n\t\tconsole.error(PREFIX, message.action, error)\n\t\treturn null\n\t})\n}\n\n/**\n * Agent side page controller.\n * - live in the agent env (extension page or content script)\n * - communicates with remote PageController via sw\n */\nexport class RemotePageController {\n\ttabsController: TabsController\n\n\tconstructor(tabsController: TabsController) {\n\t\tthis.tabsController = tabsController\n\t}\n\n\tget currentTabId(): number | null {\n\t\treturn this.tabsController.currentTabId\n\t}\n\n\tprivate async getCurrentUrl(): Promise<string> {\n\t\tif (!this.currentTabId) return ''\n\t\tconst { url } = await this.tabsController.getTabInfo(this.currentTabId)\n\t\treturn url || ''\n\t}\n\n\tprivate async getCurrentTitle(): Promise<string> {\n\t\tif (!this.currentTabId) return ''\n\t\tconst { title } = await this.tabsController.getTabInfo(this.currentTabId)\n\t\treturn title || ''\n\t}\n\n\tasync getLastUpdateTime(): Promise<number> {\n\t\tif (!this.currentTabId) throw new Error('tabsController not initialized.')\n\t\treturn sendMessage({\n\t\t\ttype: 'PAGE_CONTROL',\n\t\t\taction: 'get_last_update_time',\n\t\t\ttargetTabId: this.currentTabId,\n\t\t})\n\t}\n\n\tasync getBrowserState(): Promise<BrowserState> {\n\t\tlet browserState = {} as BrowserState\n\t\tdebug('getBrowserState', this.currentTabId)\n\n\t\tconst currentUrl = await this.getCurrentUrl()\n\t\tconst currentTitle = await this.getCurrentTitle()\n\n\t\tif (!this.currentTabId || !isContentScriptAllowed(currentUrl)) {\n\t\t\tbrowserState = {\n\t\t\t\turl: currentUrl,\n\t\t\t\ttitle: currentTitle,\n\t\t\t\theader: '',\n\t\t\t\tcontent: '(empty page. either current page is not readable or not loaded yet.)',\n\t\t\t\tfooter: '',\n\t\t\t}\n\t\t} else {\n\t\t\tbrowserState = await sendMessage({\n\t\t\t\ttype: 'PAGE_CONTROL',\n\t\t\t\taction: 'get_browser_state',\n\t\t\t\ttargetTabId: this.currentTabId,\n\t\t\t})\n\t\t}\n\n\t\tconst sum = await this.tabsController.summarizeTabs()\n\t\tbrowserState.header = sum + '\\n\\n' + (browserState.header || '')\n\n\t\tdebug('getBrowserState: success', this.currentTabId, browserState)\n\n\t\treturn browserState\n\t}\n\n\tasync updateTree(): Promise<void> {\n\t\tif (!this.currentTabId || !isContentScriptAllowed(await this.getCurrentUrl())) {\n\t\t\treturn\n\t\t}\n\n\t\tawait sendMessage({\n\t\t\ttype: 'PAGE_CONTROL',\n\t\t\taction: 'update_tree',\n\t\t\ttargetTabId: this.currentTabId,\n\t\t})\n\t}\n\n\tasync cleanUpHighlights(): Promise<void> {\n\t\tif (!this.currentTabId || !isContentScriptAllowed(await this.getCurrentUrl())) {\n\t\t\treturn\n\t\t}\n\n\t\tawait sendMessage({\n\t\t\ttype: 'PAGE_CONTROL',\n\t\t\taction: 'clean_up_highlights',\n\t\t\ttargetTabId: this.currentTabId,\n\t\t})\n\t}\n\n\tasync clickElement(...args: any[]): Promise<DomActionReturn> {\n\t\tconst res = await this.remoteCallDomAction('click_element', args)\n\t\t// @note may cause page navigation, wait for 1 second to ensure the page loading started\n\t\tawait new Promise((resolve) => setTimeout(resolve, 1000))\n\t\treturn res\n\t}\n\n\tasync inputText(...args: any[]): Promise<DomActionReturn> {\n\t\treturn this.remoteCallDomAction('input_text', args)\n\t}\n\n\tasync selectOption(...args: any[]): Promise<DomActionReturn> {\n\t\treturn this.remoteCallDomAction('select_option', args)\n\t}\n\n\tasync scroll(...args: any[]): Promise<DomActionReturn> {\n\t\treturn this.remoteCallDomAction('scroll', args)\n\t}\n\n\tasync scrollHorizontally(...args: any[]): Promise<DomActionReturn> {\n\t\treturn this.remoteCallDomAction('scroll_horizontally', args)\n\t}\n\n\tasync executeJavascript(...args: any[]): Promise<DomActionReturn> {\n\t\treturn this.remoteCallDomAction('execute_javascript', args)\n\t}\n\n\t/** @note Managed by content script via storage polling. */\n\tasync showMask(): Promise<void> {}\n\t/** @note Managed by content script via storage polling. */\n\tasync hideMask(): Promise<void> {}\n\t/** @note Managed by content script via storage polling. */\n\tdispose(): void {}\n\n\tprivate async remoteCallDomAction(action: string, payload: any[]): Promise<DomActionReturn> {\n\t\tif (!this.currentTabId) {\n\t\t\treturn { success: false, message: 'RemotePageController not initialized.' }\n\t\t}\n\n\t\tif (!isContentScriptAllowed(await this.getCurrentUrl())) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\tmessage:\n\t\t\t\t\t'Operation not allowed on this page. Use open_new_tab to navigate to a web page first.',\n\t\t\t}\n\t\t}\n\n\t\treturn sendMessage({\n\t\t\ttype: 'PAGE_CONTROL',\n\t\t\taction: action,\n\t\t\ttargetTabId: this.currentTabId!,\n\t\t\tpayload,\n\t\t})\n\t}\n}\n\ninterface DomActionReturn {\n\tsuccess: boolean\n\tmessage: string\n}\n\n/**\n * Check if a URL can run content scripts.\n */\nexport function isContentScriptAllowed(url: string | undefined): boolean {\n\tif (!url) return false\n\n\tconst restrictedPatterns = [\n\t\t/^chrome:\\/\\//,\n\t\t/^chrome-extension:\\/\\//,\n\t\t/^about:/,\n\t\t/^edge:\\/\\//,\n\t\t/^brave:\\/\\//,\n\t\t/^opera:\\/\\//,\n\t\t/^vivaldi:\\/\\//,\n\t\t/^file:\\/\\//,\n\t\t/^view-source:/,\n\t\t/^devtools:\\/\\//,\n\t]\n\n\treturn !restrictedPatterns.some((pattern) => pattern.test(url))\n}\n"
  },
  {
    "path": "packages/extension/src/agent/TabsController.background.ts",
    "content": "/**\n * background logics for TabsController\n */\nimport type { TabAction } from './TabsController'\n\nconst PREFIX = '[TabsController.background]'\n\nfunction debug(...messages: any[]) {\n\tconsole.debug(`\\x1b[90m${PREFIX}\\x1b[0m`, ...messages)\n}\n\nexport function handleTabControlMessage(\n\tmessage: { type: 'TAB_CONTROL'; action: TabAction; payload: any },\n\tsender: chrome.runtime.MessageSender,\n\tsendResponse: (response: unknown) => void\n): true | undefined {\n\tconst { action, payload } = message\n\n\tswitch (action as TabAction) {\n\t\tcase 'get_active_tab': {\n\t\t\tdebug('get_active_tab')\n\t\t\tchrome.tabs\n\t\t\t\t.query({ active: true, currentWindow: true })\n\t\t\t\t.then((tabs) => {\n\t\t\t\t\tconst tabId = tabs.length > 0 ? tabs[0].id || null : null\n\t\t\t\t\tdebug('get_active_tab: success', tabId)\n\t\t\t\t\tsendResponse({ success: true, tabId })\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\tsendResponse({ error: error instanceof Error ? error.message : String(error) })\n\t\t\t\t})\n\t\t\treturn true // async response\n\t\t}\n\n\t\tcase 'get_tab_info': {\n\t\t\tdebug('get_tab_info', payload)\n\t\t\tchrome.tabs\n\t\t\t\t.get(payload.tabId)\n\t\t\t\t.then((tab) => {\n\t\t\t\t\tdebug('get_tab_info: success', tab)\n\t\t\t\t\tsendResponse(tab)\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\tsendResponse({ error: error instanceof Error ? error.message : String(error) })\n\t\t\t\t})\n\t\t\treturn true // async response\n\t\t}\n\n\t\tcase 'open_new_tab': {\n\t\t\tdebug('open_new_tab', payload)\n\t\t\tchrome.tabs\n\t\t\t\t.create({ url: payload.url, active: false })\n\t\t\t\t.then((newTab) => {\n\t\t\t\t\tdebug('open_new_tab: success', newTab)\n\t\t\t\t\tsendResponse({ success: true, tabId: newTab.id })\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\tsendResponse({ error: error instanceof Error ? error.message : String(error) })\n\t\t\t\t})\n\t\t\treturn true // async response\n\t\t}\n\n\t\tcase 'create_tab_group': {\n\t\t\tdebug('create_tab_group', payload)\n\t\t\tchrome.tabs\n\t\t\t\t.group({ tabIds: payload.tabIds })\n\t\t\t\t.then((groupId) => {\n\t\t\t\t\tdebug('create_tab_group: success', groupId)\n\t\t\t\t\tsendResponse({ success: true, groupId })\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\tconsole.error(PREFIX, 'Failed to create tab group', error)\n\t\t\t\t\tsendResponse({ error: error instanceof Error ? error.message : String(error) })\n\t\t\t\t})\n\t\t\treturn true // async response\n\t\t}\n\n\t\tcase 'update_tab_group': {\n\t\t\tdebug('update_tab_group', payload)\n\t\t\tchrome.tabGroups\n\t\t\t\t.update(payload.groupId, payload.properties)\n\t\t\t\t.then(() => {\n\t\t\t\t\tsendResponse({ success: true })\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\tsendResponse({ error: error instanceof Error ? error.message : String(error) })\n\t\t\t\t})\n\t\t\treturn true // async response\n\t\t}\n\n\t\tcase 'add_tab_to_group': {\n\t\t\tdebug('add_tab_to_group', payload)\n\t\t\tchrome.tabs\n\t\t\t\t.group({ tabIds: payload.tabId, groupId: payload.groupId })\n\t\t\t\t.then(() => {\n\t\t\t\t\tsendResponse({ success: true })\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\tsendResponse({ error: error instanceof Error ? error.message : String(error) })\n\t\t\t\t})\n\t\t\treturn true // async response\n\t\t}\n\n\t\tcase 'close_tab': {\n\t\t\tdebug('close_tab', payload)\n\t\t\tchrome.tabs\n\t\t\t\t.remove(payload.tabId)\n\t\t\t\t.then(() => {\n\t\t\t\t\tsendResponse({ success: true })\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\tsendResponse({ error: error instanceof Error ? error.message : String(error) })\n\t\t\t\t})\n\t\t\treturn true // async response\n\t\t}\n\n\t\tdefault:\n\t\t\tsendResponse({ error: `Unknown action: ${action}` })\n\t\t\treturn\n\t}\n}\n\nexport function setupTabChangeEvents() {\n\tconsole.log('[TabsController.background] setupTabChangeEvents')\n\n\tchrome.tabs.onCreated.addListener((tab) => {\n\t\tdebug('onCreated', tab)\n\t\tchrome.runtime\n\t\t\t.sendMessage({ type: 'TAB_CHANGE', action: 'created', payload: { tab } })\n\t\t\t.catch((error) => {\n\t\t\t\tdebug('onCreated error:', error)\n\t\t\t})\n\t})\n\n\tchrome.tabs.onRemoved.addListener((tabId, removeInfo) => {\n\t\tdebug('onRemoved', tabId, removeInfo)\n\t\tchrome.runtime\n\t\t\t.sendMessage({\n\t\t\t\ttype: 'TAB_CHANGE',\n\t\t\t\taction: 'removed',\n\t\t\t\tpayload: { tabId, removeInfo },\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tdebug('onRemoved error:', error)\n\t\t\t})\n\t})\n\n\tchrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {\n\t\tdebug('onUpdated', tabId, changeInfo)\n\t\tchrome.runtime\n\t\t\t.sendMessage({\n\t\t\t\ttype: 'TAB_CHANGE',\n\t\t\t\taction: 'updated',\n\t\t\t\tpayload: { tabId, changeInfo, tab },\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tdebug('onUpdated error:', error)\n\t\t\t})\n\t})\n}\n"
  },
  {
    "path": "packages/extension/src/agent/TabsController.ts",
    "content": "import { isContentScriptAllowed } from './RemotePageController'\n\nconst PREFIX = '[TabsController]'\n\nfunction debug(...messages: any[]) {\n\tconsole.debug(`\\x1b[90m${PREFIX}\\x1b[0m`, ...messages)\n}\n\nfunction sendMessage(message: {\n\ttype: 'TAB_CONTROL'\n\taction: TabAction\n\tpayload?: any\n}): Promise<any> {\n\treturn chrome.runtime.sendMessage(message).catch((error) => {\n\t\tconsole.error(PREFIX, message.action, error)\n\t\treturn null\n\t})\n}\n\n/**\n * Controller for managing browser tabs.\n * - live in the agent env (extension page or content script)\n * - no chrome apis. call sw for tab operations\n */\nexport class TabsController extends EventTarget {\n\tcurrentTabId: number | null = null\n\n\tprivate tabs: TabMeta[] = []\n\tprivate initialTabId: number | null = null\n\tprivate tabGroupId: number | null = null\n\tprivate task: string = ''\n\n\tasync init(task: string, includeInitialTab: boolean = true) {\n\t\tdebug('init', task, includeInitialTab)\n\n\t\tthis.task = task\n\t\tthis.tabs = []\n\t\tthis.currentTabId = null\n\t\tthis.tabGroupId = null\n\t\tthis.initialTabId = null\n\n\t\tconst result = await sendMessage({\n\t\t\ttype: 'TAB_CONTROL',\n\t\t\taction: 'get_active_tab',\n\t\t})\n\n\t\tthis.initialTabId = result.tabId\n\n\t\tif (!this.initialTabId) {\n\t\t\tthrow new Error('Failed to get initial tab ID')\n\t\t}\n\n\t\tif (includeInitialTab) {\n\t\t\tconst info = await sendMessage({\n\t\t\t\ttype: 'TAB_CONTROL',\n\t\t\t\taction: 'get_tab_info',\n\t\t\t\tpayload: { tabId: this.initialTabId },\n\t\t\t})\n\n\t\t\tif (isContentScriptAllowed(info.url)) {\n\t\t\t\tthis.currentTabId = this.initialTabId\n\n\t\t\t\tthis.tabs.push({\n\t\t\t\t\tid: result.tabId,\n\t\t\t\t\tisInitial: true,\n\t\t\t\t\turl: info.url,\n\t\t\t\t\ttitle: info.title,\n\t\t\t\t\tstatus: info.status,\n\t\t\t\t})\n\n\t\t\t\tawait this.createTabGroup([this.initialTabId])\n\t\t\t}\n\t\t}\n\n\t\tawait this.updateCurrentTabId(this.currentTabId)\n\n\t\tconst tabChangeHandler = (message: any): void => {\n\t\t\tif (message.type !== 'TAB_CHANGE') {\n\t\t\t\t// throw new Error(`[TabsController]: Invalid message type: ${message.type}`)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (message.action === 'created') {\n\t\t\t\tconst tab = message.payload.tab as chrome.tabs.Tab\n\t\t\t\tif (tab.groupId === this.tabGroupId && tab.id != null) {\n\t\t\t\t\t// Tab created in our controlled group\n\t\t\t\t\tif (!this.tabs.find((t) => t.id === tab.id)) {\n\t\t\t\t\t\tthis.tabs.push({ id: tab.id, isInitial: false })\n\t\t\t\t\t}\n\t\t\t\t\tthis.switchToTab(tab.id)\n\t\t\t\t}\n\t\t\t} else if (message.action === 'removed') {\n\t\t\t\tconst { tabId } = message.payload as { tabId: number }\n\t\t\t\tconst targetTab = this.tabs.find((t) => t.id === tabId)\n\t\t\t\tif (targetTab) {\n\t\t\t\t\tthis.tabs = this.tabs.filter((t) => t.id !== tabId)\n\t\t\t\t\tif (this.currentTabId === tabId) {\n\t\t\t\t\t\tconst newCurrentTab = this.tabs[this.tabs.length - 1] || null\n\t\t\t\t\t\tif (newCurrentTab) {\n\t\t\t\t\t\t\tthis.switchToTab(newCurrentTab.id)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tthis.updateCurrentTabId(null)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (message.action === 'updated') {\n\t\t\t\tconst { tabId, tab } = message.payload as { tabId: number; tab: chrome.tabs.Tab }\n\t\t\t\tconst targetTab = this.tabs.find((t) => t.id === tabId)\n\t\t\t\tif (targetTab) {\n\t\t\t\t\ttargetTab.url = tab.url\n\t\t\t\t\ttargetTab.title = tab.title\n\t\t\t\t\ttargetTab.status = tab.status\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tchrome.runtime.onMessage.addListener(tabChangeHandler)\n\n\t\tthis.addEventListener('dispose', () => {\n\t\t\tchrome.runtime.onMessage.removeListener(tabChangeHandler)\n\t\t})\n\t}\n\n\tasync openNewTab(url: string): Promise<string> {\n\t\tdebug('openNewTab', url)\n\n\t\tconst result = await sendMessage({\n\t\t\ttype: 'TAB_CONTROL',\n\t\t\taction: 'open_new_tab',\n\t\t\tpayload: { url },\n\t\t})\n\n\t\tif (!result.success) {\n\t\t\tthrow new Error(`Failed to open new tab: ${result.error}`)\n\t\t}\n\n\t\tconst tabId = result.tabId as number\n\n\t\tthis.tabs.push({\n\t\t\tid: tabId,\n\t\t\tisInitial: false,\n\t\t})\n\n\t\tawait this.switchToTab(tabId)\n\n\t\tif (!this.tabGroupId) {\n\t\t\tawait this.createTabGroup([tabId])\n\t\t} else {\n\t\t\tawait sendMessage({\n\t\t\t\ttype: 'TAB_CONTROL',\n\t\t\t\taction: 'add_tab_to_group',\n\t\t\t\tpayload: { tabId: result.tabId, groupId: this.tabGroupId },\n\t\t\t})\n\t\t}\n\n\t\tawait this.waitUntilTabLoaded(tabId)\n\n\t\treturn `✅ Opened new tab ID ${tabId} with URL ${url}`\n\t}\n\n\tasync switchToTab(tabId: number): Promise<string> {\n\t\tdebug('switchToTab', tabId)\n\n\t\tconst targetTab = this.tabs.find((t) => t.id === tabId)\n\t\tif (!targetTab) {\n\t\t\tthrow new Error(`Tab ID ${tabId} not found in tab list.`)\n\t\t}\n\n\t\tawait this.updateCurrentTabId(tabId)\n\n\t\treturn `✅ Switched to tab ID ${tabId}.`\n\t}\n\n\tasync closeTab(tabId: number): Promise<string> {\n\t\tdebug('closeTab', tabId)\n\n\t\tconst targetTab = this.tabs.find((t) => t.id === tabId)\n\t\tif (!targetTab) {\n\t\t\tthrow new Error(`Tab ID ${tabId} not found in tab list.`)\n\t\t}\n\t\tif (targetTab.isInitial) {\n\t\t\tthrow new Error(`Cannot close the initial tab ID ${tabId}.`)\n\t\t}\n\n\t\tconst result = await sendMessage({\n\t\t\ttype: 'TAB_CONTROL',\n\t\t\taction: 'close_tab',\n\t\t\tpayload: { tabId },\n\t\t})\n\n\t\tif (result.success) {\n\t\t\tthis.tabs = this.tabs.filter((t) => t.id !== tabId)\n\t\t\tif (this.currentTabId === tabId) {\n\t\t\t\tconst newCurrentTab = this.tabs[this.tabs.length - 1] || null\n\t\t\t\tif (newCurrentTab) {\n\t\t\t\t\tawait this.switchToTab(newCurrentTab.id)\n\t\t\t\t} else {\n\t\t\t\t\tawait this.updateCurrentTabId(null)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn `✅ Closed tab ID ${tabId}.`\n\t\t} else {\n\t\t\tthrow new Error(`Failed to close tab ID ${tabId}: ${result.error}`)\n\t\t}\n\t}\n\n\tprivate async createTabGroup(tabIds: number[]) {\n\t\tconst result = await sendMessage({\n\t\t\ttype: 'TAB_CONTROL',\n\t\t\taction: 'create_tab_group',\n\t\t\tpayload: { tabIds },\n\t\t})\n\n\t\tif (!result?.success) {\n\t\t\tthrow new Error(`Failed to create tab group: ${result?.error}`)\n\t\t}\n\n\t\tthis.tabGroupId = result.groupId as number\n\n\t\tawait sendMessage({\n\t\t\ttype: 'TAB_CONTROL',\n\t\t\taction: 'update_tab_group',\n\t\t\tpayload: {\n\t\t\t\tgroupId: this.tabGroupId,\n\t\t\t\tproperties: {\n\t\t\t\t\ttitle: `PageAgent(${this.task})`,\n\t\t\t\t\tcolor: randomColor(),\n\t\t\t\t\tcollapsed: false,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t}\n\n\tasync updateCurrentTabId(tabId: number | null) {\n\t\tdebug('updateCurrentTabId', tabId)\n\n\t\tthis.currentTabId = tabId\n\t\tawait chrome.storage.local.set({ currentTabId: tabId })\n\t}\n\n\tasync getTabInfo(tabId: number): Promise<{ title: string; url: string }> {\n\t\t// use cached tab info if available\n\t\tconst tabMeta = this.tabs.find((t) => t.id === tabId)\n\t\tif (tabMeta && tabMeta.url && tabMeta.title) {\n\t\t\treturn { title: tabMeta.title, url: tabMeta.url }\n\t\t}\n\n\t\t// otherwise, pull the latest tab info from the background script\n\t\tdebug('getTabInfo: pulling from background script', tabId)\n\t\tconst result = await sendMessage({\n\t\t\ttype: 'TAB_CONTROL',\n\t\t\taction: 'get_tab_info',\n\t\t\tpayload: { tabId },\n\t\t})\n\n\t\tif (tabMeta) {\n\t\t\ttabMeta.url = result.url\n\t\t\ttabMeta.title = result.title\n\t\t}\n\n\t\treturn result\n\t}\n\n\tasync summarizeTabs(): Promise<string> {\n\t\tconst summaries = [`| Tab ID | URL | Title | Current |`, `|-----|-----|-----|-----|`]\n\t\tfor (const tab of this.tabs) {\n\t\t\tconst { title, url } = await this.getTabInfo(tab.id)\n\t\t\tsummaries.push(\n\t\t\t\t`| ${tab.id} | ${url} | ${title} | ${this.currentTabId === tab.id ? '✅' : ''} |`\n\t\t\t)\n\t\t}\n\t\tif (!this.tabs.length) {\n\t\t\tsummaries.push('\\nNo tabs available. Open a tab if needed.')\n\t\t}\n\n\t\treturn summaries.join('\\n')\n\t}\n\n\tasync waitUntilTabLoaded(tabId: number): Promise<void> {\n\t\tconst tab = this.tabs.find((t) => t.id === tabId)\n\t\tif (!tab) throw new Error(`Tab ID ${tabId} not found in tab list.`)\n\n\t\tif (tab.status === 'unloaded') throw new Error(`Tab ID ${tabId} is unloaded.`)\n\t\tif (tab.status === 'complete') return\n\n\t\tdebug('waitUntilTabLoaded', tabId)\n\t\tawait waitUntil(() => tab.status === 'complete', 4_000)\n\t}\n\n\tdispose() {\n\t\tthis.dispatchEvent(new Event('dispose'))\n\t}\n}\n\nexport type TabAction =\n\t| 'get_active_tab'\n\t| 'get_tab_info'\n\t| 'open_new_tab'\n\t| 'create_tab_group'\n\t| 'update_tab_group'\n\t| 'add_tab_to_group'\n\t| 'close_tab'\n\t| 'get_tab_title'\n\ninterface TabMeta {\n\tid: number\n\tisInitial: boolean\n\turl?: string\n\ttitle?: string\n\tstatus?: 'loading' | 'unloaded' | 'complete'\n}\n\nconst TAB_GROUP_COLORS = ['blue', 'red', 'yellow', 'green', 'pink', 'purple', 'cyan'] as const\n\ntype TabGroupColor = (typeof TAB_GROUP_COLORS)[number]\n\nfunction randomColor(): TabGroupColor {\n\treturn TAB_GROUP_COLORS[Math.floor(Math.random() * TAB_GROUP_COLORS.length)]\n}\n\n/**\n * Wait until condition becomes true\n * @returns Returns when condition becomes true, throws otherwise\n * @param timeoutMS Timeout in milliseconds, default 1 minutes, throws error on timeout\n * @param error Error object to reject on timeout. If not provided, will resolve with false\n */\nexport async function waitUntil(\n\tcheck: () => boolean | Promise<boolean>,\n\ttimeoutMS = 60_000,\n\terror?: string\n): Promise<boolean> {\n\tif (await check()) return true\n\n\treturn new Promise((resolve, reject) => {\n\t\tconst start = Date.now()\n\t\tconst poll = async () => {\n\t\t\tif (await check()) return resolve(true)\n\t\t\tif (Date.now() - start > timeoutMS) {\n\t\t\t\tif (error) {\n\t\t\t\t\treturn reject(new Error(error))\n\t\t\t\t} else {\n\t\t\t\t\treturn resolve(false)\n\t\t\t\t}\n\t\t\t}\n\t\t\tsetTimeout(poll, 100)\n\t\t}\n\t\tsetTimeout(poll, 100)\n\t})\n}\n"
  },
  {
    "path": "packages/extension/src/agent/constants.ts",
    "content": "import type { LLMConfig } from '@page-agent/llms'\n\n// Demo LLM for testing\nexport const DEMO_MODEL = 'qwen3.5-plus'\nexport const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'\n// export const DEMO_API_KEY = 'NA'\n\nexport const DEMO_CONFIG: LLMConfig = {\n\tbaseURL: DEMO_BASE_URL,\n\tmodel: DEMO_MODEL,\n\t// apiKey: DEMO_API_KEY,\n}\n\n/** Legacy testing endpoints that should be auto-migrated to DEMO_BASE_URL */\nexport const LEGACY_TESTING_ENDPOINTS = [\n\t'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy',\n]\n\nexport function isTestingEndpoint(url: string): boolean {\n\tconst normalized = url.replace(/\\/+$/, '')\n\treturn normalized === DEMO_BASE_URL || LEGACY_TESTING_ENDPOINTS.some((ep) => normalized === ep)\n}\n\nexport function migrateLegacyEndpoint(config: LLMConfig): LLMConfig {\n\tconst normalized = config.baseURL.replace(/\\/+$/, '')\n\tif (LEGACY_TESTING_ENDPOINTS.some((ep) => normalized === ep)) {\n\t\treturn { ...DEMO_CONFIG }\n\t}\n\treturn config\n}\n"
  },
  {
    "path": "packages/extension/src/agent/system_prompt.md",
    "content": "You are an AI agent designed to operate in an iterative loop to automate browser tasks. Your ultimate goal is accomplishing the task provided in <user_request>.\n\n<intro>\nYou excel at following tasks:\n1. Navigating complex websites and extracting precise information\n2. Automating form submissions and interactive web actions\n3. Gathering and saving information \n4. Operate effectively in an agent loop\n5. Efficiently performing diverse web tasks\n</intro>\n\n<language_settings>\n- Default working language: **English**\n- Use the language that user is using. Return in user's language.\n</language_settings>\n\n<input>\nAt every step, your input will consist of: \n1. <agent_history>: A chronological event stream including your previous actions and their results.\n2. <agent_state>: Current <user_request> and <step_info>.\n3. <browser_state>: Tabs, Current Tab, Current URL, interactive elements indexed for actions, and visible page content.\n</input>\n\n<agent_history>\nAgent history will be given as a list of step information as follows:\n\n<step_{step_number}>:\nEvaluation of Previous Step: Assessment of last action\nMemory: Your memory of this step\nNext Goal: Your goal for this step\nAction Results: Your actions and their results\n</step_{step_number}>\n\nand system messages wrapped in <sys> tag.\n</agent_history>\n\n<user_request>\nUSER REQUEST: This is your ultimate objective and always remains visible.\n- This has the highest priority. Make the user happy.\n- If the user request is very specific - then carefully follow each step and dont skip or hallucinate steps.\n- If the task is open ended you can plan yourself how to get it done.\n</user_request>\n\n<browser_state>\n1. Browser State will be given as:\n\nOpen Tabs: Open tabs with their ids.\nCurrent Tab: The tab you are currently viewing.\nCurrent URL: URL of the page you are currently viewing.\nInteractive Elements: All interactive elements will be provided in format as [index]<type>text</type> where\n- index: Numeric identifier for interaction\n- type: HTML element type (button, input, etc.)\n- text: Element description\n\nExamples:\n[33]<div>User form</div>\n\\t*[35]<button aria-label='Submit form'>Submit</button>\n\nNote that:\n- Only elements with numeric indexes in [] are interactive\n- (stacked) indentation (with \\t) is important and means that the element is a (html) child of the element above (with a lower index)\n- Elements tagged with `*[` are the new clickable elements that appeared on the website since the last step - if url has not changed.\n- Pure text elements without [] are not interactive.\n</browser_state>\n\n<browser_rules>\nStrictly follow these rules while using the browser and navigating the web:\n- Only interact with elements that have a numeric [index] assigned.\n- Only use indexes that are explicitly provided.\n- If the page changes after, for example, an input text action, analyze if you need to interact with new elements, e.g. selecting the right option from the list.\n- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.\n- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).\n- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.\n- If a captcha appears, tell user you can not solve captcha. Finish the task and ask user to solve it.\n- If expected elements are missing, try scrolling, or navigating back.\n- If the page is not fully loaded, use the `wait` action.\n- Do not repeat one action for more than 3 times unless some conditions changed.\n- If you fill an input field and your action sequence is interrupted, most often something changed e.g. suggestions popped up under the field.\n- If the <user_request> includes specific page information such as product type, rating, price, location, etc., try to apply filters to be more efficient.\n- The <user_request> is the ultimate goal. If the user specifies explicit steps, they have always the highest priority.\n- If you input_text into a field, you might need to press enter, click the search button, or select from dropdown for completion.\n- Don't login into a page if you don't have to. Don't login if you don't have the credentials. \n- There are 2 types of tasks always first think which type of request you are dealing with:\n1. Very specific step by step instructions:\n- Follow them as very precise and don't skip steps. Try to complete everything as requested.\n2. Open ended tasks. Plan yourself, be creative in achieving them.\n- If you get stuck e.g. with logins or captcha in open-ended tasks you can re-evaluate the task and try alternative ways, e.g. sometimes accidentally login pops up, even though there some part of the page is accessible or you get some information via web search.\n</browser_rules>\n\n<task_completion_rules>\nYou must call the `done` action in one of three cases:\n- When you have fully completed the USER REQUEST.\n- When you reach the final allowed step (`max_steps`), even if the task is incomplete.\n- When you feel stuck or unable to solve user request. Or user request is not clear or contains inappropriate content.\n- When it is ABSOLUTELY IMPOSSIBLE to continue.\n\nThe `done` action is your opportunity to terminate and share your findings with the user.\n- Set `success` to `true` only if the full USER REQUEST has been completed with no missing components.\n- If any part of the request is missing, incomplete, or uncertain, set `success` to `false`.\n- You can use the `text` field of the `done` action to communicate your findings and to provide a coherent reply to the user and fulfill the USER REQUEST.\n- You are ONLY ALLOWED to call `done` as a single action. Don't call it together with other actions.\n- If the user asks for specified format, such as \"return JSON with following structure\", \"return a list of format...\", MAKE sure to use the right format in your answer.\n- If the user asks for a structured output, your `done` action's schema may be modified. Take this schema into account when solving the task!\n</task_completion_rules>\n\n<reasoning_rules>\nExhibit the following reasoning patterns to successfully achieve the <user_request>:\n\n- Reason about <agent_history> to track progress and context toward <user_request>.\n- Analyze the most recent \"Next Goal\" and \"Action Result\" in <agent_history> and clearly state what you previously tried to achieve.\n- Analyze all relevant items in <agent_history> and <browser_state> to understand your state.\n- Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because it appears to be executed in your last step in <agent_history>. If the expected change is missing, mark the last action as failed (or uncertain) and plan a recovery.\n- Analyze whether you are stuck, e.g. when you repeat the same actions multiple times without any progress. Then consider alternative approaches e.g. scrolling for more context or ask user for help.\n- Ask user for help if you have any difficulty. Keep user in the loop.\n- If you see information relevant to <user_request>, plan saving the information to memory.\n- Always reason about the <user_request>. Make sure to carefully analyze the specific steps and information required. E.g. specific filters, specific form fields, specific information to search. Make sure to always compare the current trajectory with the user request and think carefully if thats how the user requested it.\n</reasoning_rules>\n\n<examples>\nHere are examples of good output patterns. Use them as reference but never copy them directly.\n\n<evaluation_examples>\n\"evaluation_previous_goal\": \"Successfully navigated to the product page and found the target information. Verdict: Success\"\n\"evaluation_previous_goal\": \"Clicked the login button and user authentication form appeared. Verdict: Success\"\n</evaluation_examples>\n\n<memory_examples>\n\"memory\": \"Found many pending reports that need to be analyzed in the main page. Successfully processed the first 2 reports on quarterly sales data and moving on to inventory analysis and customer feedback reports.\"\n</memory_examples>\n\n<next_goal_examples>\n\"next_goal\": \"Click on the 'Add to Cart' button to proceed with the purchase flow.\"\n</next_goal_examples>\n</examples>\n\n<output>\n{\n  \"evaluation_previous_goal\": \"Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.\",\n  \"memory\": \"1-3 concise sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.\",\n  \"next_goal\": \"State the next immediate goal and action to achieve it, in one clear sentence.\",\n  \"action\":{\n    \"Action name\": {// Action parameters}\n  }\n}\n</output>\n"
  },
  {
    "path": "packages/extension/src/agent/tabTools.ts",
    "content": "/**\n * Tab control tools for browser extension\n *\n * These tools allow the agent to manage multiple browser tabs:\n * - open_new_tab: Open a new tab and set it as current\n * - switch_to_tab: Switch to an existing tab\n * - close_tab: Close a tab (optionally switch to another)\n */\nimport * as z from 'zod/v4'\n\nimport type { TabsController } from './TabsController'\n\n/** Tool definition compatible with PageAgentCore customTools */\ninterface TabTool {\n\tdescription: string\n\tinputSchema: z.ZodType\n\texecute: (input: unknown) => Promise<string>\n}\n\n/**\n * Create tab control tools bound to a TabsManager instance.\n * These tools are injected into PageAgentCore via customTools config.\n */\nexport function createTabTools(tabsController: TabsController): Record<string, TabTool> {\n\treturn {\n\t\topen_new_tab: {\n\t\t\tdescription:\n\t\t\t\t'Open a new browser tab with the specified URL. The new tab becomes the current tab for all subsequent page operations.',\n\t\t\tinputSchema: z.object({\n\t\t\t\turl: z.string().describe('The URL to open in the new tab'),\n\t\t\t}),\n\t\t\texecute: async (input: unknown) => {\n\t\t\t\tconst { url } = input as { url: string }\n\t\t\t\ttry {\n\t\t\t\t\treturn await tabsController.openNewTab(url)\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn `❌ Failed: ${error instanceof Error ? error.message : String(error)}`\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\tswitch_to_tab: {\n\t\t\tdescription:\n\t\t\t\t'Switch to an existing tab by its ID. After switching, all page operations will target the new current tab. You can only switch to tabs in the tab list shown in browser state.',\n\t\t\tinputSchema: z.object({\n\t\t\t\ttab_id: z.number().int().describe('The tab ID to switch to'),\n\t\t\t}),\n\t\t\texecute: async (input: unknown) => {\n\t\t\t\tconst { tab_id } = input as { tab_id: number }\n\t\t\t\ttry {\n\t\t\t\t\treturn await tabsController.switchToTab(tab_id)\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn `❌ Failed: ${error instanceof Error ? error.message : String(error)}`\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\tclose_tab: {\n\t\t\tdescription:\n\t\t\t\t'Close a tab by its ID. Cannot close the initial tab. Optionally specify which tab to switch to after closing.',\n\t\t\tinputSchema: z.object({\n\t\t\t\ttab_id: z.number().int().describe('The tab ID to close'),\n\t\t\t}),\n\t\t\texecute: async (input: unknown) => {\n\t\t\t\tconst { tab_id } = input as { tab_id: number }\n\t\t\t\ttry {\n\t\t\t\t\treturn await tabsController.closeTab(tab_id)\n\t\t\t\t} catch (error) {\n\t\t\t\t\treturn `❌ Failed: ${error instanceof Error ? error.message : String(error)}`\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "packages/extension/src/agent/useAgent.ts",
    "content": "/**\n * React hook for using AgentController\n */\nimport type {\n\tAgentActivity,\n\tAgentStatus,\n\tExecutionResult,\n\tHistoricalEvent,\n\tSupportedLanguage,\n} from '@page-agent/core'\nimport type { LLMConfig } from '@page-agent/llms'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { MultiPageAgent } from './MultiPageAgent'\nimport { DEMO_CONFIG, migrateLegacyEndpoint } from './constants'\n\n/** Language preference: undefined means follow system */\nexport type LanguagePreference = SupportedLanguage | undefined\n\nexport interface AdvancedConfig {\n\tmaxSteps?: number\n\tsystemInstruction?: string\n\texperimentalLlmsTxt?: boolean\n\tdisableNamedToolChoice?: boolean\n}\n\nexport interface ExtConfig extends LLMConfig, AdvancedConfig {\n\tlanguage?: LanguagePreference\n}\n\nexport interface UseAgentResult {\n\tstatus: AgentStatus\n\thistory: HistoricalEvent[]\n\tactivity: AgentActivity | null\n\tcurrentTask: string\n\tconfig: ExtConfig | null\n\texecute: (task: string) => Promise<ExecutionResult>\n\tstop: () => void\n\tconfigure: (config: ExtConfig) => Promise<void>\n}\n\nexport function useAgent(): UseAgentResult {\n\tconst agentRef = useRef<MultiPageAgent | null>(null)\n\tconst [status, setStatus] = useState<AgentStatus>('idle')\n\tconst [history, setHistory] = useState<HistoricalEvent[]>([])\n\tconst [activity, setActivity] = useState<AgentActivity | null>(null)\n\tconst [currentTask, setCurrentTask] = useState('')\n\tconst [config, setConfig] = useState<ExtConfig | null>(null)\n\n\tuseEffect(() => {\n\t\tchrome.storage.local.get(['llmConfig', 'language', 'advancedConfig']).then((result) => {\n\t\t\tlet llmConfig = (result.llmConfig as LLMConfig) ?? DEMO_CONFIG\n\t\t\tconst language = (result.language as SupportedLanguage) || undefined\n\t\t\tconst advancedConfig = (result.advancedConfig as AdvancedConfig) ?? {}\n\n\t\t\t// Auto-migrate legacy testing endpoints\n\t\t\tconst migrated = migrateLegacyEndpoint(llmConfig)\n\t\t\tif (migrated !== llmConfig) {\n\t\t\t\tllmConfig = migrated\n\t\t\t\tchrome.storage.local.set({ llmConfig: migrated })\n\t\t\t} else if (!result.llmConfig) {\n\t\t\t\tchrome.storage.local.set({ llmConfig: DEMO_CONFIG })\n\t\t\t}\n\n\t\t\tsetConfig({ ...llmConfig, ...advancedConfig, language })\n\t\t})\n\t}, [])\n\n\tuseEffect(() => {\n\t\tif (!config) return\n\n\t\tconst { systemInstruction, ...agentConfig } = config\n\t\tconst agent = new MultiPageAgent({\n\t\t\t...agentConfig,\n\t\t\tinstructions: systemInstruction ? { system: systemInstruction } : undefined,\n\t\t})\n\t\tagentRef.current = agent\n\n\t\tconst handleStatusChange = (e: Event) => {\n\t\t\tconst newStatus = agent.status as AgentStatus\n\t\t\tsetStatus(newStatus)\n\t\t\tif (newStatus === 'idle' || newStatus === 'completed' || newStatus === 'error') {\n\t\t\t\tsetActivity(null)\n\t\t\t}\n\t\t}\n\n\t\tconst handleHistoryChange = (e: Event) => {\n\t\t\tsetHistory([...agent.history])\n\t\t}\n\n\t\tconst handleActivity = (e: Event) => {\n\t\t\tconst newActivity = (e as CustomEvent).detail as AgentActivity\n\t\t\tsetActivity(newActivity)\n\t\t}\n\n\t\tagent.addEventListener('statuschange', handleStatusChange)\n\t\tagent.addEventListener('historychange', handleHistoryChange)\n\t\tagent.addEventListener('activity', handleActivity)\n\n\t\treturn () => {\n\t\t\tagent.removeEventListener('statuschange', handleStatusChange)\n\t\t\tagent.removeEventListener('historychange', handleHistoryChange)\n\t\t\tagent.removeEventListener('activity', handleActivity)\n\t\t\tagent.dispose()\n\t\t}\n\t}, [config])\n\n\tconst execute = useCallback(async (task: string) => {\n\t\tconst agent = agentRef.current\n\t\tconsole.log('🚀 [useAgent] start executing task:', task)\n\t\tif (!agent) throw new Error('Agent not initialized')\n\n\t\tsetCurrentTask(task)\n\t\tsetHistory([])\n\t\treturn agent.execute(task)\n\t}, [])\n\n\tconst stop = useCallback(() => {\n\t\tagentRef.current?.stop()\n\t}, [])\n\n\tconst configure = useCallback(\n\t\tasync ({\n\t\t\tlanguage,\n\t\t\tmaxSteps,\n\t\t\tsystemInstruction,\n\t\t\texperimentalLlmsTxt,\n\t\t\tdisableNamedToolChoice,\n\t\t\t...llmConfig\n\t\t}: ExtConfig) => {\n\t\t\tawait chrome.storage.local.set({ llmConfig })\n\t\t\tif (language) {\n\t\t\t\tawait chrome.storage.local.set({ language })\n\t\t\t} else {\n\t\t\t\tawait chrome.storage.local.remove('language')\n\t\t\t}\n\t\t\tconst advancedConfig: AdvancedConfig = {\n\t\t\t\tmaxSteps,\n\t\t\t\tsystemInstruction,\n\t\t\t\texperimentalLlmsTxt,\n\t\t\t\tdisableNamedToolChoice,\n\t\t\t}\n\t\t\tawait chrome.storage.local.set({ advancedConfig })\n\t\t\tsetConfig({ ...llmConfig, ...advancedConfig, language })\n\t\t},\n\t\t[]\n\t)\n\n\treturn {\n\t\tstatus,\n\t\thistory,\n\t\tactivity,\n\t\tcurrentTask,\n\t\tconfig,\n\t\texecute,\n\t\tstop,\n\t\tconfigure,\n\t}\n}\n"
  },
  {
    "path": "packages/extension/src/assets/index.css",
    "content": "@import 'tailwindcss';\n@import 'tw-animate-css';\n\n@custom-variant dark (&:is(.dark *));\n\n:root {\n\t--background: oklch(1 0 0);\n\t--foreground: oklch(0.145 0 0);\n\t--card: oklch(1 0 0);\n\t--card-foreground: oklch(0.145 0 0);\n\t--popover: oklch(1 0 0);\n\t--popover-foreground: oklch(0.145 0 0);\n\t--primary: oklch(0.205 0 0);\n\t--primary-foreground: oklch(0.985 0 0);\n\t--secondary: oklch(0.97 0 0);\n\t--secondary-foreground: oklch(0.205 0 0);\n\t--muted: oklch(0.97 0 0);\n\t--muted-foreground: oklch(0.556 0 0);\n\t--accent: oklch(0.97 0 0);\n\t--accent-foreground: oklch(0.205 0 0);\n\t--destructive: oklch(0.577 0.245 27.325);\n\t--destructive-foreground: oklch(0.577 0.245 27.325);\n\t--border: oklch(0.922 0 0);\n\t--input: oklch(0.922 0 0);\n\t--ring: oklch(0.708 0 0);\n\t--chart-1: oklch(0.646 0.222 41.116);\n\t--chart-2: oklch(0.6 0.118 184.704);\n\t--chart-3: oklch(0.398 0.07 227.392);\n\t--chart-4: oklch(0.828 0.189 84.429);\n\t--chart-5: oklch(0.769 0.188 70.08);\n\t--radius: 0.625rem;\n\t--sidebar: oklch(0.985 0 0);\n\t--sidebar-foreground: oklch(0.145 0 0);\n\t--sidebar-primary: oklch(0.205 0 0);\n\t--sidebar-primary-foreground: oklch(0.985 0 0);\n\t--sidebar-accent: oklch(0.97 0 0);\n\t--sidebar-accent-foreground: oklch(0.205 0 0);\n\t--sidebar-border: oklch(0.922 0 0);\n\t--sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n\t--background: oklch(0.19 0 0);\n\t--foreground: oklch(0.985 0 0);\n\t--card: oklch(0.145 0 0);\n\t--card-foreground: oklch(0.985 0 0);\n\t--popover: oklch(0.145 0 0);\n\t--popover-foreground: oklch(0.985 0 0);\n\t--primary: oklch(0.985 0 0);\n\t--primary-foreground: oklch(0.205 0 0);\n\t--secondary: oklch(0.269 0 0);\n\t--secondary-foreground: oklch(0.985 0 0);\n\t--muted: oklch(0.269 0 0);\n\t--muted-foreground: oklch(0.708 0 0);\n\t--accent: oklch(0.269 0 0);\n\t--accent-foreground: oklch(0.985 0 0);\n\t--destructive: oklch(0.396 0.141 25.723);\n\t--destructive-foreground: oklch(0.637 0.237 25.331);\n\t--border: oklch(0.269 0 0);\n\t--input: oklch(0.269 0 0);\n\t--ring: oklch(0.439 0 0);\n\t--chart-1: oklch(0.488 0.243 264.376);\n\t--chart-2: oklch(0.696 0.17 162.48);\n\t--chart-3: oklch(0.769 0.188 70.08);\n\t--chart-4: oklch(0.627 0.265 303.9);\n\t--chart-5: oklch(0.645 0.246 16.439);\n\t--sidebar: oklch(0.205 0 0);\n\t--sidebar-foreground: oklch(0.985 0 0);\n\t--sidebar-primary: oklch(0.488 0.243 264.376);\n\t--sidebar-primary-foreground: oklch(0.985 0 0);\n\t--sidebar-accent: oklch(0.269 0 0);\n\t--sidebar-accent-foreground: oklch(0.985 0 0);\n\t--sidebar-border: oklch(0.269 0 0);\n\t--sidebar-ring: oklch(0.439 0 0);\n}\n\n@theme inline {\n\t--color-background: var(--background);\n\t--color-foreground: var(--foreground);\n\t--color-card: var(--card);\n\t--color-card-foreground: var(--card-foreground);\n\t--color-popover: var(--popover);\n\t--color-popover-foreground: var(--popover-foreground);\n\t--color-primary: var(--primary);\n\t--color-primary-foreground: var(--primary-foreground);\n\t--color-secondary: var(--secondary);\n\t--color-secondary-foreground: var(--secondary-foreground);\n\t--color-muted: var(--muted);\n\t--color-muted-foreground: var(--muted-foreground);\n\t--color-accent: var(--accent);\n\t--color-accent-foreground: var(--accent-foreground);\n\t--color-destructive: var(--destructive);\n\t--color-destructive-foreground: var(--destructive-foreground);\n\t--color-border: var(--border);\n\t--color-input: var(--input);\n\t--color-ring: var(--ring);\n\t--color-chart-1: var(--chart-1);\n\t--color-chart-2: var(--chart-2);\n\t--color-chart-3: var(--chart-3);\n\t--color-chart-4: var(--chart-4);\n\t--color-chart-5: var(--chart-5);\n\t--radius-sm: calc(var(--radius) - 4px);\n\t--radius-md: calc(var(--radius) - 2px);\n\t--radius-lg: var(--radius);\n\t--radius-xl: calc(var(--radius) + 4px);\n\t--color-sidebar: var(--sidebar);\n\t--color-sidebar-foreground: var(--sidebar-foreground);\n\t--color-sidebar-primary: var(--sidebar-primary);\n\t--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n\t--color-sidebar-accent: var(--sidebar-accent);\n\t--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n\t--color-sidebar-border: var(--sidebar-border);\n\t--color-sidebar-ring: var(--sidebar-ring);\n\t--animate-blink-cursor: blink-cursor 1.2s step-end infinite;\n\t@keyframes blink-cursor {\n\t\t0%,\n\t\t49% {\n\t\t\topacity: 1;\n\t\t}\n\t\t50%,\n\t\t100% {\n\t\t\topacity: 0;\n\t\t}\n\t}\n}\n\n@keyframes glow-a {\n\t0%,\n\t100% {\n\t\topacity: 0.45;\n\t\ttransform: scale(1);\n\t}\n\t50% {\n\t\topacity: 0;\n\t\ttransform: scale(1.1);\n\t}\n}\n\n@keyframes glow-b {\n\t0%,\n\t100% {\n\t\topacity: 0;\n\t\ttransform: scale(1.1);\n\t}\n\t50% {\n\t\topacity: 0.45;\n\t\ttransform: scale(1);\n\t}\n}\n\n@layer base {\n\t* {\n\t\t@apply border-border outline-ring/50;\n\t}\n\tbody {\n\t\t@apply bg-background text-foreground;\n\t}\n}\n"
  },
  {
    "path": "packages/extension/src/components/ConfigPanel.tsx",
    "content": "import {\n\tCopy,\n\tCornerUpLeft,\n\tExternalLink,\n\tEye,\n\tEyeOff,\n\tFoldVertical,\n\tHatGlasses,\n\tHome,\n\tLoader2,\n\tScale,\n\tUnfoldVertical,\n} from 'lucide-react'\nimport { useEffect, useState } from 'react'\nimport { siGithub } from 'simple-icons'\n\nimport { DEMO_BASE_URL, DEMO_MODEL, isTestingEndpoint } from '@/agent/constants'\nimport type { ExtConfig, LanguagePreference } from '@/agent/useAgent'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Switch } from '@/components/ui/switch'\n\ninterface ConfigPanelProps {\n\tconfig: ExtConfig | null\n\tonSave: (config: ExtConfig) => Promise<void>\n\tonClose: () => void\n}\n\nexport function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {\n\tconst [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL)\n\tconst [model, setModel] = useState(config?.model || DEMO_MODEL)\n\tconst [apiKey, setApiKey] = useState(config?.apiKey)\n\tconst [language, setLanguage] = useState<LanguagePreference>(config?.language)\n\tconst [maxSteps, setMaxSteps] = useState<number | undefined>(config?.maxSteps)\n\tconst [systemInstruction, setSystemInstruction] = useState(config?.systemInstruction ?? '')\n\tconst [experimentalLlmsTxt, setExperimentalLlmsTxt] = useState(\n\t\tconfig?.experimentalLlmsTxt ?? false\n\t)\n\tconst [disableNamedToolChoice, setDisableNamedToolChoice] = useState(\n\t\tconfig?.disableNamedToolChoice ?? false\n\t)\n\tconst [advancedOpen, setAdvancedOpen] = useState(false)\n\tconst [saving, setSaving] = useState(false)\n\tconst [userAuthToken, setUserAuthToken] = useState<string>('')\n\tconst [copied, setCopied] = useState(false)\n\tconst [showToken, setShowToken] = useState(false)\n\tconst [showApiKey, setShowApiKey] = useState(false)\n\n\tuseEffect(() => {\n\t\tsetBaseURL(config?.baseURL || DEMO_BASE_URL)\n\t\tsetModel(config?.model || DEMO_MODEL)\n\t\tsetApiKey(config?.apiKey)\n\t\tsetLanguage(config?.language)\n\t\tsetMaxSteps(config?.maxSteps)\n\t\tsetSystemInstruction(config?.systemInstruction ?? '')\n\t\tsetExperimentalLlmsTxt(config?.experimentalLlmsTxt ?? false)\n\t\tsetDisableNamedToolChoice(config?.disableNamedToolChoice ?? false)\n\t}, [config])\n\n\t// Poll for user auth token every second until found\n\tuseEffect(() => {\n\t\tlet interval: NodeJS.Timeout | null = null\n\n\t\tconst fetchToken = async () => {\n\t\t\tconst result = await chrome.storage.local.get('PageAgentExtUserAuthToken')\n\t\t\tconst token = result.PageAgentExtUserAuthToken\n\t\t\tif (typeof token === 'string' && token) {\n\t\t\t\tsetUserAuthToken(token)\n\t\t\t\tif (interval) {\n\t\t\t\t\tclearInterval(interval)\n\t\t\t\t\tinterval = null\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfetchToken()\n\t\tinterval = setInterval(fetchToken, 1000)\n\n\t\treturn () => {\n\t\t\tif (interval) clearInterval(interval)\n\t\t}\n\t}, [])\n\n\tconst handleCopyToken = async () => {\n\t\tif (userAuthToken) {\n\t\t\tawait navigator.clipboard.writeText(userAuthToken)\n\t\t\tsetCopied(true)\n\t\t\tsetTimeout(() => setCopied(false), 2000)\n\t\t}\n\t}\n\n\tconst handleSave = async () => {\n\t\tsetSaving(true)\n\t\ttry {\n\t\t\tawait onSave({\n\t\t\t\tapiKey,\n\t\t\t\tbaseURL,\n\t\t\t\tmodel,\n\t\t\t\tlanguage,\n\t\t\t\tmaxSteps: maxSteps || undefined,\n\t\t\t\tsystemInstruction: systemInstruction || undefined,\n\t\t\t\texperimentalLlmsTxt,\n\t\t\t\tdisableNamedToolChoice,\n\t\t\t})\n\t\t} finally {\n\t\t\tsetSaving(false)\n\t\t}\n\t}\n\n\treturn (\n\t\t<div className=\"flex flex-col gap-4 p-4 relative\">\n\t\t\t<div className=\"flex items-center justify-between\">\n\t\t\t\t<h2 className=\"text-base font-semibold\">Settings</h2>\n\t\t\t\t<Button\n\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\tsize=\"icon-sm\"\n\t\t\t\t\tonClick={onClose}\n\t\t\t\t\tclassName=\"absolute top-2 right-3 cursor-pointer\"\n\t\t\t\t>\n\t\t\t\t\t<CornerUpLeft className=\"size-3.5\" />\n\t\t\t\t</Button>\n\t\t\t</div>\n\n\t\t\t{/* User Auth Token Section */}\n\t\t\t<div className=\"flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border\">\n\t\t\t\t<label className=\"text-xs font-medium text-muted-foreground\">User Auth Token</label>\n\t\t\t\t<p className=\"text-[10px] text-muted-foreground mb-1\">\n\t\t\t\t\tGive a website the ability to call this extension.\n\t\t\t\t</p>\n\t\t\t\t<div className=\"flex gap-2 items-center\">\n\t\t\t\t\t<Input\n\t\t\t\t\t\treadOnly\n\t\t\t\t\t\tvalue={\n\t\t\t\t\t\t\tuserAuthToken\n\t\t\t\t\t\t\t\t? showToken\n\t\t\t\t\t\t\t\t\t? userAuthToken\n\t\t\t\t\t\t\t\t\t: `${userAuthToken.slice(0, 4)}${'•'.repeat(userAuthToken.length - 8)}${userAuthToken.slice(-4)}`\n\t\t\t\t\t\t\t\t: 'Loading...'\n\t\t\t\t\t\t}\n\t\t\t\t\t\tclassName=\"text-xs h-8 font-mono bg-background\"\n\t\t\t\t\t/>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\tclassName=\"h-8 w-8 shrink-0 cursor-pointer\"\n\t\t\t\t\t\tonClick={() => setShowToken(!showToken)}\n\t\t\t\t\t\tdisabled={!userAuthToken}\n\t\t\t\t\t>\n\t\t\t\t\t\t{showToken ? <EyeOff className=\"size-3\" /> : <Eye className=\"size-3\" />}\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\tclassName=\"h-8 w-8 shrink-0 cursor-pointer\"\n\t\t\t\t\t\tonClick={handleCopyToken}\n\t\t\t\t\t\tdisabled={!userAuthToken}\n\t\t\t\t\t>\n\t\t\t\t\t\t{copied ? <span className=\"\">✓</span> : <Copy className=\"size-3\" />}\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t{/* Hub link */}\n\t\t\t<a\n\t\t\t\thref=\"/hub.html\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\tclassName=\"flex items-center justify-between p-3 rounded-md border bg-muted/50 text-xs font-medium text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors\"\n\t\t\t>\n\t\t\t\tManage Page Agent Hub\n\t\t\t\t<ExternalLink className=\"size-3\" />\n\t\t\t</a>\n\n\t\t\t<div className=\"flex flex-col gap-1.5\">\n\t\t\t\t<label className=\"text-xs text-muted-foreground\">Base URL</label>\n\t\t\t\t<Input\n\t\t\t\t\tplaceholder=\"https://api.openai.com/v1\"\n\t\t\t\t\tvalue={baseURL}\n\t\t\t\t\tonChange={(e) => setBaseURL(e.target.value)}\n\t\t\t\t\tclassName=\"text-xs h-8\"\n\t\t\t\t/>\n\t\t\t</div>\n\n\t\t\t{/* Testing API notice */}\n\t\t\t{isTestingEndpoint(baseURL) && (\n\t\t\t\t<div className=\"p-2.5 rounded-md border border-amber-500/30 bg-amber-500/5 text-[11px] text-muted-foreground leading-relaxed\">\n\t\t\t\t\t<Scale className=\"size-3 inline-block mr-1 -mt-0.5 text-amber-600\" />\n\t\t\t\t\tYou are using our testing API. By using this you agree to the{' '}\n\t\t\t\t\t<a\n\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\tclassName=\"underline hover:text-foreground\"\n\t\t\t\t\t>\n\t\t\t\t\t\tTerms of Use & Privacy Policy\n\t\t\t\t\t</a>\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t<div className=\"flex flex-col gap-1.5\">\n\t\t\t\t<label className=\"text-xs text-muted-foreground\">Model</label>\n\t\t\t\t<Input\n\t\t\t\t\tplaceholder=\"gpt-5.1\"\n\t\t\t\t\tvalue={model}\n\t\t\t\t\tonChange={(e) => setModel(e.target.value)}\n\t\t\t\t\tclassName=\"text-xs h-8\"\n\t\t\t\t/>\n\t\t\t</div>\n\n\t\t\t<div className=\"flex flex-col gap-1.5\">\n\t\t\t\t<label className=\"text-xs text-muted-foreground\">API Key</label>\n\t\t\t\t<div className=\"flex gap-2 items-center\">\n\t\t\t\t\t<Input\n\t\t\t\t\t\ttype={showApiKey ? 'text' : 'password'}\n\t\t\t\t\t\t// placeholder=\"sk-...\"\n\t\t\t\t\t\tvalue={apiKey}\n\t\t\t\t\t\tonChange={(e) => setApiKey(e.target.value)}\n\t\t\t\t\t\tclassName=\"text-xs h-8\"\n\t\t\t\t\t/>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\tclassName=\"h-8 w-8 shrink-0 cursor-pointer\"\n\t\t\t\t\t\tonClick={() => setShowApiKey(!showApiKey)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{showApiKey ? <EyeOff className=\"size-3\" /> : <Eye className=\"size-3\" />}\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div className=\"flex flex-col gap-1.5\">\n\t\t\t\t<label className=\"text-xs text-muted-foreground\">Response Language</label>\n\t\t\t\t<select\n\t\t\t\t\tvalue={language ?? ''}\n\t\t\t\t\tonChange={(e) => setLanguage((e.target.value || undefined) as LanguagePreference)}\n\t\t\t\t\tclassName=\"h-8 text-xs rounded-md border border-input bg-background px-2 cursor-pointer\"\n\t\t\t\t>\n\t\t\t\t\t<option value=\"\">System</option>\n\t\t\t\t\t<option value=\"en-US\">English</option>\n\t\t\t\t\t<option value=\"zh-CN\">中文</option>\n\t\t\t\t</select>\n\t\t\t</div>\n\n\t\t\t{/* Advanced Config */}\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tonClick={() => setAdvancedOpen(!advancedOpen)}\n\t\t\t\tclassName=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground cursor-pointer mt-1 font-bold\"\n\t\t\t>\n\t\t\t\tAdvanced\n\t\t\t\t{advancedOpen ? <FoldVertical className=\"size-3\" /> : <UnfoldVertical className=\"size-3\" />}\n\t\t\t</button>\n\n\t\t\t{advancedOpen && (\n\t\t\t\t<>\n\t\t\t\t\t<div className=\"flex flex-col gap-1.5\">\n\t\t\t\t\t\t<label className=\"text-xs text-muted-foreground\">Max Steps</label>\n\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\tplaceholder=\"40\"\n\t\t\t\t\t\t\tmin={1}\n\t\t\t\t\t\t\tmax={200}\n\t\t\t\t\t\t\tvalue={maxSteps ?? ''}\n\t\t\t\t\t\t\tonChange={(e) => setMaxSteps(e.target.value ? Number(e.target.value) : undefined)}\n\t\t\t\t\t\t\tclassName=\"text-xs h-8 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [-moz-appearance:textfield]\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div className=\"flex flex-col gap-1.5\">\n\t\t\t\t\t\t<label className=\"text-xs text-muted-foreground\">System Instruction</label>\n\t\t\t\t\t\t<textarea\n\t\t\t\t\t\t\tplaceholder=\"Additional instructions for the agent...\"\n\t\t\t\t\t\t\tvalue={systemInstruction}\n\t\t\t\t\t\t\tonChange={(e) => setSystemInstruction(e.target.value)}\n\t\t\t\t\t\t\trows={3}\n\t\t\t\t\t\t\tclassName=\"text-xs rounded-md border border-input bg-background px-3 py-2 resize-y min-h-[60px]\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<label className=\"flex items-center justify-between cursor-pointer\">\n\t\t\t\t\t\t<span className=\"text-xs text-muted-foreground\">Disable named tool_choice</span>\n\t\t\t\t\t\t<Switch checked={disableNamedToolChoice} onCheckedChange={setDisableNamedToolChoice} />\n\t\t\t\t\t</label>\n\n\t\t\t\t\t<label className=\"flex items-center justify-between cursor-pointer\">\n\t\t\t\t\t\t<span className=\"text-xs text-muted-foreground\">Experimental llms.txt support</span>\n\t\t\t\t\t\t<Switch checked={experimentalLlmsTxt} onCheckedChange={setExperimentalLlmsTxt} />\n\t\t\t\t\t</label>\n\t\t\t\t</>\n\t\t\t)}\n\n\t\t\t<div className=\"flex gap-2 mt-2\">\n\t\t\t\t<Button variant=\"outline\" onClick={onClose} className=\"flex-1 h-8 text-xs cursor-pointer\">\n\t\t\t\t\tCancel\n\t\t\t\t</Button>\n\t\t\t\t<Button\n\t\t\t\t\tonClick={handleSave}\n\t\t\t\t\tdisabled={saving}\n\t\t\t\t\tclassName=\"flex-1 h-8 text-xs cursor-pointer\"\n\t\t\t\t>\n\t\t\t\t\t{saving ? <Loader2 className=\"size-3 animate-spin\" /> : 'Save'}\n\t\t\t\t</Button>\n\t\t\t</div>\n\n\t\t\t{/* Footer */}\n\t\t\t<div className=\"mt-4 mb-4 pt-4 border-t border-border/50 flex gap-2 justify-between text-[10px] text-muted-foreground\">\n\t\t\t\t<div className=\"flex flex-col justify-between\">\n\t\t\t\t\t<span>\n\t\t\t\t\t\tVersion <span className=\"font-mono\">v{__VERSION__}</span>\n\t\t\t\t\t</span>\n\n\t\t\t\t\t<a\n\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\tclassName=\"flex items-center gap-1 hover:text-foreground\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<svg role=\"img\" viewBox=\"0 0 24 24\" className=\"size-3 fill-current\">\n\t\t\t\t\t\t\t<path d={siGithub.path} />\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t<span>Source Code</span>\n\t\t\t\t\t</a>\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"flex flex-col items-end\">\n\t\t\t\t\t<a\n\t\t\t\t\t\thref=\"https://alibaba.github.io/page-agent/\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\tclassName=\"flex items-center gap-1 hover:text-foreground\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<Home className=\"size-3\" />\n\t\t\t\t\t\t<span>Home Page</span>\n\t\t\t\t\t</a>\n\n\t\t\t\t\t<a\n\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\tclassName=\"flex items-center gap-1 hover:text-foreground\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<HatGlasses className=\"size-3\" />\n\t\t\t\t\t\t<span>Privacy</span>\n\t\t\t\t\t</a>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t{/* attribute */}\n\t\t\t<div className=\"text-[10px] text-muted-foreground bg-background fixed bottom-0 w-full flex justify-around\">\n\t\t\t\t<span className=\"leading-loose\">\n\t\t\t\t\tBuilt with ♥️ by{' '}\n\t\t\t\t\t<a\n\t\t\t\t\t\thref=\"https://github.com/gaomeng1900\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\tclassName=\"underline hover:text-foreground\"\n\t\t\t\t\t>\n\t\t\t\t\t\t@Simon\n\t\t\t\t\t</a>\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/extension/src/components/ErrorBoundary.tsx",
    "content": "import { AlertTriangle, Eraser, RotateCcw } from 'lucide-react'\nimport { Component, type ErrorInfo, type ReactNode } from 'react'\n\nimport { Button } from '@/components/ui/button'\n\ninterface Props {\n\tchildren: ReactNode\n}\n\ninterface State {\n\thasError: boolean\n\terror: Error | null\n}\n\nexport class ErrorBoundary extends Component<Props, State> {\n\tstate: State = { hasError: false, error: null }\n\n\tstatic getDerivedStateFromError(error: Error): State {\n\t\treturn { hasError: true, error }\n\t}\n\n\tcomponentDidCatch(error: Error, errorInfo: ErrorInfo) {\n\t\tconsole.error('[ErrorBoundary]', error, errorInfo.componentStack)\n\t}\n\n\thandleReload = () => {\n\t\twindow.location.reload()\n\t}\n\n\thandleResetConfig = async () => {\n\t\tawait chrome.storage.local.remove(['llmConfig', 'language', 'advancedConfig'])\n\t\twindow.location.reload()\n\t}\n\n\trender() {\n\t\tif (!this.state.hasError) {\n\t\t\treturn this.props.children\n\t\t}\n\n\t\treturn (\n\t\t\t<div className=\"flex flex-col items-center justify-center h-screen bg-background p-6 text-center\">\n\t\t\t\t<AlertTriangle className=\"size-12 text-destructive mb-4\" />\n\t\t\t\t<h2 className=\"text-lg font-semibold mb-2\">Something went wrong</h2>\n\t\t\t\t<p className=\"text-sm text-muted-foreground mb-4 max-w-xs\">\n\t\t\t\t\t{this.state.error?.message || 'An unexpected error occurred'}\n\t\t\t\t</p>\n\t\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" onClick={this.handleResetConfig}>\n\t\t\t\t\t\t<Eraser className=\"size-3.5 mr-2\" />\n\t\t\t\t\t\tReset Config\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" onClick={this.handleReload}>\n\t\t\t\t\t\t<RotateCcw className=\"size-3.5 mr-2\" />\n\t\t\t\t\t\tReload Panel\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "packages/extension/src/components/HistoryDetail.tsx",
    "content": "import { ArrowLeft, RotateCcw, Trash2 } from 'lucide-react'\nimport { useEffect, useState } from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { type SessionRecord, deleteSession, getSession } from '@/lib/db'\n\nimport { EventCard } from './cards'\n\nexport function HistoryDetail({\n\tsessionId,\n\tonBack,\n\tonRerun,\n}: {\n\tsessionId: string\n\tonBack: () => void\n\tonRerun: (task: string) => void\n}) {\n\tconst [session, setSession] = useState<SessionRecord | null>(null)\n\n\tuseEffect(() => {\n\t\tgetSession(sessionId).then((s) => setSession(s ?? null))\n\t}, [sessionId])\n\n\tif (!session) {\n\t\treturn (\n\t\t\t<div className=\"flex items-center justify-center h-screen text-xs text-muted-foreground\">\n\t\t\t\tLoading...\n\t\t\t</div>\n\t\t)\n\t}\n\n\treturn (\n\t\t<div className=\"flex flex-col h-screen bg-background\">\n\t\t\t{/* Header */}\n\t\t\t<header className=\"flex items-center gap-2 border-b px-3 py-2\">\n\t\t\t\t<Button variant=\"ghost\" size=\"icon-sm\" onClick={onBack} className=\"cursor-pointer\">\n\t\t\t\t\t<ArrowLeft className=\"size-3.5\" />\n\t\t\t\t</Button>\n\t\t\t\t<span className=\"text-sm font-medium truncate\">History</span>\n\t\t\t</header>\n\n\t\t\t{/* Task */}\n\t\t\t<div className=\"border-b px-3 py-2 bg-muted/30\">\n\t\t\t\t<div className=\"text-[10px] text-muted-foreground uppercase tracking-wide\">Task</div>\n\t\t\t\t<div className=\"text-xs font-medium\" title={session.task}>\n\t\t\t\t\t{session.task}\n\t\t\t\t</div>\n\t\t\t\t<div className=\"mt-2 flex items-center gap-2\">\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tonClick={() => onRerun(session.task)}\n\t\t\t\t\t\tclassName=\"flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<RotateCcw className=\"size-3\" />\n\t\t\t\t\t\tRun again\n\t\t\t\t\t</button>\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tonClick={async () => {\n\t\t\t\t\t\t\tawait deleteSession(sessionId)\n\t\t\t\t\t\t\tonBack()\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"flex items-center gap-1 text-[10px] text-muted-foreground hover:text-destructive transition-colors cursor-pointer\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<Trash2 className=\"size-3\" />\n\t\t\t\t\t\tDelete\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t{/* Events (read-only) */}\n\t\t\t<div className=\"flex-1 overflow-y-auto p-3 space-y-2\">\n\t\t\t\t{session.history.map((event, index) => (\n\t\t\t\t\t// eslint-disable-next-line react-x/no-array-index-key\n\t\t\t\t\t<EventCard key={index} event={event} />\n\t\t\t\t))}\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/extension/src/components/HistoryList.tsx",
    "content": "import { ArrowDownToLine, ArrowLeft, CheckCircle, RotateCcw, Trash2, XCircle } from 'lucide-react'\nimport { useCallback, useEffect, useState } from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { type SessionRecord, clearSessions, deleteSession, listSessions } from '@/lib/db'\nimport { downloadHistoryExport } from '@/lib/history-export'\n\nfunction timeAgo(ts: number): string {\n\tconst seconds = Math.floor((Date.now() - ts) / 1000)\n\tif (seconds < 60) return 'just now'\n\tconst minutes = Math.floor(seconds / 60)\n\tif (minutes < 60) return `${minutes}m ago`\n\tconst hours = Math.floor(minutes / 60)\n\tif (hours < 24) return `${hours}h ago`\n\tconst days = Math.floor(hours / 24)\n\treturn `${days}d ago`\n}\n\nexport function HistoryList({\n\tonSelect,\n\tonBack,\n\tonRerun,\n}: {\n\tonSelect: (id: string) => void\n\tonBack: () => void\n\tonRerun: (task: string) => void\n}) {\n\tconst [sessions, setSessions] = useState<SessionRecord[]>([])\n\tconst [loading, setLoading] = useState(true)\n\n\tconst load = useCallback(async () => {\n\t\tsetSessions(await listSessions())\n\t\tsetLoading(false)\n\t}, [])\n\n\tuseEffect(() => {\n\t\t// eslint-disable-next-line react-hooks/set-state-in-effect\n\t\tload()\n\t}, [load])\n\n\tconst handleDelete = async (e: React.MouseEvent, id: string) => {\n\t\te.stopPropagation()\n\t\tawait deleteSession(id)\n\t\tsetSessions((prev) => prev.filter((s) => s.id !== id))\n\t}\n\n\tconst handleExport = (e: React.MouseEvent, session: SessionRecord) => {\n\t\te.stopPropagation()\n\t\tdownloadHistoryExport(session.task, session.createdAt, session.history)\n\t}\n\n\tconst handleRerun = (e: React.MouseEvent, task: string) => {\n\t\te.stopPropagation()\n\t\tonRerun(task)\n\t}\n\n\treturn (\n\t\t<div className=\"flex flex-col h-screen bg-background\">\n\t\t\t{/* Header */}\n\t\t\t<header className=\"flex items-center gap-2 border-b px-3 py-2\">\n\t\t\t\t<Button variant=\"ghost\" size=\"icon-sm\" onClick={onBack} className=\"cursor-pointer\">\n\t\t\t\t\t<ArrowLeft className=\"size-3.5\" />\n\t\t\t\t</Button>\n\t\t\t\t<span className=\"text-sm font-medium flex-1\">History</span>\n\t\t\t\t{sessions.length > 0 && (\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\tonClick={async () => {\n\t\t\t\t\t\t\tawait clearSessions()\n\t\t\t\t\t\t\tsetSessions([])\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"text-[10px] text-muted-foreground hover:text-destructive cursor-pointer h-6 px-2\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<Trash2 className=\"size-3 mr-1\" />\n\t\t\t\t\t\tClear All\n\t\t\t\t\t</Button>\n\t\t\t\t)}\n\t\t\t</header>\n\n\t\t\t{/* List */}\n\t\t\t<div className=\"flex-1 overflow-y-auto\">\n\t\t\t\t{loading && (\n\t\t\t\t\t<div className=\"flex items-center justify-center h-32 text-xs text-muted-foreground\">\n\t\t\t\t\t\tLoading...\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\n\t\t\t\t{!loading && sessions.length === 0 && (\n\t\t\t\t\t<div className=\"flex items-center justify-center h-32 text-xs text-muted-foreground\">\n\t\t\t\t\t\tNo history yet\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\n\t\t\t\t{sessions.map((session) => (\n\t\t\t\t\t<div\n\t\t\t\t\t\tkey={session.id}\n\t\t\t\t\t\trole=\"button\"\n\t\t\t\t\t\ttabIndex={0}\n\t\t\t\t\t\tonClick={() => onSelect(session.id)}\n\t\t\t\t\t\tclassName=\"w-full text-left px-3 py-2.5 border-b hover:bg-muted/50 transition-colors cursor-pointer flex items-start gap-2 group\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{/* Status icon */}\n\t\t\t\t\t\t{session.status === 'completed' ? (\n\t\t\t\t\t\t\t<CheckCircle className=\"size-3.5 text-green-500 shrink-0 mt-0.5\" />\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<XCircle className=\"size-3.5 text-destructive shrink-0 mt-0.5\" />\n\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t{/* Content */}\n\t\t\t\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t\t\t\t<p className=\"text-xs font-medium truncate\">{session.task}</p>\n\t\t\t\t\t\t\t<div className=\"flex items-center mt-0.5\">\n\t\t\t\t\t\t\t\t<p className=\"text-[10px] text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t{timeAgo(session.createdAt)} · {session.history.length} steps\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-0.5 ml-auto opacity-0 group-hover:opacity-100 transition-opacity\">\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => handleRerun(e, session.task)}\n\t\t\t\t\t\t\t\t\t\tclassName=\"p-0.5 text-muted-foreground hover:text-foreground transition-colors cursor-pointer\"\n\t\t\t\t\t\t\t\t\t\ttitle=\"Run task again\"\n\t\t\t\t\t\t\t\t\t\taria-label={`Run history task again: ${session.task}`}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<RotateCcw className=\"size-3\" />\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => handleExport(e, session)}\n\t\t\t\t\t\t\t\t\t\tclassName=\"p-1 text-muted-foreground hover:text-foreground transition-colors cursor-pointer\"\n\t\t\t\t\t\t\t\t\t\ttitle=\"Export history JSON\"\n\t\t\t\t\t\t\t\t\t\taria-label={`Export history for ${session.task}`}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<ArrowDownToLine className=\"size-3\" />\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tonClick={(e) => handleDelete(e, session.id)}\n\t\t\t\t\t\t\t\t\t\tclassName=\"p-0.5 text-muted-foreground hover:text-destructive transition-colors cursor-pointer\"\n\t\t\t\t\t\t\t\t\t\ttitle=\"Delete history\"\n\t\t\t\t\t\t\t\t\t\taria-label={`Delete history for ${session.task}`}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<Trash2 className=\"size-3\" />\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t))}\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/extension/src/components/cards.tsx",
    "content": "import type {\n\tAgentActivity,\n\tAgentErrorEvent,\n\tAgentStepEvent,\n\tHistoricalEvent,\n\tObservationEvent,\n\tRetryEvent,\n} from '@page-agent/core'\nimport {\n\tCheckCircle,\n\tEye,\n\tGlobe,\n\tKeyboard,\n\tMouse,\n\tMoveVertical,\n\tRefreshCw,\n\tSparkles,\n\tXCircle,\n\tZap,\n} from 'lucide-react'\nimport { Fragment, useState } from 'react'\n\nimport { cn } from '@/lib/utils'\n\n// Result card for done action\nfunction ResultCard({\n\tsuccess,\n\ttext,\n\tchildren,\n}: {\n\tsuccess: boolean\n\ttext: string\n\tchildren?: React.ReactNode\n}) {\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t'rounded-lg border p-3',\n\t\t\t\tsuccess ? 'border-green-500/30 bg-green-500/10' : 'border-destructive/30 bg-destructive/10'\n\t\t\t)}\n\t\t>\n\t\t\t<div className=\"flex items-center gap-2 mb-2\">\n\t\t\t\t{success ? (\n\t\t\t\t\t<CheckCircle className=\"size-3.5 text-green-500\" />\n\t\t\t\t) : (\n\t\t\t\t\t<XCircle className=\"size-3.5 text-destructive\" />\n\t\t\t\t)}\n\t\t\t\t<span\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t'text-xs font-medium',\n\t\t\t\t\t\tsuccess ? 'text-green-600 dark:text-green-400' : 'text-destructive'\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\tResult: {success ? 'Success' : 'Failed'}\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t\t<p className=\"text-xs text-[11px] text-muted-foreground pl-5 whitespace-pre-wrap\">{text}</p>\n\t\t\t{children}\n\t\t</div>\n\t)\n}\n\n// Single reflection item with truncation\nfunction ReflectionItem({ icon, value }: { icon: string; value: string }) {\n\tconst [expanded, setExpanded] = useState(false)\n\n\treturn (\n\t\t<Fragment>\n\t\t\t<span className=\"text-xs flex justify-center\">{icon}</span>\n\t\t\t<span\n\t\t\t\tclassName={cn(\n\t\t\t\t\t'text-[11px] text-muted-foreground cursor-pointer hover:text-muted-foreground/70',\n\t\t\t\t\t!expanded && 'line-clamp-1'\n\t\t\t\t)}\n\t\t\t\tonClick={() => setExpanded(!expanded)}\n\t\t\t>\n\t\t\t\t{value}\n\t\t\t</span>\n\t\t</Fragment>\n\t)\n}\n\n// Reflection section in step card\nfunction ReflectionSection({\n\treflection,\n}: {\n\treflection: {\n\t\tevaluation_previous_goal?: string\n\t\tmemory?: string\n\t\tnext_goal?: string\n\t}\n}) {\n\tconst items = [\n\t\t{ icon: '☑️', label: 'eval', value: reflection.evaluation_previous_goal },\n\t\t{ icon: '🧠', label: 'memory', value: reflection.memory },\n\t\t{ icon: '🎯', label: 'goal', value: reflection.next_goal },\n\t].filter((item) => item.value)\n\n\tif (items.length === 0) return null\n\n\treturn (\n\t\t<div className=\"mb-2\">\n\t\t\t{/* <div className=\"text-[11px] font-semibold text-foreground uppercase tracking-wide mb-2\">\n\t\t\t\tReflection\n\t\t\t</div> */}\n\t\t\t<div className=\"grid grid-cols-[14px_1fr] gap-x-2 gap-y-2\">\n\t\t\t\t{items.map((item) => (\n\t\t\t\t\t<ReflectionItem key={item.label} icon={item.icon} value={item.value!} />\n\t\t\t\t))}\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\n// Get icon for action type\nfunction ActionIcon({ name, className }: { name: string; className?: string }) {\n\tconst icons: Record<string, React.ReactNode> = {\n\t\tclick_element_by_index: <Mouse className={className} />,\n\t\tinput: <Keyboard className={className} />,\n\t\tscroll: <MoveVertical className={className} />,\n\t\tgo_to_url: <Globe className={className} />,\n\t}\n\treturn icons[name] || <Zap className={className} />\n}\n\n// Copy button with \"Copied!\" feedback\nfunction CopyButton({ text, label }: { text: string; label: string }) {\n\tconst [copied, setCopied] = useState(false)\n\n\treturn (\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\tonClick={() => {\n\t\t\t\tnavigator.clipboard.writeText(text)\n\t\t\t\tsetCopied(true)\n\t\t\t\tsetTimeout(() => setCopied(false), 1500)\n\t\t\t}}\n\t\t\tclassName=\"text-[9px] text-muted-foreground hover:text-foreground transition-colors border px-1 rounded shrink-0 cursor-pointer backdrop-blur-xs\"\n\t\t>\n\t\t\t{copied ? 'Copied!' : label}\n\t\t</button>\n\t)\n}\n\n// Extract message content by role from raw request\nfunction extractPrompt(rawRequest: unknown, role: 'system' | 'user'): string | null {\n\tconst messages = (rawRequest as { messages?: { role: string; content?: unknown }[] })?.messages\n\tif (!messages) return null\n\tconst msg =\n\t\trole === 'system'\n\t\t\t? messages.find((m) => m.role === role)\n\t\t\t: messages.findLast((m) => m.role === role)\n\tif (!msg?.content) return null\n\treturn typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content, null, 2)\n}\n\n// Raw request/response section (collapsible tabs, for debugging)\nfunction RawSection({ rawRequest, rawResponse }: { rawRequest?: unknown; rawResponse?: unknown }) {\n\tconst [activeTab, setActiveTab] = useState<'request' | 'response' | null>(null)\n\n\tif (!rawRequest && !rawResponse) return null\n\n\tconst handleTabClick = (tab: 'request' | 'response') => {\n\t\tsetActiveTab(activeTab === tab ? null : tab)\n\t}\n\n\tconst content =\n\t\tactiveTab === 'request' ? rawRequest : activeTab === 'response' ? rawResponse : null\n\n\tconst systemPrompt = activeTab === 'request' ? extractPrompt(rawRequest, 'system') : null\n\tconst userPrompt = activeTab === 'request' ? extractPrompt(rawRequest, 'user') : null\n\n\treturn (\n\t\t<div className=\"mt-2 border-t border-dashed pt-2\">\n\t\t\t<div className=\"flex items-center gap-3 -my-1\">\n\t\t\t\t{rawRequest != null && (\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tonClick={() => handleTabClick('request')}\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t'text-[10px] mt-0.5 transition-colors border-b cursor-pointer',\n\t\t\t\t\t\t\tactiveTab === 'request'\n\t\t\t\t\t\t\t\t? 'text-foreground border-foreground'\n\t\t\t\t\t\t\t\t: 'text-muted-foreground border-transparent hover:text-foreground'\n\t\t\t\t\t\t)}\n\t\t\t\t\t>\n\t\t\t\t\t\tRaw Request\n\t\t\t\t\t</button>\n\t\t\t\t)}\n\t\t\t\t{rawResponse != null && (\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tonClick={() => handleTabClick('response')}\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t'text-[10px] mt-0.5 transition-colors border-b cursor-pointer',\n\t\t\t\t\t\t\tactiveTab === 'response'\n\t\t\t\t\t\t\t\t? 'text-foreground border-foreground'\n\t\t\t\t\t\t\t\t: 'text-muted-foreground border-transparent hover:text-foreground'\n\t\t\t\t\t\t)}\n\t\t\t\t\t>\n\t\t\t\t\t\tRaw Response\n\t\t\t\t\t</button>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t\t{content != null && (\n\t\t\t\t<div className=\"relative mt-1.5\">\n\t\t\t\t\t<div className=\"absolute top-1 right-1 flex gap-1\">\n\t\t\t\t\t\t{systemPrompt && <CopyButton text={systemPrompt} label=\"Copy System\" />}\n\t\t\t\t\t\t{userPrompt && <CopyButton text={userPrompt} label=\"Copy User\" />}\n\t\t\t\t\t\t<CopyButton text={JSON.stringify(content, null, 4)} label=\"Copy\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<pre className=\"p-2 pt-5 text-[10px] text-foreground/70 bg-muted rounded overflow-x-auto max-h-60 overflow-y-auto\">\n\t\t\t\t\t\t{JSON.stringify(content, null, 4)}\n\t\t\t\t\t</pre>\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n\nfunction StepCard({ event }: { event: AgentStepEvent }) {\n\treturn (\n\t\t<div className=\"rounded-lg border-l-2 border-l-blue-500/50 border bg-muted/40 p-2.5\">\n\t\t\t<div className=\"text-[11px] font-semibold text-foreground tracking-wide mb-2\">\n\t\t\t\tStep #{event.stepIndex! + 1}\n\t\t\t</div>\n\n\t\t\t{/* Reflection */}\n\t\t\t{event.reflection && <ReflectionSection reflection={event.reflection} />}\n\n\t\t\t{/* Action */}\n\t\t\t{event.action && (\n\t\t\t\t<div>\n\t\t\t\t\t<div className=\"text-[11px] font-semibold text-foreground tracking-wide mb-1\">\n\t\t\t\t\t\tActions\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"flex items-start gap-2\">\n\t\t\t\t\t\t<ActionIcon\n\t\t\t\t\t\t\tname={event.action.name}\n\t\t\t\t\t\t\tclassName=\"size-3.5 text-blue-500 shrink-0 mt-0.5\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t\t\t\t<p className=\"text-xs text-foreground/80 mb-0.5 wrap-anywhere break-all line-clamp-1 hover:line-clamp-none\">\n\t\t\t\t\t\t\t\t<span className=\"font-medium text-foreground/70\">{event.action.name}</span>\n\t\t\t\t\t\t\t\t{event.action.name !== 'done' && (\n\t\t\t\t\t\t\t\t\t<span className=\"text-muted-foreground/70 ml-1.5\">\n\t\t\t\t\t\t\t\t\t\t{JSON.stringify(event.action.input)}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<p className=\"text-[11px] text-muted-foreground/70 grid grid-cols-[auto_1fr] gap-1.5\">\n\t\t\t\t\t\t\t\t<span className=\"\">└</span>\n\t\t\t\t\t\t\t\t<span className=\"wrap-anywhere break-all line-clamp-1 hover:line-clamp-3\">\n\t\t\t\t\t\t\t\t\t{event.action.output}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t{/* Raw Response */}\n\t\t\t<RawSection rawRequest={event.rawRequest} rawResponse={event.rawResponse} />\n\t\t</div>\n\t)\n}\n\nfunction ObservationCard({ event }: { event: ObservationEvent }) {\n\treturn (\n\t\t<div className=\"rounded-lg border-l-2 border-l-green-500/50 border bg-muted/40 p-2.5\">\n\t\t\t{/* <div className=\"text-[11px] font-semibold text-foreground uppercase tracking-wide mb-2\">\n\t\t\t\tObservation\n\t\t\t</div> */}\n\t\t\t<div className=\"flex items-start gap-2\">\n\t\t\t\t<Eye className=\"size-3.5 text-green-500 shrink-0 mt-0.5\" />\n\t\t\t\t<span className=\"text-[11px] text-muted-foreground\">{event.content}</span>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nfunction RetryCard({ event }: { event: RetryEvent }) {\n\treturn (\n\t\t<div className=\"rounded-lg border border-amber-500/30 bg-amber-500/10 p-2.5\">\n\t\t\t<div className=\"flex items-start gap-1.5\">\n\t\t\t\t<RefreshCw className=\"size-3 text-amber-500 shrink-0 mt-0.5\" />\n\t\t\t\t<span className=\"text-xs text-amber-600 dark:text-amber-400\">\n\t\t\t\t\t{event.message} ({event.attempt}/{event.maxAttempts})\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nfunction ErrorCard({ event }: { event: AgentErrorEvent }) {\n\treturn (\n\t\t<div className=\"rounded-lg border border-destructive/30 bg-destructive/10 p-2.5\">\n\t\t\t<div className=\"flex items-start gap-1.5\">\n\t\t\t\t<XCircle className=\"size-3 text-destructive shrink-0 mt-0.5\" />\n\t\t\t\t<span className=\"text-xs text-destructive\">{event.message}</span>\n\t\t\t</div>\n\t\t\t<RawSection rawResponse={event.rawResponse} />\n\t\t</div>\n\t)\n}\n\n// History event card component\nexport function EventCard({ event }: { event: HistoricalEvent }) {\n\t// Done action - show as result card\n\tif (event.type === 'step' && event.action?.name === 'done') {\n\t\tconst input = event.action.input as { text?: string; success?: boolean }\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<StepCard event={event as AgentStepEvent} />\n\t\t\t\t<ResultCard\n\t\t\t\t\tsuccess={input?.success ?? true}\n\t\t\t\t\ttext={input?.text || event.action.output || ''}\n\t\t\t\t/>\n\t\t\t</>\n\t\t)\n\t}\n\n\tif (event.type === 'step') {\n\t\treturn <StepCard event={event as AgentStepEvent} />\n\t}\n\n\tif (event.type === 'observation') {\n\t\treturn <ObservationCard event={event as ObservationEvent} />\n\t}\n\n\tif (event.type === 'retry') {\n\t\treturn <RetryCard event={event as RetryEvent} />\n\t}\n\n\tif (event.type === 'error') {\n\t\treturn <ErrorCard event={event as AgentErrorEvent} />\n\t}\n\n\treturn null\n}\n\n// Activity card with animation\nexport function ActivityCard({ activity }: { activity: AgentActivity }) {\n\tconst getActivityInfo = () => {\n\t\tswitch (activity.type) {\n\t\t\tcase 'thinking':\n\t\t\t\treturn { text: 'Thinking...', color: 'text-blue-500' }\n\t\t\tcase 'executing':\n\t\t\t\treturn { text: `Executing ${activity.tool}...`, color: 'text-amber-500' }\n\t\t\tcase 'executed':\n\t\t\t\treturn { text: `Done: ${activity.tool}`, color: 'text-green-500' }\n\t\t\tcase 'retrying':\n\t\t\t\treturn {\n\t\t\t\t\ttext: `Retrying (${activity.attempt}/${activity.maxAttempts})...`,\n\t\t\t\t\tcolor: 'text-amber-500',\n\t\t\t\t}\n\t\t\tcase 'error':\n\t\t\t\treturn { text: activity.message, color: 'text-destructive' }\n\t\t}\n\t}\n\n\tconst info = getActivityInfo()\n\n\treturn (\n\t\t<div className=\"flex items-center gap-2 rounded-lg border bg-muted/40 p-2.5 animate-pulse\">\n\t\t\t<div className=\"relative\">\n\t\t\t\t<Sparkles className={cn('size-3.5', info.color)} />\n\t\t\t\t<span\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t'absolute -top-0.5 -right-0.5 size-1.5 rounded-full animate-ping',\n\t\t\t\t\t\tactivity.type === 'thinking'\n\t\t\t\t\t\t\t? 'bg-blue-500'\n\t\t\t\t\t\t\t: activity.type === 'executing'\n\t\t\t\t\t\t\t\t? 'bg-amber-500'\n\t\t\t\t\t\t\t\t: activity.type === 'retrying'\n\t\t\t\t\t\t\t\t\t? 'bg-amber-500'\n\t\t\t\t\t\t\t\t\t: activity.type === 'error'\n\t\t\t\t\t\t\t\t\t\t? 'bg-destructive'\n\t\t\t\t\t\t\t\t\t\t: 'bg-green-500'\n\t\t\t\t\t)}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t<span className={cn('text-xs', info.color)}>{info.text}</span>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/extension/src/components/misc.tsx",
    "content": "import type { AgentStatus } from '@page-agent/core'\nimport { Motion } from 'ai-motion'\nimport { BookOpen, Globe } from 'lucide-react'\nimport { useEffect, useRef } from 'react'\nimport { siGithub } from 'simple-icons'\n\nimport { TypingAnimation } from '@/components/ui/typing-animation'\nimport { cn } from '@/lib/utils'\n\n// Status dot indicator\nexport function StatusDot({ status }: { status: AgentStatus }) {\n\tconst colorClass = {\n\t\tidle: 'bg-muted-foreground',\n\t\trunning: 'bg-blue-500',\n\t\tcompleted: 'bg-green-500',\n\t\terror: 'bg-destructive',\n\t}[status]\n\n\tconst label = {\n\t\tidle: 'Ready',\n\t\trunning: 'Running',\n\t\tcompleted: 'Done',\n\t\terror: 'Error',\n\t}[status]\n\n\treturn (\n\t\t<div className=\"flex items-center gap-1.5 mr-2\">\n\t\t\t<span\n\t\t\t\tclassName={cn('size-2 rounded-full', colorClass, status === 'running' && 'animate-pulse')}\n\t\t\t/>\n\t\t\t<span className=\"text-xs text-muted-foreground\">{label}</span>\n\t\t</div>\n\t)\n}\n\nexport function Logo({ className }: { className?: string }) {\n\treturn <img src=\"/assets/page-agent-256.webp\" alt=\"Page Agent\" className={cn('', className)} />\n}\n\n// Full-screen ai-motion glow overlay, shown only while running\nexport function MotionOverlay({ active }: { active: boolean }) {\n\tconst containerRef = useRef<HTMLDivElement>(null)\n\tconst motionRef = useRef<Motion | null>(null)\n\n\tuseEffect(() => {\n\t\ttry {\n\t\t\tconst mode = document.documentElement.classList.contains('dark') ? 'dark' : 'light'\n\t\t\tconst motion = new Motion({\n\t\t\t\tmode,\n\t\t\t\tborderWidth: 4,\n\t\t\t\tborderRadius: 14,\n\t\t\t\tglowWidth: mode === 'dark' ? 120 : 60,\n\t\t\t\tstyles: { position: 'absolute', inset: '0' },\n\t\t\t})\n\t\t\tmotionRef.current = motion\n\t\t\tcontainerRef.current!.appendChild(motion.element)\n\t\t\tmotion.autoResize(containerRef.current!)\n\t\t} catch (e) {\n\t\t\tconsole.warn('[MotionOverlay] Motion unavailable:', e)\n\t\t}\n\n\t\treturn () => {\n\t\t\tmotionRef.current?.dispose()\n\t\t\tmotionRef.current = null\n\t\t}\n\t}, [])\n\n\tuseEffect(() => {\n\t\tconst motion = motionRef.current\n\t\tif (!motion) return\n\n\t\tlet disposed = false\n\t\tif (active) {\n\t\t\tmotion.start()\n\t\t\tmotion.fadeIn()\n\t\t} else {\n\t\t\tmotion.fadeOut().then(() => !disposed && motion.pause())\n\t\t}\n\t\treturn () => {\n\t\t\tdisposed = true\n\t\t}\n\t}, [active])\n\n\treturn (\n\t\t<div\n\t\t\tref={containerRef}\n\t\t\tclassName=\"pointer-events-none absolute inset-0 z-10 opacity-60 overflow-hidden\"\n\t\t\tstyle={{ display: active ? undefined : 'none' }}\n\t\t/>\n\t)\n}\n\n// Empty state with logo and breathing glow\nexport function EmptyState() {\n\treturn (\n\t\t<div className=\"flex flex-col items-center justify-center h-full gap-4 text-center px-6\">\n\t\t\t<div className=\"relative select-none pointer-events-none\">\n\t\t\t\t<div className=\"absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_180deg,oklch(0.55_0.2_280),oklch(0.5_0.15_230),oklch(0.6_0.18_310),oklch(0.55_0.2_280))] blur-2xl animate-[glow-a_5s_ease-in-out_infinite]\" />\n\t\t\t\t<div className=\"absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_0deg,oklch(0.55_0.18_160),oklch(0.5_0.2_200),oklch(0.6_0.15_120),oklch(0.55_0.18_160))] blur-2xl animate-[glow-b_5s_ease-in-out_infinite]\" />\n\t\t\t\t<Logo className=\"relative size-20 opacity-80\" />\n\t\t\t</div>\n\t\t\t<div>\n\t\t\t\t<h2 className=\"text-base font-medium text-foreground mb-1\">Page Agent Ext</h2>\n\t\t\t\t<TypingAnimation\n\t\t\t\t\tclassName=\"text-sm text-muted-foreground\"\n\t\t\t\t\twords={[\n\t\t\t\t\t\t'Enter a task to automate this page',\n\t\t\t\t\t\t'Execute multi-page tasks',\n\t\t\t\t\t\t'Call this extension from your web page',\n\t\t\t\t\t\t'Use this extension in your own agents',\n\t\t\t\t\t]}\n\t\t\t\t\tcursorStyle=\"underscore\"\n\t\t\t\t\tloop\n\t\t\t\t\ttypeSpeed={20}\n\t\t\t\t\tdeleteSpeed={10}\n\t\t\t\t\tpauseDelay={3000}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t<div className=\"flex items-center gap-3 mt-1 text-muted-foreground\">\n\t\t\t\t<a\n\t\t\t\t\thref=\"https://github.com/alibaba/page-agent\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\tclassName=\"hover:text-foreground transition-colors\"\n\t\t\t\t\ttitle=\"GitHub\"\n\t\t\t\t>\n\t\t\t\t\t<svg role=\"img\" viewBox=\"0 0 24 24\" className=\"size-4 fill-current\">\n\t\t\t\t\t\t<path d={siGithub.path} />\n\t\t\t\t\t</svg>\n\t\t\t\t</a>\n\t\t\t\t<a\n\t\t\t\t\thref=\"https://alibaba.github.io/page-agent/docs/features/chrome-extension\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\tclassName=\"hover:text-foreground transition-colors\"\n\t\t\t\t\ttitle=\"Documentation\"\n\t\t\t\t>\n\t\t\t\t\t<BookOpen className=\"size-4\" />\n\t\t\t\t</a>\n\t\t\t\t<a\n\t\t\t\t\thref=\"https://alibaba.github.io/page-agent\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\tclassName=\"hover:text-foreground transition-colors\"\n\t\t\t\t\ttitle=\"Website\"\n\t\t\t\t>\n\t\t\t\t\t<Globe className=\"size-4\" />\n\t\t\t\t</a>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/extension/src/components/ui/button.tsx",
    "content": "import { Slot } from '@radix-ui/react-slot'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nconst buttonVariants = cva(\n\t\"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n\t{\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: 'bg-primary text-primary-foreground hover:bg-primary/90',\n\t\t\t\tdestructive:\n\t\t\t\t\t'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',\n\t\t\t\toutline:\n\t\t\t\t\t'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',\n\t\t\t\tsecondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',\n\t\t\t\tghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',\n\t\t\t\tlink: 'text-primary underline-offset-4 hover:underline',\n\t\t\t},\n\t\t\tsize: {\n\t\t\t\tdefault: 'h-9 px-4 py-2 has-[>svg]:px-3',\n\t\t\t\tsm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',\n\t\t\t\tlg: 'h-10 rounded-md px-6 has-[>svg]:px-4',\n\t\t\t\ticon: 'size-9',\n\t\t\t\t'icon-sm': 'size-8',\n\t\t\t\t'icon-lg': 'size-10',\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: 'default',\n\t\t\tsize: 'default',\n\t\t},\n\t}\n)\n\nfunction Button({\n\tclassName,\n\tvariant = 'default',\n\tsize = 'default',\n\tasChild = false,\n\t...props\n}: React.ComponentProps<'button'> &\n\tVariantProps<typeof buttonVariants> & {\n\t\tasChild?: boolean\n\t}) {\n\tconst Comp = asChild ? Slot : 'button'\n\n\treturn (\n\t\t<Comp\n\t\t\tdata-slot=\"button\"\n\t\t\tdata-variant={variant}\n\t\t\tdata-size={size}\n\t\t\tclassName={cn(buttonVariants({ variant, size, className }))}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "packages/extension/src/components/ui/card.tsx",
    "content": "import * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Card({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"card\"\n\t\t\tclassName={cn(\n\t\t\t\t'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"card-header\"\n\t\t\tclassName={cn(\n\t\t\t\t'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"card-title\"\n\t\t\tclassName={cn('leading-none font-semibold', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"card-description\"\n\t\t\tclassName={cn('text-muted-foreground text-sm', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"card-action\"\n\t\t\tclassName={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn <div data-slot=\"card-content\" className={cn('px-6', className)} {...props} />\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"card-footer\"\n\t\t\tclassName={cn('flex items-center px-6 [.border-t]:pt-6', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }\n"
  },
  {
    "path": "packages/extension/src/components/ui/field.tsx",
    "content": "import { type VariantProps, cva } from 'class-variance-authority'\nimport { useMemo } from 'react'\n\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\nimport { cn } from '@/lib/utils'\n\nfunction FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {\n\treturn (\n\t\t<fieldset\n\t\t\tdata-slot=\"field-set\"\n\t\t\tclassName={cn(\n\t\t\t\t'flex flex-col gap-6',\n\t\t\t\t'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction FieldLegend({\n\tclassName,\n\tvariant = 'legend',\n\t...props\n}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {\n\treturn (\n\t\t<legend\n\t\t\tdata-slot=\"field-legend\"\n\t\t\tdata-variant={variant}\n\t\t\tclassName={cn(\n\t\t\t\t'mb-3 font-medium',\n\t\t\t\t'data-[variant=legend]:text-base',\n\t\t\t\t'data-[variant=label]:text-sm',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"field-group\"\n\t\t\tclassName={cn(\n\t\t\t\t'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nconst fieldVariants = cva('group/field flex w-full gap-3 data-[invalid=true]:text-destructive', {\n\tvariants: {\n\t\torientation: {\n\t\t\tvertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],\n\t\t\thorizontal: [\n\t\t\t\t'flex-row items-center',\n\t\t\t\t'[&>[data-slot=field-label]]:flex-auto',\n\t\t\t\t'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',\n\t\t\t],\n\t\t\tresponsive: [\n\t\t\t\t'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',\n\t\t\t\t'@md/field-group:[&>[data-slot=field-label]]:flex-auto',\n\t\t\t\t'@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',\n\t\t\t],\n\t\t},\n\t},\n\tdefaultVariants: {\n\t\torientation: 'vertical',\n\t},\n})\n\nfunction Field({\n\tclassName,\n\torientation = 'vertical',\n\t...props\n}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {\n\treturn (\n\t\t<div\n\t\t\trole=\"group\"\n\t\t\tdata-slot=\"field\"\n\t\t\tdata-orientation={orientation}\n\t\t\tclassName={cn(fieldVariants({ orientation }), className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction FieldContent({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"field-content\"\n\t\t\tclassName={cn('group/field-content flex flex-1 flex-col gap-1.5 leading-snug', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) {\n\treturn (\n\t\t<Label\n\t\t\tdata-slot=\"field-label\"\n\t\t\tclassName={cn(\n\t\t\t\t'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',\n\t\t\t\t'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',\n\t\t\t\t'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"field-label\"\n\t\t\tclassName={cn(\n\t\t\t\t'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {\n\treturn (\n\t\t<p\n\t\t\tdata-slot=\"field-description\"\n\t\t\tclassName={cn(\n\t\t\t\t'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',\n\t\t\t\t'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',\n\t\t\t\t'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction FieldSeparator({\n\tchildren,\n\tclassName,\n\t...props\n}: React.ComponentProps<'div'> & {\n\tchildren?: React.ReactNode\n}) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"field-separator\"\n\t\t\tdata-content={!!children}\n\t\t\tclassName={cn(\n\t\t\t\t'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t>\n\t\t\t<Separator className=\"absolute inset-0 top-1/2\" />\n\t\t\t{children && (\n\t\t\t\t<span\n\t\t\t\t\tclassName=\"bg-background text-muted-foreground relative mx-auto block w-fit px-2\"\n\t\t\t\t\tdata-slot=\"field-separator-content\"\n\t\t\t\t>\n\t\t\t\t\t{children}\n\t\t\t\t</span>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n\nfunction FieldError({\n\tclassName,\n\tchildren,\n\terrors,\n\t...props\n}: React.ComponentProps<'div'> & {\n\terrors?: Array<{ message?: string } | undefined>\n}) {\n\tconst content = useMemo(() => {\n\t\tif (children) {\n\t\t\treturn children\n\t\t}\n\n\t\tif (!errors?.length) {\n\t\t\treturn null\n\t\t}\n\n\t\tconst uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()]\n\n\t\tif (uniqueErrors?.length == 1) {\n\t\t\treturn uniqueErrors[0]?.message\n\t\t}\n\n\t\treturn (\n\t\t\t<ul className=\"ml-4 flex list-disc flex-col gap-1\">\n\t\t\t\t{uniqueErrors.map((error, index) => error?.message && <li key={index}>{error.message}</li>)}\n\t\t\t</ul>\n\t\t)\n\t}, [children, errors])\n\n\tif (!content) {\n\t\treturn null\n\t}\n\n\treturn (\n\t\t<div\n\t\t\trole=\"alert\"\n\t\t\tdata-slot=\"field-error\"\n\t\t\tclassName={cn('text-destructive text-sm font-normal', className)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{content}\n\t\t</div>\n\t)\n}\n\nexport {\n\tField,\n\tFieldLabel,\n\tFieldDescription,\n\tFieldError,\n\tFieldGroup,\n\tFieldLegend,\n\tFieldSeparator,\n\tFieldSet,\n\tFieldContent,\n\tFieldTitle,\n}\n"
  },
  {
    "path": "packages/extension/src/components/ui/hover-card.tsx",
    "content": "import * as HoverCardPrimitive from '@radix-ui/react-hover-card'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {\n\treturn <HoverCardPrimitive.Root data-slot=\"hover-card\" {...props} />\n}\n\nfunction HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {\n\treturn <HoverCardPrimitive.Trigger data-slot=\"hover-card-trigger\" {...props} />\n}\n\nfunction HoverCardContent({\n\tclassName,\n\talign = 'center',\n\tsideOffset = 4,\n\t...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {\n\treturn (\n\t\t<HoverCardPrimitive.Portal data-slot=\"hover-card-portal\">\n\t\t\t<HoverCardPrimitive.Content\n\t\t\t\tdata-slot=\"hover-card-content\"\n\t\t\t\talign={align}\n\t\t\t\tsideOffset={sideOffset}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t\t{...props}\n\t\t\t/>\n\t\t</HoverCardPrimitive.Portal>\n\t)\n}\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent }\n"
  },
  {
    "path": "packages/extension/src/components/ui/input-group.tsx",
    "content": "import { type VariantProps, cva } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Textarea } from '@/components/ui/textarea'\nimport { cn } from '@/lib/utils'\n\nfunction InputGroup({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"input-group\"\n\t\t\trole=\"group\"\n\t\t\tclassName={cn(\n\t\t\t\t'group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none',\n\t\t\t\t'h-9 min-w-0 has-[>textarea]:h-auto',\n\n\t\t\t\t// Variants based on alignment.\n\t\t\t\t'has-[>[data-align=inline-start]]:[&>input]:pl-2',\n\t\t\t\t'has-[>[data-align=inline-end]]:[&>input]:pr-2',\n\t\t\t\t'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',\n\t\t\t\t'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',\n\n\t\t\t\t// Focus state — soft multi-color glow matching ai-motion palette\n\t\t\t\t'has-[[data-slot=input-group-control]:focus-visible]:border-blue-400/60',\n\t\t\t\t'has-[[data-slot=input-group-control]:focus-visible]:shadow-[0_0_0_1px_rgba(57,182,255,0.2),0_0_8px_rgba(57,182,255,0.15),0_0_16px_rgba(189,69,251,0.1)]',\n\n\t\t\t\t// Error state.\n\t\t\t\t'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',\n\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nconst inputGroupAddonVariants = cva(\n\t\"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50\",\n\t{\n\t\tvariants: {\n\t\t\talign: {\n\t\t\t\t'inline-start': 'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',\n\t\t\t\t'inline-end': 'order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]',\n\t\t\t\t'block-start':\n\t\t\t\t\t'order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5',\n\t\t\t\t'block-end':\n\t\t\t\t\t'order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5',\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\talign: 'inline-start',\n\t\t},\n\t}\n)\n\nfunction InputGroupAddon({\n\tclassName,\n\talign = 'inline-start',\n\t...props\n}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {\n\treturn (\n\t\t<div\n\t\t\trole=\"group\"\n\t\t\tdata-slot=\"input-group-addon\"\n\t\t\tdata-align={align}\n\t\t\tclassName={cn(inputGroupAddonVariants({ align }), className)}\n\t\t\tonClick={(e) => {\n\t\t\t\tif ((e.target as HTMLElement).closest('button')) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\te.currentTarget.parentElement?.querySelector('input')?.focus()\n\t\t\t}}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nconst inputGroupButtonVariants = cva('text-sm shadow-none flex gap-2 items-center', {\n\tvariants: {\n\t\tsize: {\n\t\t\txs: \"h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2\",\n\t\t\tsm: 'h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5',\n\t\t\t'icon-xs': 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',\n\t\t\t'icon-sm': 'size-8 p-0 has-[>svg]:p-0',\n\t\t},\n\t},\n\tdefaultVariants: {\n\t\tsize: 'xs',\n\t},\n})\n\nfunction InputGroupButton({\n\tclassName,\n\ttype = 'button',\n\tvariant = 'ghost',\n\tsize = 'xs',\n\t...props\n}: Omit<React.ComponentProps<typeof Button>, 'size'> &\n\tVariantProps<typeof inputGroupButtonVariants>) {\n\treturn (\n\t\t<Button\n\t\t\ttype={type}\n\t\t\tdata-size={size}\n\t\t\tvariant={variant}\n\t\t\tclassName={cn(inputGroupButtonVariants({ size }), className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {\n\treturn (\n\t\t<span\n\t\t\tclassName={cn(\n\t\t\t\t\"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction InputGroupInput({ className, ...props }: React.ComponentProps<'input'>) {\n\treturn (\n\t\t<Input\n\t\t\tdata-slot=\"input-group-control\"\n\t\t\tclassName={cn(\n\t\t\t\t'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) {\n\treturn (\n\t\t<Textarea\n\t\t\tdata-slot=\"input-group-control\"\n\t\t\tclassName={cn(\n\t\t\t\t'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport {\n\tInputGroup,\n\tInputGroupAddon,\n\tInputGroupButton,\n\tInputGroupText,\n\tInputGroupInput,\n\tInputGroupTextarea,\n}\n"
  },
  {
    "path": "packages/extension/src/components/ui/input.tsx",
    "content": "import * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Input({ className, type, ...props }: React.ComponentProps<'input'>) {\n\treturn (\n\t\t<input\n\t\t\ttype={type}\n\t\t\tdata-slot=\"input\"\n\t\t\tclassName={cn(\n\t\t\t\t'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',\n\t\t\t\t'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',\n\t\t\t\t'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Input }\n"
  },
  {
    "path": "packages/extension/src/components/ui/item.tsx",
    "content": "import { Slot } from '@radix-ui/react-slot'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport { Separator } from '@/components/ui/separator'\nimport { cn } from '@/lib/utils'\n\nfunction ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\trole=\"list\"\n\t\t\tdata-slot=\"item-group\"\n\t\t\tclassName={cn('group/item-group flex flex-col', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {\n\treturn (\n\t\t<Separator\n\t\t\tdata-slot=\"item-separator\"\n\t\t\torientation=\"horizontal\"\n\t\t\tclassName={cn('my-0', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nconst itemVariants = cva(\n\t'group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',\n\t{\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: 'bg-transparent',\n\t\t\t\toutline: 'border-border',\n\t\t\t\tmuted: 'bg-muted/50',\n\t\t\t},\n\t\t\tsize: {\n\t\t\t\tdefault: 'p-4 gap-4 ',\n\t\t\t\tsm: 'py-3 px-4 gap-2.5',\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: 'default',\n\t\t\tsize: 'default',\n\t\t},\n\t}\n)\n\nfunction Item({\n\tclassName,\n\tvariant = 'default',\n\tsize = 'default',\n\tasChild = false,\n\t...props\n}: React.ComponentProps<'div'> & VariantProps<typeof itemVariants> & { asChild?: boolean }) {\n\tconst Comp = asChild ? Slot : 'div'\n\treturn (\n\t\t<Comp\n\t\t\tdata-slot=\"item\"\n\t\t\tdata-variant={variant}\n\t\t\tdata-size={size}\n\t\t\tclassName={cn(itemVariants({ variant, size, className }))}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nconst itemMediaVariants = cva(\n\t'flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5',\n\t{\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: 'bg-transparent',\n\t\t\t\ticon: \"size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4\",\n\t\t\t\timage: 'size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover',\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: 'default',\n\t\t},\n\t}\n)\n\nfunction ItemMedia({\n\tclassName,\n\tvariant = 'default',\n\t...props\n}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"item-media\"\n\t\t\tdata-variant={variant}\n\t\t\tclassName={cn(itemMediaVariants({ variant, className }))}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction ItemContent({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"item-content\"\n\t\t\tclassName={cn('flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"item-title\"\n\t\t\tclassName={cn('flex w-fit items-center gap-2 text-sm leading-snug font-medium', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {\n\treturn (\n\t\t<p\n\t\t\tdata-slot=\"item-description\"\n\t\t\tclassName={cn(\n\t\t\t\t'text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance',\n\t\t\t\t'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction ItemActions({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div data-slot=\"item-actions\" className={cn('flex items-center gap-2', className)} {...props} />\n\t)\n}\n\nfunction ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"item-header\"\n\t\t\tclassName={cn('flex basis-full items-center justify-between gap-2', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"item-footer\"\n\t\t\tclassName={cn('flex basis-full items-center justify-between gap-2', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport {\n\tItem,\n\tItemMedia,\n\tItemContent,\n\tItemActions,\n\tItemGroup,\n\tItemSeparator,\n\tItemTitle,\n\tItemDescription,\n\tItemHeader,\n\tItemFooter,\n}\n"
  },
  {
    "path": "packages/extension/src/components/ui/label.tsx",
    "content": "import * as LabelPrimitive from '@radix-ui/react-label'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {\n\treturn (\n\t\t<LabelPrimitive.Root\n\t\t\tdata-slot=\"label\"\n\t\t\tclassName={cn(\n\t\t\t\t'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Label }\n"
  },
  {
    "path": "packages/extension/src/components/ui/separator.tsx",
    "content": "import * as SeparatorPrimitive from '@radix-ui/react-separator'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Separator({\n\tclassName,\n\torientation = 'horizontal',\n\tdecorative = true,\n\t...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n\treturn (\n\t\t<SeparatorPrimitive.Root\n\t\t\tdata-slot=\"separator\"\n\t\t\tdecorative={decorative}\n\t\t\torientation={orientation}\n\t\t\tclassName={cn(\n\t\t\t\t'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Separator }\n"
  },
  {
    "path": "packages/extension/src/components/ui/sonner.tsx",
    "content": "import {\n\tCircleCheckIcon,\n\tInfoIcon,\n\tLoader2Icon,\n\tOctagonXIcon,\n\tTriangleAlertIcon,\n} from 'lucide-react'\nimport { useTheme } from 'next-themes'\nimport { Toaster as Sonner, type ToasterProps } from 'sonner'\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n\tconst { theme = 'system' } = useTheme()\n\n\treturn (\n\t\t<Sonner\n\t\t\ttheme={theme as ToasterProps['theme']}\n\t\t\tclassName=\"toaster group\"\n\t\t\ticons={{\n\t\t\t\tsuccess: <CircleCheckIcon className=\"size-4\" />,\n\t\t\t\tinfo: <InfoIcon className=\"size-4\" />,\n\t\t\t\twarning: <TriangleAlertIcon className=\"size-4\" />,\n\t\t\t\terror: <OctagonXIcon className=\"size-4\" />,\n\t\t\t\tloading: <Loader2Icon className=\"size-4 animate-spin\" />,\n\t\t\t}}\n\t\t\tstyle={\n\t\t\t\t{\n\t\t\t\t\t'--normal-bg': 'var(--popover)',\n\t\t\t\t\t'--normal-text': 'var(--popover-foreground)',\n\t\t\t\t\t'--normal-border': 'var(--border)',\n\t\t\t\t\t'--border-radius': 'var(--radius)',\n\t\t\t\t} as React.CSSProperties\n\t\t\t}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Toaster }\n"
  },
  {
    "path": "packages/extension/src/components/ui/spinner.tsx",
    "content": "import { Loader2Icon } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Spinner({ className, ...props }: React.ComponentProps<'svg'>) {\n\treturn (\n\t\t<Loader2Icon\n\t\t\trole=\"status\"\n\t\t\taria-label=\"Loading\"\n\t\t\tclassName={cn('size-4 animate-spin', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Spinner }\n"
  },
  {
    "path": "packages/extension/src/components/ui/switch.tsx",
    "content": "import * as SwitchPrimitive from '@radix-ui/react-switch'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {\n\treturn (\n\t\t<SwitchPrimitive.Root\n\t\t\tdata-slot=\"switch\"\n\t\t\tclassName={cn(\n\t\t\t\t'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 cursor-pointer items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t>\n\t\t\t<SwitchPrimitive.Thumb\n\t\t\t\tdata-slot=\"switch-thumb\"\n\t\t\t\tclassName={cn(\n\t\t\t\t\t'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0'\n\t\t\t\t)}\n\t\t\t/>\n\t\t</SwitchPrimitive.Root>\n\t)\n}\n\nexport { Switch }\n"
  },
  {
    "path": "packages/extension/src/components/ui/textarea.tsx",
    "content": "import * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {\n\treturn (\n\t\t<textarea\n\t\t\tdata-slot=\"textarea\"\n\t\t\tclassName={cn(\n\t\t\t\t'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Textarea }\n"
  },
  {
    "path": "packages/extension/src/components/ui/typing-animation.tsx",
    "content": "import { MotionProps, motion, useInView } from 'motion/react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\n\nimport { cn } from '@/lib/utils'\n\ninterface TypingAnimationProps extends MotionProps {\n\tchildren?: string\n\twords?: string[]\n\tclassName?: string\n\tduration?: number\n\ttypeSpeed?: number\n\tdeleteSpeed?: number\n\tdelay?: number\n\tpauseDelay?: number\n\tloop?: boolean\n\tas?: React.ElementType\n\tstartOnView?: boolean\n\tshowCursor?: boolean\n\tblinkCursor?: boolean\n\tcursorStyle?: 'line' | 'block' | 'underscore'\n}\n\nexport function TypingAnimation({\n\tchildren,\n\twords,\n\tclassName,\n\tduration = 100,\n\ttypeSpeed,\n\tdeleteSpeed,\n\tdelay = 0,\n\tpauseDelay = 1000,\n\tloop = false,\n\tas: Component = 'span',\n\tstartOnView = true,\n\tshowCursor = true,\n\tblinkCursor = true,\n\tcursorStyle = 'line',\n\t...props\n}: TypingAnimationProps) {\n\tconst MotionComponent = motion.create(Component, {\n\t\tforwardMotionProps: true,\n\t})\n\n\tconst [displayedText, setDisplayedText] = useState<string>('')\n\tconst [currentWordIndex, setCurrentWordIndex] = useState(0)\n\tconst [currentCharIndex, setCurrentCharIndex] = useState(0)\n\tconst [phase, setPhase] = useState<'typing' | 'pause' | 'deleting'>('typing')\n\tconst elementRef = useRef<HTMLElement | null>(null)\n\tconst isInView = useInView(elementRef as React.RefObject<Element>, {\n\t\tamount: 0.3,\n\t\tonce: true,\n\t})\n\n\tconst wordsToAnimate = useMemo(() => words || (children ? [children] : []), [words, children])\n\tconst hasMultipleWords = wordsToAnimate.length > 1\n\n\tconst typingSpeed = typeSpeed || duration\n\tconst deletingSpeed = deleteSpeed || typingSpeed / 2\n\n\tconst shouldStart = startOnView ? isInView : true\n\n\tuseEffect(() => {\n\t\tif (!shouldStart || wordsToAnimate.length === 0) return\n\n\t\tconst timeoutDelay =\n\t\t\tdelay > 0 && displayedText === ''\n\t\t\t\t? delay\n\t\t\t\t: phase === 'typing'\n\t\t\t\t\t? typingSpeed\n\t\t\t\t\t: phase === 'deleting'\n\t\t\t\t\t\t? deletingSpeed\n\t\t\t\t\t\t: pauseDelay\n\n\t\tconst timeout = setTimeout(() => {\n\t\t\tconst currentWord = wordsToAnimate[currentWordIndex] || ''\n\t\t\tconst graphemes = Array.from(currentWord)\n\n\t\t\tswitch (phase) {\n\t\t\t\tcase 'typing':\n\t\t\t\t\tif (currentCharIndex < graphemes.length) {\n\t\t\t\t\t\tsetDisplayedText(graphemes.slice(0, currentCharIndex + 1).join(''))\n\t\t\t\t\t\tsetCurrentCharIndex(currentCharIndex + 1)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (hasMultipleWords || loop) {\n\t\t\t\t\t\t\tconst isLastWord = currentWordIndex === wordsToAnimate.length - 1\n\t\t\t\t\t\t\tif (!isLastWord || loop) {\n\t\t\t\t\t\t\t\tsetPhase('pause')\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase 'pause':\n\t\t\t\t\tsetPhase('deleting')\n\t\t\t\t\tbreak\n\n\t\t\t\tcase 'deleting':\n\t\t\t\t\tif (currentCharIndex > 0) {\n\t\t\t\t\t\tsetDisplayedText(graphemes.slice(0, currentCharIndex - 1).join(''))\n\t\t\t\t\t\tsetCurrentCharIndex(currentCharIndex - 1)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst nextIndex = (currentWordIndex + 1) % wordsToAnimate.length\n\t\t\t\t\t\tsetCurrentWordIndex(nextIndex)\n\t\t\t\t\t\tsetPhase('typing')\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t}\n\t\t}, timeoutDelay)\n\n\t\treturn () => clearTimeout(timeout)\n\t}, [\n\t\tshouldStart,\n\t\tphase,\n\t\tcurrentCharIndex,\n\t\tcurrentWordIndex,\n\t\tdisplayedText,\n\t\twordsToAnimate,\n\t\thasMultipleWords,\n\t\tloop,\n\t\ttypingSpeed,\n\t\tdeletingSpeed,\n\t\tpauseDelay,\n\t\tdelay,\n\t])\n\n\tconst currentWordGraphemes = Array.from(wordsToAnimate[currentWordIndex] || '')\n\tconst isComplete =\n\t\t!loop &&\n\t\tcurrentWordIndex === wordsToAnimate.length - 1 &&\n\t\tcurrentCharIndex >= currentWordGraphemes.length &&\n\t\tphase !== 'deleting'\n\n\tconst shouldShowCursor =\n\t\tshowCursor &&\n\t\t!isComplete &&\n\t\t(hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)\n\n\tconst getCursorChar = () => {\n\t\tswitch (cursorStyle) {\n\t\t\tcase 'block':\n\t\t\t\treturn '▌'\n\t\t\tcase 'underscore':\n\t\t\t\treturn '_'\n\t\t\tcase 'line':\n\t\t\tdefault:\n\t\t\t\treturn '|'\n\t\t}\n\t}\n\n\treturn (\n\t\t<MotionComponent\n\t\t\tref={elementRef}\n\t\t\tclassName={cn('leading-[5rem] tracking-[-0.02em]', className)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{displayedText}\n\t\t\t{shouldShowCursor && (\n\t\t\t\t<span className={cn('inline-block', blinkCursor && 'animate-blink-cursor')}>\n\t\t\t\t\t{getCursorChar()}\n\t\t\t\t</span>\n\t\t\t)}\n\t\t</MotionComponent>\n\t)\n}\n"
  },
  {
    "path": "packages/extension/src/entrypoints/background.ts",
    "content": "import { handlePageControlMessage } from '@/agent/RemotePageController.background'\nimport { handleTabControlMessage, setupTabChangeEvents } from '@/agent/TabsController.background'\n\nexport default defineBackground(() => {\n\tconsole.log('[Background] Service Worker started')\n\n\t// tab change events\n\n\tsetupTabChangeEvents()\n\n\t// generate user auth token\n\n\tchrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => {\n\t\tif (result.PageAgentExtUserAuthToken) return\n\n\t\tconst userAuthToken = crypto.randomUUID()\n\t\tchrome.storage.local.set({ PageAgentExtUserAuthToken: userAuthToken })\n\t})\n\n\t// message proxy\n\n\tchrome.runtime.onMessage.addListener((message, sender, sendResponse): true | undefined => {\n\t\tif (message.type === 'TAB_CONTROL') {\n\t\t\treturn handleTabControlMessage(message, sender, sendResponse)\n\t\t} else if (message.type === 'PAGE_CONTROL') {\n\t\t\treturn handlePageControlMessage(message, sender, sendResponse)\n\t\t} else {\n\t\t\tsendResponse({ error: 'Unknown message type' })\n\t\t\treturn\n\t\t}\n\t})\n\n\t// external messages (from localhost launcher page via externally_connectable)\n\n\tchrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {\n\t\tif (message.type === 'OPEN_HUB') {\n\t\t\topenOrFocusHubTab(message.wsPort).then(() => {\n\t\t\t\tif (sender.tab?.id) chrome.tabs.remove(sender.tab.id)\n\t\t\t\tsendResponse({ ok: true })\n\t\t\t})\n\t\t\treturn true\n\t\t}\n\t})\n\n\t// setup\n\n\tchrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})\n})\n\nasync function openOrFocusHubTab(wsPort: number) {\n\tconst hubUrl = chrome.runtime.getURL('hub.html')\n\tconst existing = await chrome.tabs.query({ url: `${hubUrl}*` })\n\n\tif (existing.length > 0 && existing[0].id) {\n\t\tawait chrome.tabs.update(existing[0].id, {\n\t\t\tactive: true,\n\t\t\turl: `${hubUrl}?ws=${wsPort}`,\n\t\t})\n\t\treturn\n\t}\n\n\tawait chrome.tabs.create({ url: `${hubUrl}?ws=${wsPort}`, pinned: true })\n}\n"
  },
  {
    "path": "packages/extension/src/entrypoints/content.ts",
    "content": "import { initPageController } from '@/agent/RemotePageController.content'\n\n// import { DEMO_CONFIG } from '@/agent/constants'\n\nconst DEBUG_PREFIX = '[Content]'\n\nexport default defineContentScript({\n\tmatches: ['<all_urls>'],\n\trunAt: 'document_end',\n\n\tmain() {\n\t\tconsole.debug(`${DEBUG_PREFIX} Loaded on ${window.location.href}`)\n\t\tinitPageController()\n\n\t\t// if auth token matches, expose agent to page\n\t\tchrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => {\n\t\t\t// extension side token.\n\t\t\t// @note this is isolated world. it is safe to assume user script cannot access it\n\t\t\tconst extToken = result.PageAgentExtUserAuthToken\n\t\t\tif (!extToken) return\n\n\t\t\t// page side token\n\t\t\tconst pageToken = localStorage.getItem('PageAgentExtUserAuthToken')\n\t\t\tif (!pageToken) return\n\n\t\t\tif (pageToken !== extToken) return\n\n\t\t\tconsole.log('[PageAgentExt]: Auth tokens match. Exposing agent to page.')\n\n\t\t\t// add isolated world script\n\t\t\texposeAgentToPage().then(\n\t\t\t\t// add main-world script\n\t\t\t\t() => injectScript('/main-world.js')\n\t\t\t)\n\t\t})\n\t},\n})\n\nasync function exposeAgentToPage() {\n\tconst { MultiPageAgent } = await import('@/agent/MultiPageAgent')\n\tconsole.log('[PageAgentExt]: MultiPageAgent loaded')\n\n\t/**\n\t * singleton MultiPageAgent to handle requests from the page\n\t */\n\tlet multiPageAgent: InstanceType<typeof MultiPageAgent> | null = null\n\n\twindow.addEventListener('message', async (e) => {\n\t\tconst data = e.data\n\t\tif (typeof data !== 'object' || data === null) return\n\t\tif (data.channel !== 'PAGE_AGENT_EXT_REQUEST') return\n\n\t\tconst { action, payload, id } = data\n\n\t\tswitch (action) {\n\t\t\tcase 'execute': {\n\t\t\t\t// singleton check\n\t\t\t\tif (multiPageAgent && multiPageAgent.status === 'running') {\n\t\t\t\t\twindow.postMessage(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tchannel: 'PAGE_AGENT_EXT_RESPONSE',\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\taction: 'execute_result',\n\t\t\t\t\t\t\terror: 'Agent is already running a task. Please wait until it finishes.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'*'\n\t\t\t\t\t)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst { task, config } = payload\n\n\t\t\t\t\t// Dispose old instance before creating new one\n\t\t\t\t\tmultiPageAgent?.dispose()\n\n\t\t\t\t\tmultiPageAgent = new MultiPageAgent(config)\n\n\t\t\t\t\t// events\n\n\t\t\t\t\tmultiPageAgent.addEventListener('statuschange', (event) => {\n\t\t\t\t\t\tif (!multiPageAgent) return\n\t\t\t\t\t\twindow.postMessage(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tchannel: 'PAGE_AGENT_EXT_RESPONSE',\n\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\taction: 'status_change_event',\n\t\t\t\t\t\t\t\tpayload: multiPageAgent.status,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t'*'\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\n\t\t\t\t\tmultiPageAgent.addEventListener('activity', (event) => {\n\t\t\t\t\t\tif (!multiPageAgent) return\n\t\t\t\t\t\twindow.postMessage(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tchannel: 'PAGE_AGENT_EXT_RESPONSE',\n\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\taction: 'activity_event',\n\t\t\t\t\t\t\t\tpayload: (event as CustomEvent).detail,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t'*'\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\n\t\t\t\t\tmultiPageAgent.addEventListener('historychange', (event) => {\n\t\t\t\t\t\tif (!multiPageAgent) return\n\t\t\t\t\t\twindow.postMessage(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tchannel: 'PAGE_AGENT_EXT_RESPONSE',\n\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\taction: 'history_change_event',\n\t\t\t\t\t\t\t\tpayload: multiPageAgent.history,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t'*'\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\n\t\t\t\t\t// result\n\n\t\t\t\t\tconst result = await multiPageAgent.execute(task)\n\n\t\t\t\t\twindow.postMessage(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tchannel: 'PAGE_AGENT_EXT_RESPONSE',\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\taction: 'execute_result',\n\t\t\t\t\t\t\tpayload: result,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'*'\n\t\t\t\t\t)\n\t\t\t\t} catch (error) {\n\t\t\t\t\twindow.postMessage(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tchannel: 'PAGE_AGENT_EXT_RESPONSE',\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\taction: 'execute_result',\n\t\t\t\t\t\t\terror: (error as Error).message,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'*'\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tcase 'stop': {\n\t\t\t\tmultiPageAgent?.stop()\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tdefault:\n\t\t\t\tconsole.warn(`${DEBUG_PREFIX} Unknown action from page:`, action)\n\t\t\t\tbreak\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "packages/extension/src/entrypoints/hub/App.tsx",
    "content": "import { FoldVertical, Plug, PlugZap, Square, UnfoldVertical, Unplug } from 'lucide-react'\nimport { useEffect, useRef, useState } from 'react'\n\nimport { useAgent } from '@/agent/useAgent'\nimport { ActivityCard, EventCard } from '@/components/cards'\nimport { Logo, MotionOverlay, StatusDot } from '@/components/misc'\nimport { Button } from '@/components/ui/button'\nimport { Switch } from '@/components/ui/switch'\n\nimport { useHubWs } from './hub-ws'\n\nexport default function App() {\n\tconst { status, history, activity, currentTask, config, execute, stop, configure } = useAgent()\n\tconst { wsState } = useHubWs(execute, stop, configure, config)\n\n\tconst historyRef = useRef<HTMLDivElement>(null)\n\n\tuseEffect(() => {\n\t\tif (historyRef.current) {\n\t\t\thistoryRef.current.scrollTop = historyRef.current.scrollHeight\n\t\t}\n\t}, [history, activity])\n\n\tconst isRunning = status === 'running'\n\tconst WsIcon = wsState === 'connected' ? PlugZap : wsState === 'connecting' ? Plug : Unplug\n\tconst wsLabel = {\n\t\tconnected: 'Connected',\n\t\tconnecting: 'Connecting…',\n\t\tdisconnected: new URLSearchParams(location.search).get('ws') ? 'Disconnected' : 'No connection',\n\t}[wsState]\n\n\treturn (\n\t\t<div className=\"flex h-screen bg-background\">\n\t\t\t{/* Left — Protocol docs */}\n\t\t\t<aside className=\"w-80 shrink-0 border-r flex flex-col bg-muted/20\">\n\t\t\t\t<a\n\t\t\t\t\thref=\"https://alibaba.github.io/page-agent/\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\tclassName=\"flex items-center gap-2 px-5 h-12 border-b hover:bg-muted/30 transition-colors\"\n\t\t\t\t>\n\t\t\t\t\t<Logo className=\"size-5\" />\n\t\t\t\t\t<span className=\"text-sm font-semibold tracking-tight\">Page Agent Hub</span>\n\t\t\t\t\t<span className=\"text-[9px] font-medium uppercase tracking-wider text-amber-600 bg-amber-500/10 border border-amber-500/30 rounded px-1.5 py-0.5\">\n\t\t\t\t\t\tBeta\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\n\t\t\t\t<div className=\"flex-1 overflow-y-auto px-5 py-4 space-y-6\">\n\t\t\t\t\t<div className=\"text-xs text-muted-foreground leading-relaxed space-y-2\">\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\tPage Agent Hub lets local apps (e.g. MCP servers) control the Page Agent extension via\n\t\t\t\t\t\t\tWebSocket.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\tCheck out the official{' '}\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/tree/main/packages/mcp\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\tclassName=\"underline hover:text-foreground\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tMCP server package\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<HubConfig />\n\n\t\t\t\t\t<ProtocolDocsCollapsible />\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"border-t px-5 py-3 text-[10px] text-muted-foreground/60 flex items-center justify-between\">\n\t\t\t\t\t<span className=\"font-mono\">v{__VERSION__}</span>\n\t\t\t\t\t<span>\n\t\t\t\t\t\tBuilt with ♥️ by{' '}\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\thref=\"https://github.com/gaomeng1900\"\n\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\tclassName=\"underline hover:text-foreground\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t@Simon\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t</aside>\n\n\t\t\t{/* Right — Live session */}\n\t\t\t<main className=\"flex-1 flex flex-col min-w-0 relative\">\n\t\t\t\t<MotionOverlay active={isRunning} />\n\n\t\t\t\t<header className=\"flex items-center justify-between border-b px-5 h-12\">\n\t\t\t\t\t<div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n\t\t\t\t\t\t<WsIcon className=\"size-3.5\" />\n\t\t\t\t\t\t<span>{wsLabel}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"flex items-center gap-3\">\n\t\t\t\t\t\t<StatusDot status={status} />\n\t\t\t\t\t\t{isRunning && (\n\t\t\t\t\t\t\t<Button variant=\"destructive\" size=\"sm\" onClick={stop} className=\"h-7 text-xs\">\n\t\t\t\t\t\t\t\t<Square className=\"size-3 mr-1\" />\n\t\t\t\t\t\t\t\tStop\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t</header>\n\n\t\t\t\t{/* Task banner */}\n\t\t\t\t{currentTask && (\n\t\t\t\t\t<div className=\"border-b px-5 py-2 bg-muted/30\">\n\t\t\t\t\t\t<div className=\"text-[10px] text-muted-foreground uppercase tracking-wide\">\n\t\t\t\t\t\t\tCurrent Task\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"text-sm font-medium truncate\" title={currentTask}>\n\t\t\t\t\t\t\t{currentTask}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\n\t\t\t\t{/* Event stream */}\n\t\t\t\t<div ref={historyRef} className=\"flex-1 overflow-y-auto p-5 space-y-2\">\n\t\t\t\t\t{!currentTask && history.length === 0 && !isRunning && (\n\t\t\t\t\t\t<div className=\"flex flex-col items-center justify-center h-full text-muted-foreground gap-3\">\n\t\t\t\t\t\t\t<WsIcon className=\"size-10 opacity-30\" />\n\t\t\t\t\t\t\t<p className=\"text-sm\">\n\t\t\t\t\t\t\t\t{wsState === 'connected'\n\t\t\t\t\t\t\t\t\t? 'Waiting for task from external caller…'\n\t\t\t\t\t\t\t\t\t: 'No active session'}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\n\t\t\t\t\t{history.map((event, index) => (\n\t\t\t\t\t\t// eslint-disable-next-line react-x/no-array-index-key\n\t\t\t\t\t\t<EventCard key={index} event={event} />\n\t\t\t\t\t))}\n\n\t\t\t\t\t{activity && <ActivityCard activity={activity} />}\n\t\t\t\t</div>\n\t\t\t</main>\n\t\t</div>\n\t)\n}\n\nfunction HubConfig() {\n\tconst [allowAll, setAllowAll] = useState(false)\n\n\tuseEffect(() => {\n\t\tchrome.storage.local.get('allowAllHubConnection').then((r) => {\n\t\t\tsetAllowAll(r.allowAllHubConnection === true)\n\t\t})\n\t}, [])\n\n\tconst toggle = (checked: boolean) => {\n\t\tsetAllowAll(checked)\n\t\tchrome.storage.local.set({ allowAllHubConnection: checked })\n\t}\n\n\treturn (\n\t\t<div>\n\t\t\t<h3 className=\"text-[11px] font-semibold text-foreground/80 uppercase tracking-wider mb-2\">\n\t\t\t\tConfig\n\t\t\t</h3>\n\t\t\t<div className=\"group/hub relative\">\n\t\t\t\t<label\n\t\t\t\t\tclassName={`flex items-center justify-between p-3 rounded-md border cursor-pointer text-xs ${allowAll ? 'bg-amber-500/10 border-amber-500/30 text-amber-600' : 'bg-muted/50 text-muted-foreground'}`}\n\t\t\t\t>\n\t\t\t\t\tAuto-approve connections\n\t\t\t\t\t<Switch\n\t\t\t\t\t\tchecked={allowAll}\n\t\t\t\t\t\tonCheckedChange={toggle}\n\t\t\t\t\t\tclassName={allowAll ? 'data-[state=checked]:bg-amber-500' : ''}\n\t\t\t\t\t/>\n\t\t\t\t</label>\n\n\t\t\t\t{/* hide with invisible absolute opacity-0*/}\n\t\t\t\t<div className=\"group-hover/hub:visible group-hover/hub:opacity-100 transition-opacity duration-150  left-0 right-0 top-full z-10 pt-2\">\n\t\t\t\t\t<div className=\"relative p-2.5 rounded-md border border-border bg-background/60 backdrop-blur-md shadow-2xl text-muted-foreground text-xs leading-relaxed\">\n\t\t\t\t\t\t<div className=\"absolute -top-1.5 left-5 size-3 rotate-45 rounded-[1px] border-l border-t border-border bg-background/60 backdrop-blur-md\" />\n\t\t\t\t\t\tBy default, each connection requires your approval before running tasks. <br />\n\t\t\t\t\t\tEnable this to skip per-session approval.\n\t\t\t\t\t\t<br />\n\t\t\t\t\t\t<span className=\"font-semibold\">* Use with caution!</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nfunction ProtocolDocsCollapsible() {\n\tconst [open, setOpen] = useState(false)\n\n\treturn (\n\t\t<div>\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tonClick={() => setOpen(!open)}\n\t\t\t\tclassName=\"flex items-center gap-1 text-[11px] font-semibold text-foreground/80 uppercase tracking-wider cursor-pointer\"\n\t\t\t>\n\t\t\t\tDocs\n\t\t\t\t{open ? <FoldVertical className=\"size-3\" /> : <UnfoldVertical className=\"size-3\" />}\n\t\t\t</button>\n\n\t\t\t{open && (\n\t\t\t\t<div className=\"mt-3 space-y-4 text-xs text-muted-foreground\">\n\t\t\t\t\t<p className=\"text-[10px]\">\n\t\t\t\t\t\tConnect via <code className=\"text-[10px]\">hub.html?ws=PORT</code>\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<section>\n\t\t\t\t\t\t<h4 className=\"text-[11px] font-medium text-foreground/60 mb-1.5\">Flow</h4>\n\t\t\t\t\t\t<ol className=\"list-decimal list-inside space-y-1 text-[11px] leading-relaxed\">\n\t\t\t\t\t\t\t<li>Hub opens WS to caller's server</li>\n\t\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t\tSends <code className=\"text-[10px]\">ready</code>\n\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t\tCaller sends <code className=\"text-[10px]\">execute</code> with task\n\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t<li>Hub runs agent, streams events</li>\n\t\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t\tHub sends <code className=\"text-[10px]\">result</code> or{' '}\n\t\t\t\t\t\t\t\t<code className=\"text-[10px]\">error</code>\n\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t</ol>\n\t\t\t\t\t</section>\n\n\t\t\t\t\t<section>\n\t\t\t\t\t\t<h4 className=\"text-[11px] font-medium text-foreground/60 mb-1.5\">Caller → Hub</h4>\n\t\t\t\t\t\t<pre className=\"bg-muted/50 rounded-md p-3 font-mono text-[10px] leading-relaxed whitespace-pre-wrap\">\n\t\t\t\t\t\t\t{`{ type: \"execute\", task: string, config?: object }\n{ type: \"stop\" }`}\n\t\t\t\t\t\t</pre>\n\t\t\t\t\t</section>\n\n\t\t\t\t\t<section>\n\t\t\t\t\t\t<h4 className=\"text-[11px] font-medium text-foreground/60 mb-1.5\">Hub → Caller</h4>\n\t\t\t\t\t\t<pre className=\"bg-muted/50 rounded-md p-3 font-mono text-[10px] leading-relaxed whitespace-pre-wrap\">\n\t\t\t\t\t\t\t{`{ type: \"ready\" }\n{ type: \"result\", success: boolean, data: string }\n{ type: \"error\", message: string }`}\n\t\t\t\t\t\t</pre>\n\t\t\t\t\t</section>\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/extension/src/entrypoints/hub/hub-ws.ts",
    "content": "/**\n * Hub WebSocket Protocol\n *\n * Hub connects as WS client to `ws://localhost:{port}`.\n * All messages are JSON. One task at a time.\n *\n * Inbound (Caller → Hub):\n *   { type: \"execute\", task: string, config?: object }\n *   { type: \"stop\" }\n *\n * Outbound (Hub → Caller):\n *   { type: \"ready\" }\n *   { type: \"result\", success: boolean, data: string }\n *   { type: \"error\", message: string }\n */\nimport type { ExecutionResult } from '@page-agent/core'\nimport { useEffect, useRef, useState } from 'react'\n\nimport type { ExtConfig } from '@/agent/useAgent'\n\n// --- Protocol types ---\n\ninterface ExecuteMessage {\n\ttype: 'execute'\n\ttask: string\n\tconfig?: Record<string, unknown>\n}\n\ninterface StopMessage {\n\ttype: 'stop'\n}\n\ntype InboundMessage = ExecuteMessage | StopMessage\n\ninterface ReadyMessage {\n\ttype: 'ready'\n}\n\ninterface ResultMessage {\n\ttype: 'result'\n\tsuccess: boolean\n\tdata: string\n}\n\ninterface ErrorMessage {\n\ttype: 'error'\n\tmessage: string\n}\n\ntype OutboundMessage = ReadyMessage | ResultMessage | ErrorMessage\n\nexport type HubWsState = 'connecting' | 'connected' | 'disconnected'\n\n// --- HubWs class ---\n\nexport interface HubWsHandlers {\n\tonExecute: (\n\t\ttask: string,\n\t\tconfig?: Record<string, unknown>\n\t) => Promise<{ success: boolean; data: string }>\n\tonStop: () => void\n}\n\n/**\n * Framework-agnostic WebSocket client for Hub.\n * Connects to an external WS server, receives tasks, dispatches to handlers,\n * and sends results back. No React, no DOM.\n */\nexport class HubWs {\n\t#ws: WebSocket | null = null\n\t#state: HubWsState = 'disconnected'\n\t#busy = false\n\t#approved = false\n\t#handlers: HubWsHandlers\n\t#port: number\n\t#onStateChange: (state: HubWsState) => void\n\n\tconstructor(port: number, handlers: HubWsHandlers, onStateChange: (state: HubWsState) => void) {\n\t\tthis.#port = port\n\t\tthis.#handlers = handlers\n\t\tthis.#onStateChange = onStateChange\n\t}\n\n\tget state() {\n\t\treturn this.#state\n\t}\n\n\tget busy() {\n\t\treturn this.#busy\n\t}\n\n\tconnect() {\n\t\tif (this.#ws) return\n\t\tthis.#setState('connecting')\n\n\t\tconst ws = new WebSocket(`ws://localhost:${this.#port}`)\n\t\tthis.#ws = ws\n\n\t\tws.addEventListener('open', () => {\n\t\t\tthis.#setState('connected')\n\t\t\tthis.#send({ type: 'ready' })\n\t\t})\n\n\t\tws.addEventListener('close', () => {\n\t\t\tthis.#ws = null\n\t\t\tthis.#busy = false\n\t\t\tthis.#approved = false\n\t\t\tthis.#setState('disconnected')\n\t\t})\n\n\t\tws.addEventListener('message', (event) => {\n\t\t\tthis.#handleMessage(event.data as string)\n\t\t})\n\t}\n\n\tdisconnect() {\n\t\tthis.#ws?.close()\n\t\tthis.#ws = null\n\t\tthis.#busy = false\n\t\tthis.#approved = false\n\t\tthis.#setState('disconnected')\n\t}\n\n\t#setState(state: HubWsState) {\n\t\tif (this.#state === state) return\n\t\tthis.#state = state\n\t\tthis.#onStateChange(state)\n\t}\n\n\t#send(msg: OutboundMessage) {\n\t\tif (this.#ws?.readyState === WebSocket.OPEN) {\n\t\t\tthis.#ws.send(JSON.stringify(msg))\n\t\t}\n\t}\n\n\tasync #handleMessage(raw: string) {\n\t\tlet msg: InboundMessage\n\t\ttry {\n\t\t\tmsg = JSON.parse(raw)\n\t\t} catch {\n\t\t\treturn\n\t\t}\n\n\t\tif (!(await this.#checkApproval())) {\n\t\t\tthis.#send({ type: 'error', message: 'User denied the connection request.' })\n\t\t\treturn\n\t\t}\n\n\t\tswitch (msg.type) {\n\t\t\tcase 'execute':\n\t\t\t\tthis.#handleExecute(msg)\n\t\t\t\tbreak\n\t\t\tcase 'stop':\n\t\t\t\tthis.#handlers.onStop()\n\t\t\t\tbreak\n\t\t}\n\t}\n\n\tasync #checkApproval(): Promise<boolean> {\n\t\tif (this.#approved) return true\n\n\t\tconst { allowAllHubConnection } = await chrome.storage.local.get('allowAllHubConnection')\n\t\tif (allowAllHubConnection === true) {\n\t\t\tthis.#approved = true\n\t\t\treturn true\n\t\t}\n\n\t\tconst ok = window.confirm(\n\t\t\t'An external application is requesting to control your browser via Page Agent Ext.\\nAllow this session?'\n\t\t)\n\t\tif (ok) this.#approved = true\n\t\treturn ok\n\t}\n\n\tasync #handleExecute(msg: ExecuteMessage) {\n\t\tif (this.#busy) {\n\t\t\tthis.#send({ type: 'error', message: 'Hub is busy with another task' })\n\t\t\treturn\n\t\t}\n\n\t\tthis.#busy = true\n\t\ttry {\n\t\t\tconst result = await this.#handlers.onExecute(msg.task, msg.config)\n\t\t\tthis.#send({ type: 'result', success: result.success, data: result.data })\n\t\t} catch (err) {\n\t\t\tthis.#send({ type: 'error', message: err instanceof Error ? err.message : String(err) })\n\t\t} finally {\n\t\t\tthis.#busy = false\n\t\t}\n\t}\n}\n\n// --- React hook ---\n\n/**\n * React hook that bridges HubWs to the agent's execute/stop/configure.\n * Handles the config-before-execute dance internally.\n */\nexport function useHubWs(\n\texecute: (task: string) => Promise<ExecutionResult>,\n\tstop: () => void,\n\tconfigure: (config: ExtConfig) => Promise<void>,\n\tconfig: ExtConfig | null\n): { wsState: HubWsState } {\n\tconst wsPort = new URLSearchParams(location.search).get('ws')\n\tconst [wsState, setWsState] = useState<HubWsState>(() => (wsPort ? 'connecting' : 'disconnected'))\n\tconst hubWsRef = useRef<HubWs | null>(null)\n\n\tconst latest = useRef({ execute, stop, configure, config })\n\tuseEffect(() => {\n\t\tlatest.current = { execute, stop, configure, config }\n\t})\n\n\tuseEffect(() => {\n\t\tif (!wsPort) return\n\n\t\tconst hubWs = new HubWs(\n\t\t\tNumber(wsPort),\n\t\t\t{\n\t\t\t\tonExecute: async (task, incomingConfig) => {\n\t\t\t\t\tconst { execute, configure, config } = latest.current\n\t\t\t\t\tif (incomingConfig) {\n\t\t\t\t\t\tawait configure({ ...config, ...incomingConfig } as ExtConfig)\n\t\t\t\t\t}\n\t\t\t\t\tconst result = await execute(task)\n\t\t\t\t\treturn { success: result.success, data: result.data }\n\t\t\t\t},\n\t\t\t\tonStop: () => latest.current.stop(),\n\t\t\t},\n\t\t\tsetWsState\n\t\t)\n\n\t\thubWs.connect()\n\t\thubWsRef.current = hubWs\n\n\t\treturn () => {\n\t\t\thubWs.disconnect()\n\t\t\thubWsRef.current = null\n\t\t}\n\t}, [wsPort])\n\n\treturn { wsState }\n}\n"
  },
  {
    "path": "packages/extension/src/entrypoints/hub/index.html",
    "content": "<!doctype html>\n<html>\n\t<head>\n\t\t<meta charset=\"UTF-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\t\t<link rel=\"icon\" type=\"image/png\" href=\"/assets/page-agent-64.png\" />\n\t\t<title>Page Agent Hub</title>\n\t</head>\n\t<body>\n\t\t<div id=\"root\"></div>\n\t\t<script type=\"module\" src=\"./main.tsx\"></script>\n\t</body>\n</html>\n"
  },
  {
    "path": "packages/extension/src/entrypoints/hub/main.tsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom/client'\n\nimport { ErrorBoundary } from '@/components/ErrorBoundary'\n\nimport App from './App'\n\nimport '@/assets/index.css'\n\nconst syncDarkMode = () => {\n\tdocument.documentElement.classList.toggle(\n\t\t'dark',\n\t\tmatchMedia('(prefers-color-scheme: dark)').matches\n\t)\n}\nsyncDarkMode()\nmatchMedia('(prefers-color-scheme: dark)').addEventListener('change', syncDarkMode)\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n\t<React.StrictMode>\n\t\t<ErrorBoundary>\n\t\t\t<App />\n\t\t</ErrorBoundary>\n\t</React.StrictMode>\n)\n"
  },
  {
    "path": "packages/extension/src/entrypoints/main-world.ts",
    "content": "import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'\n\nexport type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>\n\nexport interface ExecuteConfig {\n\tbaseURL: string\n\tmodel: string\n\tapiKey?: string\n\n\t/**\n\t * Whether to include the initial tab (that holds this main world script) in the task.\n\t * @default true\n\t */\n\tincludeInitialTab?: boolean\n\n\tonStatusChange?: (status: AgentStatus) => void\n\tonActivity?: (activity: AgentActivity) => void\n\tonHistoryUpdate?: (history: HistoricalEvent[]) => void\n}\n\nexport default defineUnlistedScript(() => {\n\tlet _lastId = 0\n\tfunction getId() {\n\t\t_lastId += 1\n\t\treturn _lastId\n\t}\n\n\tconst execute: Execute = async (task, config) => {\n\t\tif (typeof task !== 'string') throw new Error('Task must be a string')\n\t\tif (task.trim().length === 0) throw new Error('Task cannot be empty')\n\t\tif (!config) throw new Error('Config is required')\n\t\tif (!config.baseURL) throw new Error('Config must have a baseURL')\n\t\tif (!config.model) throw new Error('Config must have a model')\n\n\t\tconst id = getId()\n\n\t\tconst promise = new Promise<ExecutionResult>((resolve, reject) => {\n\t\t\tfunction handleMessage(e: MessageEvent) {\n\t\t\t\tconst data = e.data\n\t\t\t\tif (typeof data !== 'object' || data === null) return\n\t\t\t\tif (data.channel !== 'PAGE_AGENT_EXT_RESPONSE') return\n\t\t\t\tif (data.id !== id) return\n\n\t\t\t\t// events\n\n\t\t\t\tif (data.action === 'status_change_event' && config.onStatusChange) {\n\t\t\t\t\tconfig.onStatusChange(data.payload)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif (data.action === 'activity_event' && config.onActivity) {\n\t\t\t\t\tconfig.onActivity(data.payload)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif (data.action === 'history_change_event' && config.onHistoryUpdate) {\n\t\t\t\t\tconfig.onHistoryUpdate(data.payload)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif (data.action !== 'execute_result') return\n\n\t\t\t\t// execute_result\n\n\t\t\t\twindow.removeEventListener('message', handleMessage)\n\n\t\t\t\tif (data.error) {\n\t\t\t\t\treject(new Error(data.error))\n\t\t\t\t} else {\n\t\t\t\t\tresolve(data.payload)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// @note will be removed on dispose or result\n\t\t\twindow.addEventListener('message', handleMessage)\n\t\t})\n\n\t\twindow.postMessage(\n\t\t\t{\n\t\t\t\tchannel: 'PAGE_AGENT_EXT_REQUEST',\n\t\t\t\tid,\n\t\t\t\taction: 'execute',\n\t\t\t\tpayload: {\n\t\t\t\t\ttask,\n\t\t\t\t\tconfig: {\n\t\t\t\t\t\tbaseURL: config.baseURL,\n\t\t\t\t\t\tmodel: config.model,\n\t\t\t\t\t\tapiKey: config.apiKey,\n\t\t\t\t\t\tincludeInitialTab: config.includeInitialTab,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t'*'\n\t\t)\n\n\t\treturn promise\n\t}\n\n\tconst stop = () => {\n\t\tconst id = getId()\n\n\t\twindow.postMessage(\n\t\t\t{\n\t\t\t\tchannel: 'PAGE_AGENT_EXT_REQUEST',\n\t\t\t\tid,\n\t\t\t\taction: 'stop',\n\t\t\t},\n\t\t\t'*'\n\t\t)\n\t}\n\n\t;(window as any).PAGE_AGENT_EXT_VERSION = __VERSION__\n\t;(window as any).PAGE_AGENT_EXT = {\n\t\tversion: __VERSION__,\n\t\texecute,\n\t\tstop,\n\t}\n})\n"
  },
  {
    "path": "packages/extension/src/entrypoints/sidepanel/App.tsx",
    "content": "import { History, Send, Settings, Square } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { ConfigPanel } from '@/components/ConfigPanel'\nimport { HistoryDetail } from '@/components/HistoryDetail'\nimport { HistoryList } from '@/components/HistoryList'\nimport { ActivityCard, EventCard } from '@/components/cards'\nimport { EmptyState, Logo, MotionOverlay, StatusDot } from '@/components/misc'\nimport { Button } from '@/components/ui/button'\nimport {\n\tInputGroup,\n\tInputGroupAddon,\n\tInputGroupButton,\n\tInputGroupTextarea,\n} from '@/components/ui/input-group'\nimport { saveSession } from '@/lib/db'\n\nimport { useAgent } from '../../agent/useAgent'\n\ntype View =\n\t| { name: 'chat' }\n\t| { name: 'config' }\n\t| { name: 'history' }\n\t| { name: 'history-detail'; sessionId: string }\n\nexport default function App() {\n\tconst [view, setView] = useState<View>({ name: 'chat' })\n\tconst [inputValue, setInputValue] = useState('')\n\tconst historyRef = useRef<HTMLDivElement>(null)\n\tconst textareaRef = useRef<HTMLTextAreaElement>(null)\n\n\tconst { status, history, activity, currentTask, config, execute, stop, configure } = useAgent()\n\n\t// Persist session when task finishes\n\tconst prevStatusRef = useRef(status)\n\tuseEffect(() => {\n\t\tconst prev = prevStatusRef.current\n\t\tprevStatusRef.current = status\n\n\t\tif (\n\t\t\tprev === 'running' &&\n\t\t\t(status === 'completed' || status === 'error') &&\n\t\t\thistory.length > 0 &&\n\t\t\tcurrentTask\n\t\t) {\n\t\t\tsaveSession({ task: currentTask, history, status }).catch((err) =>\n\t\t\t\tconsole.error('[SidePanel] Failed to save session:', err)\n\t\t\t)\n\t\t}\n\t}, [status, history, currentTask])\n\n\t// Auto-scroll to bottom on new events\n\tuseEffect(() => {\n\t\tif (historyRef.current) {\n\t\t\thistoryRef.current.scrollTop = historyRef.current.scrollHeight\n\t\t}\n\t}, [history, activity])\n\n\tconst runTask = useCallback(\n\t\t(task: string) => {\n\t\t\tconst normalizedTask = task.trim()\n\t\t\tif (!normalizedTask || status === 'running') return\n\n\t\t\tsetInputValue('')\n\t\t\tsetView({ name: 'chat' })\n\n\t\t\texecute(normalizedTask).catch((error) => {\n\t\t\t\tconsole.error('[SidePanel] Failed to execute task:', error)\n\t\t\t})\n\t\t},\n\t\t[execute, status]\n\t)\n\n\tconst handleSubmit = useCallback(\n\t\t(e?: React.SyntheticEvent) => {\n\t\t\te?.preventDefault()\n\t\t\trunTask(inputValue)\n\t\t},\n\t\t[inputValue, runTask]\n\t)\n\n\tconst handleStop = useCallback(() => {\n\t\tconsole.log('[SidePanel] Stopping task...')\n\t\tstop()\n\t}, [stop])\n\n\tconst handleKeyDown = (e: React.KeyboardEvent) => {\n\t\tif (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {\n\t\t\te.preventDefault()\n\t\t\thandleSubmit()\n\t\t}\n\t}\n\n\t// --- View routing ---\n\n\tif (view.name === 'config') {\n\t\treturn (\n\t\t\t<ConfigPanel\n\t\t\t\tconfig={config}\n\t\t\t\tonSave={async (newConfig) => {\n\t\t\t\t\tawait configure(newConfig)\n\t\t\t\t\tsetView({ name: 'chat' })\n\t\t\t\t}}\n\t\t\t\tonClose={() => setView({ name: 'chat' })}\n\t\t\t/>\n\t\t)\n\t}\n\n\tif (view.name === 'history') {\n\t\treturn (\n\t\t\t<HistoryList\n\t\t\t\tonSelect={(id) => setView({ name: 'history-detail', sessionId: id })}\n\t\t\t\tonBack={() => setView({ name: 'chat' })}\n\t\t\t\tonRerun={runTask}\n\t\t\t/>\n\t\t)\n\t}\n\n\tif (view.name === 'history-detail') {\n\t\treturn (\n\t\t\t<HistoryDetail\n\t\t\t\tsessionId={view.sessionId}\n\t\t\t\tonBack={() => setView({ name: 'history' })}\n\t\t\t\tonRerun={runTask}\n\t\t\t/>\n\t\t)\n\t}\n\n\t// --- Chat view ---\n\n\tconst isRunning = status === 'running'\n\tconst showEmptyState = !currentTask && history.length === 0 && !isRunning\n\n\treturn (\n\t\t<div className=\"relative flex flex-col h-screen bg-background\">\n\t\t\t<MotionOverlay active={isRunning} />\n\t\t\t{/* Header */}\n\t\t\t<header className=\"flex items-center justify-between border-b px-3 py-2\">\n\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t<Logo className=\"size-5\" />\n\t\t\t\t\t<span className=\"text-sm font-medium\">Page Agent Ext</span>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"flex items-center gap-1\">\n\t\t\t\t\t<StatusDot status={status} />\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tsize=\"icon-sm\"\n\t\t\t\t\t\tonClick={() => setView({ name: 'history' })}\n\t\t\t\t\t\tclassName=\"cursor-pointer\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<History className=\"size-3.5\" />\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tsize=\"icon-sm\"\n\t\t\t\t\t\tonClick={() => setView({ name: 'config' })}\n\t\t\t\t\t\tclassName=\"cursor-pointer\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<Settings className=\"size-3.5\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</header>\n\n\t\t\t{/* Content */}\n\t\t\t<main className=\"flex-1 overflow-hidden flex flex-col\">\n\t\t\t\t{/* Current task */}\n\t\t\t\t{currentTask && (\n\t\t\t\t\t<div className=\"border-b px-3 py-2 bg-muted/30\">\n\t\t\t\t\t\t<div className=\"text-[10px] text-muted-foreground uppercase tracking-wide\">Task</div>\n\t\t\t\t\t\t<div className=\"text-xs font-medium truncate\" title={currentTask}>\n\t\t\t\t\t\t\t{currentTask}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\n\t\t\t\t{/* History */}\n\t\t\t\t<div ref={historyRef} className=\"flex-1 overflow-y-auto p-3 space-y-2\">\n\t\t\t\t\t{showEmptyState && <EmptyState />}\n\n\t\t\t\t\t{history.map((event, index) => (\n\t\t\t\t\t\t// eslint-disable-next-line react-x/no-array-index-key\n\t\t\t\t\t\t<EventCard key={index} event={event} />\n\t\t\t\t\t))}\n\n\t\t\t\t\t{/* Activity indicator at bottom */}\n\t\t\t\t\t{activity && <ActivityCard activity={activity} />}\n\t\t\t\t</div>\n\t\t\t</main>\n\n\t\t\t{/* Input */}\n\t\t\t<footer className=\"border-t p-3\">\n\t\t\t\t<InputGroup className=\"relative rounded-lg\">\n\t\t\t\t\t<InputGroupTextarea\n\t\t\t\t\t\tref={textareaRef}\n\t\t\t\t\t\tplaceholder=\"Describe your task... (Enter to send)\"\n\t\t\t\t\t\tvalue={inputValue}\n\t\t\t\t\t\tonChange={(e) => setInputValue(e.target.value)}\n\t\t\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t\t\t\tdisabled={isRunning}\n\t\t\t\t\t\tclassName=\"text-xs pr-12 min-h-10\"\n\t\t\t\t\t/>\n\t\t\t\t\t<InputGroupAddon align=\"inline-end\" className=\"absolute bottom-0 right-0\">\n\t\t\t\t\t\t{isRunning ? (\n\t\t\t\t\t\t\t<InputGroupButton\n\t\t\t\t\t\t\t\tsize=\"icon-sm\"\n\t\t\t\t\t\t\t\tvariant=\"destructive\"\n\t\t\t\t\t\t\t\tonClick={handleStop}\n\t\t\t\t\t\t\t\tclassName=\"size-7\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Square className=\"size-3\" />\n\t\t\t\t\t\t\t</InputGroupButton>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<InputGroupButton\n\t\t\t\t\t\t\t\tsize=\"icon-sm\"\n\t\t\t\t\t\t\t\tvariant=\"default\"\n\t\t\t\t\t\t\t\tonClick={() => handleSubmit()}\n\t\t\t\t\t\t\t\tdisabled={!inputValue.trim()}\n\t\t\t\t\t\t\t\tclassName=\"size-7 cursor-pointer\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Send className=\"size-3\" />\n\t\t\t\t\t\t\t</InputGroupButton>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</InputGroupAddon>\n\t\t\t\t</InputGroup>\n\t\t\t</footer>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/extension/src/entrypoints/sidepanel/index.html",
    "content": "<!doctype html>\n<html>\n\t<head>\n\t\t<meta charset=\"UTF-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\t\t<link rel=\"icon\" type=\"image/png\" href=\"/assets/page-agent-64.png\" />\n\t\t<title>Page Agent</title>\n\t</head>\n\t<body>\n\t\t<div id=\"root\"></div>\n\t\t<script type=\"module\" src=\"./main.tsx\"></script>\n\t</body>\n</html>\n"
  },
  {
    "path": "packages/extension/src/entrypoints/sidepanel/main.tsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom/client'\n\nimport { ErrorBoundary } from '@/components/ErrorBoundary'\n\nimport App from './App'\n\nimport '@/assets/index.css'\n\n// Sync dark mode with system preference\nconst syncDarkMode = () => {\n\tdocument.documentElement.classList.toggle(\n\t\t'dark',\n\t\tmatchMedia('(prefers-color-scheme: dark)').matches\n\t)\n}\nsyncDarkMode()\nmatchMedia('(prefers-color-scheme: dark)').addEventListener('change', syncDarkMode)\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n\t<React.StrictMode>\n\t\t<ErrorBoundary>\n\t\t\t<App />\n\t\t</ErrorBoundary>\n\t</React.StrictMode>\n)\n"
  },
  {
    "path": "packages/extension/src/lib/db.ts",
    "content": "import type { HistoricalEvent } from '@page-agent/core'\nimport { type DBSchema, type IDBPDatabase, openDB } from 'idb'\n\nconst DB_NAME = 'page-agent-ext'\nconst DB_VERSION = 1\n\nexport interface SessionRecord {\n\tid: string\n\ttask: string\n\thistory: HistoricalEvent[]\n\tstatus: 'completed' | 'error'\n\tcreatedAt: number\n}\n\ninterface PageAgentDB extends DBSchema {\n\tsessions: {\n\t\tkey: string\n\t\tvalue: SessionRecord\n\t\tindexes: { 'by-created': number }\n\t}\n}\n\nlet dbPromise: Promise<IDBPDatabase<PageAgentDB>> | null = null\n\nfunction getDB() {\n\tif (!dbPromise) {\n\t\tdbPromise = openDB<PageAgentDB>(DB_NAME, DB_VERSION, {\n\t\t\tupgrade(db) {\n\t\t\t\tconst store = db.createObjectStore('sessions', { keyPath: 'id' })\n\t\t\t\tstore.createIndex('by-created', 'createdAt')\n\t\t\t},\n\t\t})\n\t}\n\treturn dbPromise\n}\n\nexport async function saveSession(\n\tsession: Omit<SessionRecord, 'id' | 'createdAt'>\n): Promise<SessionRecord> {\n\tconst db = await getDB()\n\tconst record: SessionRecord = {\n\t\t...session,\n\t\tid: crypto.randomUUID(),\n\t\tcreatedAt: Date.now(),\n\t}\n\tawait db.put('sessions', record)\n\treturn record\n}\n\n/** List sessions, newest first */\nexport async function listSessions(): Promise<SessionRecord[]> {\n\tconst db = await getDB()\n\tconst all = await db.getAllFromIndex('sessions', 'by-created')\n\treturn all.reverse()\n}\n\nexport async function getSession(id: string): Promise<SessionRecord | undefined> {\n\tconst db = await getDB()\n\treturn db.get('sessions', id)\n}\n\nexport async function deleteSession(id: string): Promise<void> {\n\tconst db = await getDB()\n\tawait db.delete('sessions', id)\n}\n\nexport async function clearSessions(): Promise<void> {\n\tconst db = await getDB()\n\tawait db.clear('sessions')\n}\n"
  },
  {
    "path": "packages/extension/src/lib/history-export.ts",
    "content": "import type { HistoricalEvent } from '@page-agent/core'\n\nconst EXPORT_FILE_PREFIX = 'page-agent-history'\nconst MAX_TASK_SLUG_LENGTH = 40\n\nexport function serializeHistoryExport(history: HistoricalEvent[]): string {\n\treturn `${JSON.stringify(history, null, 2)}\\n`\n}\n\nexport function buildHistoryExportFilename(task: string, createdAt: number): string {\n\tconst taskSlug = sanitizeTaskForFilename(task)\n\tconst timestamp = formatTimestampForFilename(createdAt)\n\n\treturn taskSlug\n\t\t? `${EXPORT_FILE_PREFIX}-${taskSlug}-${timestamp}.json`\n\t\t: `${EXPORT_FILE_PREFIX}-${timestamp}.json`\n}\n\nexport function downloadHistoryExport(\n\ttask: string,\n\tcreatedAt: number,\n\thistory: HistoricalEvent[]\n): void {\n\tconst filename = buildHistoryExportFilename(task, createdAt)\n\tconst content = serializeHistoryExport(history)\n\tconst blob = new Blob([content], { type: 'application/json;charset=utf-8' })\n\tconst url = URL.createObjectURL(blob)\n\tconst link = document.createElement('a')\n\n\tlink.href = url\n\tlink.download = filename\n\tlink.click()\n\n\tURL.revokeObjectURL(url)\n}\n\nfunction sanitizeTaskForFilename(task: string): string {\n\treturn task\n\t\t.trim()\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t.replace(/^-+|-+$/g, '')\n\t\t.slice(0, MAX_TASK_SLUG_LENGTH)\n}\n\nfunction formatTimestampForFilename(createdAt: number): string {\n\tconst date = new Date(createdAt)\n\tconst year = date.getFullYear()\n\tconst month = pad(date.getMonth() + 1)\n\tconst day = pad(date.getDate())\n\tconst hours = pad(date.getHours())\n\tconst minutes = pad(date.getMinutes())\n\tconst seconds = pad(date.getSeconds())\n\n\treturn `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`\n}\n\nfunction pad(value: number): string {\n\treturn value.toString().padStart(2, '0')\n}\n"
  },
  {
    "path": "packages/extension/src/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "packages/extension/src/types/assets.d.ts",
    "content": "// Asset type declarations\ndeclare module '*.webp' {\n\tconst src: string\n\texport default src\n}\n\ndeclare module '*.png' {\n\tconst src: string\n\texport default src\n}\n\ndeclare module '*.jpg' {\n\tconst src: string\n\texport default src\n}\n\ndeclare module '*.jpeg' {\n\tconst src: string\n\texport default src\n}\n\ndeclare module '*.svg' {\n\tconst src: string\n\texport default src\n}\n"
  },
  {
    "path": "packages/extension/src/types/globals.d.ts",
    "content": "declare const __VERSION__: string\n"
  },
  {
    "path": "packages/extension/src/types/markdown.d.ts",
    "content": "declare module '*.md?raw' {\n\tconst content: string\n\texport default content\n}\n"
  },
  {
    "path": "packages/extension/tsconfig.json",
    "content": "{\n    \"extends\": \"./.wxt/tsconfig.json\",\n    \"compilerOptions\": {\n        \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.tsbuildinfo\",\n        \"useDefineForClassFields\": true,\n        \"noEmit\": false,\n        \"allowImportingTsExtensions\": false,\n        \"strictNullChecks\": true,\n        \"jsx\": \"react-jsx\",\n        \"baseUrl\": \".\",\n        \"paths\": {\n            // Self root\n            \"@/*\": [\"src/*\"],\n\n            \"@page-agent/llms\": [\"../llms/src/index.ts\"],\n            \"@page-agent/page-controller\": [\"../page-controller/src/PageController.ts\"],\n            \"@page-agent/core\": [\"../core/src/PageAgentCore.ts\"],\n            \"@page-agent/ui\": [\"../ui/src/index.ts\"]\n        }\n    },\n    \"references\": [\n        //\n        { \"path\": \"../llms\" },\n        { \"path\": \"../page-controller\" },\n        { \"path\": \"../core\" },\n        { \"path\": \"../ui\" }\n    ]\n}\n"
  },
  {
    "path": "packages/extension/wxt.config.js",
    "content": "import tailwindcss from '@tailwindcss/vite'\nimport { mkdirSync, readFileSync } from 'node:fs'\nimport { defineConfig } from 'wxt'\n\nconst chromeProfile = '.wxt/chrome-data'\nmkdirSync(chromeProfile, { recursive: true })\n\nconst pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))\n\n// See https://wxt.dev/api/config.html\nexport default defineConfig({\n\tsrcDir: 'src',\n\tmodules: ['@wxt-dev/module-react'],\n\twebExt: {\n\t\tchromiumProfile: chromeProfile,\n\t\tkeepProfileChanges: true,\n\t\tchromiumArgs: ['--hide-crash-restore-bubble'],\n\t},\n\tvite: () => ({\n\t\tplugins: [tailwindcss()],\n\t\tdefine: {\n\t\t\t__VERSION__: JSON.stringify(pkg.version),\n\t\t},\n\t\toptimizeDeps: {\n\t\t\tforce: true,\n\t\t},\n\t\tbuild: {\n\t\t\tminify: false,\n\t\t\tchunkSizeWarningLimit: 2000,\n\t\t\tcssCodeSplit: true,\n\t\t\trollupOptions: {\n\t\t\t\tonwarn: function (message, handler) {\n\t\t\t\t\tif (message.code === 'EVAL') return\n\t\t\t\t\thandler(message)\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}),\n\tzip: {\n\t\tartifactTemplate: 'page-agent-ext-{{version}}-{{browser}}.zip',\n\t},\n\tmanifest: {\n\t\tkey: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqbzT0iTYeYlnCvDJIGDnGU8oarJgZILDzSfLi/ufuSxXEPDKuMyD892GhvrMCZNVHS11Sh6NYUOc/PcUOhtaR2urHtcNkrpSJNV10zUamY7fxBdVEkOucfyLu8INVy+teis62MoRWYPaUPkfZUjrLGW8MsZ9aFzARfu9GGDEp2EAYsWDN6w6vyz9LJ82pm542EWnVT4MjmDPgvYFCWGBtaU/dfHD+GAX6URJFapsCvryVURKJ+76c/GO9/I3EX1IBfbY6dec78bLCMvVxiTmiv36KyGPwX1OpakW8IiCpXWdbAxjm+plbYlp5t5zTyyoE3sOSFeXsBH0Kg27o8GcvQIDAQAB',\n\t\tdefault_locale: 'en',\n\t\tname: '__MSG_extName__',\n\t\tdescription: '__MSG_extDescription__',\n\t\thomepage_url: 'https://alibaba.github.io/page-agent/',\n\t\tpermissions: ['tabs', 'tabGroups', 'sidePanel', 'storage'],\n\t\thost_permissions: ['<all_urls>'],\n\t\ticons: {\n\t\t\t64: 'assets/page-agent-64.png',\n\t\t},\n\t\taction: {\n\t\t\tdefault_title: '__MSG_extActionTitle__',\n\t\t},\n\t\tweb_accessible_resources: [\n\t\t\t{\n\t\t\t\tresources: ['main-world.js'],\n\t\t\t\tmatches: ['*://*/*'],\n\t\t\t},\n\t\t],\n\t\tside_panel: {\n\t\t\tdefault_path: 'sidepanel/index.html',\n\t\t},\n\t\texternally_connectable: {\n\t\t\tmatches: ['http://localhost/*'],\n\t\t},\n\t},\n})\n"
  },
  {
    "path": "packages/llms/package.json",
    "content": "{\n    \"name\": \"@page-agent/llms\",\n    \"version\": \"1.6.0\",\n    \"type\": \"module\",\n    \"main\": \"./dist/lib/page-agent-llms.js\",\n    \"module\": \"./dist/lib/page-agent-llms.js\",\n    \"types\": \"./dist/lib/index.d.ts\",\n    \"exports\": {\n        \".\": {\n            \"types\": \"./dist/lib/index.d.ts\",\n            \"import\": \"./dist/lib/page-agent-llms.js\",\n            \"default\": \"./dist/lib/page-agent-llms.js\"\n        }\n    },\n    \"files\": [\n        \"dist/\"\n    ],\n    \"description\": \"LLM client with reflection-before-action mental model for page-agent\",\n    \"keywords\": [\n        \"page-agent\",\n        \"llm\",\n        \"openai\",\n        \"tool-calling\",\n        \"agent\"\n    ],\n    \"author\": \"Simon<gaomeng1900>\",\n    \"license\": \"MIT\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/alibaba/page-agent.git\",\n        \"directory\": \"packages/llms\"\n    },\n    \"homepage\": \"https://alibaba.github.io/page-agent/\",\n    \"scripts\": {\n        \"build\": \"vite build\",\n        \"prepublishOnly\": \"node -e \\\"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\\\"\",\n        \"postpublish\": \"node -e \\\"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\\\"\"\n    },\n    \"dependencies\": {\n        \"chalk\": \"^5.6.2\"\n    },\n    \"peerDependencies\": {\n        \"zod\": \"^3.25.0 || ^4.0.0\"\n    },\n    \"devDependencies\": {\n        \"zod\": \"^4.3.5\"\n    }\n}\n"
  },
  {
    "path": "packages/llms/src/OpenAIClient.ts",
    "content": "/**\n * OpenAI Client implementation\n */\nimport * as z from 'zod/v4'\n\nimport { InvokeError, InvokeErrorType } from './errors'\nimport type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'\nimport { modelPatch, zodToOpenAITool } from './utils'\n\n/**\n * Client for OpenAI compatible APIs\n */\nexport class OpenAIClient implements LLMClient {\n\tconfig: Required<LLMConfig>\n\tprivate fetch: typeof globalThis.fetch\n\n\tconstructor(config: Required<LLMConfig>) {\n\t\tthis.config = config\n\t\tthis.fetch = config.customFetch\n\t}\n\n\tasync invoke(\n\t\tmessages: Message[],\n\t\ttools: Record<string, Tool>,\n\t\tabortSignal?: AbortSignal,\n\t\toptions?: InvokeOptions\n\t): Promise<InvokeResult> {\n\t\t// 1. Convert tools to OpenAI format\n\t\tconst openaiTools = Object.entries(tools).map(([name, t]) => zodToOpenAITool(name, t))\n\n\t\t// Build request body\n\n\t\tlet toolChoice: unknown = 'required'\n\t\tif (options?.toolChoiceName && !this.config.disableNamedToolChoice) {\n\t\t\ttoolChoice = { type: 'function', function: { name: options.toolChoiceName } }\n\t\t}\n\n\t\tconst requestBody: Record<string, unknown> = {\n\t\t\tmodel: this.config.model,\n\t\t\ttemperature: this.config.temperature,\n\t\t\tmessages,\n\t\t\ttools: openaiTools,\n\t\t\tparallel_tool_calls: false,\n\t\t\ttool_choice: toolChoice,\n\t\t}\n\n\t\tmodelPatch(requestBody)\n\n\t\t// 2. Call API\n\t\tlet response: Response\n\t\ttry {\n\t\t\tresponse = await this.fetch(`${this.config.baseURL}/chat/completions`, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t...(this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }),\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(requestBody),\n\t\t\t\tsignal: abortSignal,\n\t\t\t})\n\t\t} catch (error: unknown) {\n\t\t\tconst isAbortError = (error as any)?.name === 'AbortError'\n\t\t\tconst errorMessage = isAbortError ? 'Network request aborted' : 'Network request failed'\n\t\t\tif (!isAbortError) console.error(error)\n\t\t\tthrow new InvokeError(InvokeErrorType.NETWORK_ERROR, errorMessage, error)\n\t\t}\n\n\t\t// 3. Handle HTTP errors\n\t\tif (!response.ok) {\n\t\t\tconst errorData = await response.json().catch()\n\t\t\tconst errorMessage =\n\t\t\t\t(errorData as { error?: { message?: string } }).error?.message || response.statusText\n\n\t\t\tif (response.status === 401 || response.status === 403) {\n\t\t\t\tthrow new InvokeError(\n\t\t\t\t\tInvokeErrorType.AUTH_ERROR,\n\t\t\t\t\t`Authentication failed: ${errorMessage}`,\n\t\t\t\t\terrorData\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (response.status === 429) {\n\t\t\t\tthrow new InvokeError(\n\t\t\t\t\tInvokeErrorType.RATE_LIMIT,\n\t\t\t\t\t`Rate limit exceeded: ${errorMessage}`,\n\t\t\t\t\terrorData\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (response.status >= 500) {\n\t\t\t\tthrow new InvokeError(\n\t\t\t\t\tInvokeErrorType.SERVER_ERROR,\n\t\t\t\t\t`Server error: ${errorMessage}`,\n\t\t\t\t\terrorData\n\t\t\t\t)\n\t\t\t}\n\t\t\tthrow new InvokeError(\n\t\t\t\tInvokeErrorType.UNKNOWN,\n\t\t\t\t`HTTP ${response.status}: ${errorMessage}`,\n\t\t\t\terrorData\n\t\t\t)\n\t\t}\n\n\t\t// 4. Parse and validate response\n\t\tconst data = await response.json()\n\n\t\tconst choice = data.choices?.[0]\n\t\tif (!choice) {\n\t\t\tthrow new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', data)\n\t\t}\n\n\t\t// Check finish_reason\n\t\tswitch (choice.finish_reason) {\n\t\t\tcase 'tool_calls':\n\t\t\tcase 'function_call': // gemini\n\t\t\tcase 'stop': // some models use this even with tool calls\n\t\t\t\tbreak\n\t\t\tcase 'length':\n\t\t\t\tthrow new InvokeError(\n\t\t\t\t\tInvokeErrorType.CONTEXT_LENGTH,\n\t\t\t\t\t'Response truncated: max tokens reached',\n\t\t\t\t\tundefined,\n\t\t\t\t\tdata\n\t\t\t\t)\n\t\t\tcase 'content_filter':\n\t\t\t\tthrow new InvokeError(\n\t\t\t\t\tInvokeErrorType.CONTENT_FILTER,\n\t\t\t\t\t'Content filtered by safety system',\n\t\t\t\t\tundefined,\n\t\t\t\t\tdata\n\t\t\t\t)\n\t\t\tdefault:\n\t\t\t\tthrow new InvokeError(\n\t\t\t\t\tInvokeErrorType.UNKNOWN,\n\t\t\t\t\t`Unexpected finish_reason: ${choice.finish_reason}`,\n\t\t\t\t\tundefined,\n\t\t\t\t\tdata\n\t\t\t\t)\n\t\t}\n\n\t\t// Apply normalizeResponse if provided (for fixing format issues automatically)\n\t\tconst normalizedData = options?.normalizeResponse ? options.normalizeResponse(data) : data\n\t\tconst normalizedChoice = (normalizedData as any).choices?.[0]\n\n\t\t// Get tool name from response\n\t\tconst toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name\n\t\tif (!toolCallName) {\n\t\t\tthrow new InvokeError(\n\t\t\t\tInvokeErrorType.NO_TOOL_CALL,\n\t\t\t\t'No tool call found in response',\n\t\t\t\tundefined,\n\t\t\t\tdata\n\t\t\t)\n\t\t}\n\n\t\tconst tool = tools[toolCallName]\n\t\tif (!tool) {\n\t\t\tthrow new InvokeError(\n\t\t\t\tInvokeErrorType.UNKNOWN,\n\t\t\t\t`Tool \"${toolCallName}\" not found in tools`,\n\t\t\t\tundefined,\n\t\t\t\tdata\n\t\t\t)\n\t\t}\n\n\t\t// Extract and parse tool arguments\n\t\tconst argString = normalizedChoice.message?.tool_calls?.[0]?.function?.arguments\n\t\tif (!argString) {\n\t\t\tthrow new InvokeError(\n\t\t\t\tInvokeErrorType.INVALID_TOOL_ARGS,\n\t\t\t\t'No tool call arguments found',\n\t\t\t\tundefined,\n\t\t\t\tdata\n\t\t\t)\n\t\t}\n\n\t\tlet parsedArgs: unknown\n\t\ttry {\n\t\t\tparsedArgs = JSON.parse(argString)\n\t\t} catch (error) {\n\t\t\tthrow new InvokeError(\n\t\t\t\tInvokeErrorType.INVALID_TOOL_ARGS,\n\t\t\t\t'Failed to parse tool arguments as JSON',\n\t\t\t\terror,\n\t\t\t\tdata\n\t\t\t)\n\t\t}\n\n\t\t// Validate with schema\n\t\tconst validation = tool.inputSchema.safeParse(parsedArgs)\n\t\tif (!validation.success) {\n\t\t\tconsole.error(z.prettifyError(validation.error))\n\t\t\tthrow new InvokeError(\n\t\t\t\tInvokeErrorType.INVALID_TOOL_ARGS,\n\t\t\t\t'Tool arguments validation failed',\n\t\t\t\tvalidation.error,\n\t\t\t\tdata\n\t\t\t)\n\t\t}\n\t\tconst toolInput = validation.data\n\n\t\t// 5. Execute tool\n\t\tlet toolResult: unknown\n\t\ttry {\n\t\t\ttoolResult = await tool.execute(toolInput)\n\t\t} catch (e) {\n\t\t\tthrow new InvokeError(\n\t\t\t\tInvokeErrorType.TOOL_EXECUTION_ERROR,\n\t\t\t\t`Tool execution failed: ${(e as Error).message}`,\n\t\t\t\te,\n\t\t\t\tdata\n\t\t\t)\n\t\t}\n\n\t\t// Return result\n\t\treturn {\n\t\t\ttoolCall: {\n\t\t\t\tname: toolCallName,\n\t\t\t\targs: toolInput,\n\t\t\t},\n\t\t\ttoolResult,\n\t\t\tusage: {\n\t\t\t\tpromptTokens: data.usage?.prompt_tokens ?? 0,\n\t\t\t\tcompletionTokens: data.usage?.completion_tokens ?? 0,\n\t\t\t\ttotalTokens: data.usage?.total_tokens ?? 0,\n\t\t\t\tcachedTokens: data.usage?.prompt_tokens_details?.cached_tokens,\n\t\t\t\treasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens,\n\t\t\t},\n\t\t\trawResponse: data,\n\t\t\trawRequest: requestBody,\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "packages/llms/src/constants.ts",
    "content": "// Internal constants\nexport const LLM_MAX_RETRIES = 2\nexport const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery\n"
  },
  {
    "path": "packages/llms/src/errors.ts",
    "content": "/**\n * Error types and error handling for LLM invocations\n */\n\nexport const InvokeErrorType = {\n\t// Retryable\n\tNETWORK_ERROR: 'network_error', // Network error, retry\n\tRATE_LIMIT: 'rate_limit', // Rate limit, retry\n\tSERVER_ERROR: 'server_error', // 5xx, retry\n\tNO_TOOL_CALL: 'no_tool_call', // Model did not call tool\n\tINVALID_TOOL_ARGS: 'invalid_tool_args', // Tool args don't match schema\n\tTOOL_EXECUTION_ERROR: 'tool_execution_error', // Tool execution error\n\n\tUNKNOWN: 'unknown',\n\n\t// Non-retryable\n\tAUTH_ERROR: 'auth_error', // Authentication failed\n\tCONTEXT_LENGTH: 'context_length', // Prompt too long\n\tCONTENT_FILTER: 'content_filter', // Content filtered\n} as const\n\nexport type InvokeErrorType = (typeof InvokeErrorType)[keyof typeof InvokeErrorType]\n\nexport class InvokeError extends Error {\n\ttype: InvokeErrorType\n\tretryable: boolean\n\tstatusCode?: number\n\t/* raw error (provided if this error is caused by another error) */\n\trawError?: unknown\n\t/* raw response from the API (provided if this error is caused by an API calling) */\n\trawResponse?: unknown\n\n\tconstructor(type: InvokeErrorType, message: string, rawError?: unknown, rawResponse?: unknown) {\n\t\tsuper(message)\n\t\tthis.name = 'InvokeError'\n\t\tthis.type = type\n\t\tthis.retryable = this.isRetryable(type, rawError)\n\t\tthis.rawError = rawError\n\t\tthis.rawResponse = rawResponse\n\t}\n\n\tprivate isRetryable(type: InvokeErrorType, rawError?: unknown): boolean {\n\t\tconst isAbortError = (rawError as any)?.name === 'AbortError'\n\t\tif (isAbortError) return false\n\n\t\tconst retryableTypes: InvokeErrorType[] = [\n\t\t\tInvokeErrorType.NETWORK_ERROR,\n\t\t\tInvokeErrorType.RATE_LIMIT,\n\t\t\tInvokeErrorType.SERVER_ERROR,\n\t\t\tInvokeErrorType.NO_TOOL_CALL,\n\t\t\tInvokeErrorType.INVALID_TOOL_ARGS,\n\t\t\tInvokeErrorType.TOOL_EXECUTION_ERROR,\n\t\t\tInvokeErrorType.UNKNOWN,\n\t\t]\n\t\treturn retryableTypes.includes(type)\n\t}\n}\n"
  },
  {
    "path": "packages/llms/src/index.ts",
    "content": "import { OpenAIClient } from './OpenAIClient'\nimport { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'\nimport { InvokeError, InvokeErrorType } from './errors'\nimport type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'\n\nexport { InvokeError, InvokeErrorType }\nexport type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }\n\nexport function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {\n\t// Runtime validation as defensive programming (types already guarantee these)\n\tif (!config.baseURL || !config.model) {\n\t\tthrow new Error(\n\t\t\t'[PageAgent] LLM configuration required. Please provide: baseURL, model. ' +\n\t\t\t\t'See: https://alibaba.github.io/page-agent/docs/features/models'\n\t\t)\n\t}\n\n\treturn {\n\t\tbaseURL: config.baseURL,\n\t\tmodel: config.model,\n\t\tapiKey: config.apiKey || '',\n\t\ttemperature: config.temperature ?? DEFAULT_TEMPERATURE,\n\t\tmaxRetries: config.maxRetries ?? LLM_MAX_RETRIES,\n\t\tdisableNamedToolChoice: config.disableNamedToolChoice ?? false,\n\t\tcustomFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound\n\t}\n}\n\nexport class LLM extends EventTarget {\n\tconfig: Required<LLMConfig>\n\tclient: LLMClient\n\n\tconstructor(config: LLMConfig) {\n\t\tsuper()\n\t\tthis.config = parseLLMConfig(config)\n\n\t\t// Default to OpenAI client\n\t\tthis.client = new OpenAIClient(this.config)\n\t}\n\n\t/**\n\t * - call llm api *once*\n\t * - invoke tool call *once*\n\t * - return the result of the tool\n\t */\n\tasync invoke(\n\t\tmessages: Message[],\n\t\ttools: Record<string, Tool>,\n\t\tabortSignal: AbortSignal,\n\t\toptions?: InvokeOptions\n\t): Promise<InvokeResult> {\n\t\treturn await withRetry(\n\t\t\tasync () => {\n\t\t\t\t// in case user aborted before invoking\n\t\t\t\tif (abortSignal.aborted) throw new Error('AbortError')\n\n\t\t\t\tconst result = await this.client.invoke(messages, tools, abortSignal, options)\n\n\t\t\t\treturn result\n\t\t\t},\n\t\t\t// retry settings\n\t\t\t{\n\t\t\t\tmaxRetries: this.config.maxRetries,\n\t\t\t\tonRetry: (attempt: number) => {\n\t\t\t\t\tthis.dispatchEvent(\n\t\t\t\t\t\tnew CustomEvent('retry', { detail: { attempt, maxAttempts: this.config.maxRetries } })\n\t\t\t\t\t)\n\t\t\t\t},\n\t\t\t\tonError: (error: Error) => {\n\t\t\t\t\tthis.dispatchEvent(new CustomEvent('error', { detail: { error } }))\n\t\t\t\t},\n\t\t\t}\n\t\t)\n\t}\n}\n\nasync function withRetry<T>(\n\tfn: () => Promise<T>,\n\tsettings: {\n\t\tmaxRetries: number\n\t\tonRetry: (attempt: number) => void\n\t\tonError: (error: Error) => void\n\t}\n): Promise<T> {\n\tlet attempt = 0\n\tlet lastError: Error | null = null\n\twhile (attempt <= settings.maxRetries) {\n\t\tif (attempt > 0) {\n\t\t\tsettings.onRetry(attempt)\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 100))\n\t\t}\n\n\t\ttry {\n\t\t\treturn await fn()\n\t\t} catch (error: unknown) {\n\t\t\t// do not retry if aborted by user\n\t\t\tif ((error as any)?.rawError?.name === 'AbortError') throw error\n\n\t\t\tconsole.error(error)\n\t\t\tsettings.onError(error as Error)\n\n\t\t\t// do not retry if error is not retryable (InvokeError)\n\t\t\tif (error instanceof InvokeError && !error.retryable) throw error\n\n\t\t\tlastError = error as Error\n\t\t\tattempt++\n\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 100))\n\t\t}\n\t}\n\n\tthrow lastError!\n}\n"
  },
  {
    "path": "packages/llms/src/types.ts",
    "content": "/**\n * Core types for LLM integration\n */\nimport type * as z from 'zod/v4'\n\n/**\n * Message format - OpenAI standard (industry standard)\n */\nexport interface Message {\n\trole: 'system' | 'user' | 'assistant' | 'tool'\n\tcontent?: string | null\n\ttool_calls?: {\n\t\tid: string\n\t\ttype: 'function'\n\t\tfunction: {\n\t\t\tname: string\n\t\t\targuments: string // JSON string\n\t\t}\n\t}[]\n\ttool_call_id?: string\n\tname?: string\n}\n\n/**\n * Tool definition - uses Zod schema (LLM-agnostic)\n * Supports generics for type-safe parameters and return values\n */\nexport interface Tool<TParams = any, TResult = any> {\n\t// name: string\n\tdescription?: string\n\tinputSchema: z.ZodType<TParams>\n\texecute: (args: TParams) => Promise<TResult>\n}\n\n/**\n * Invoke options for LLM call\n */\nexport interface InvokeOptions {\n\t/**\n\t * Force LLM to call a specific tool by name.\n\t * If provided: tool_choice = { type: 'function', function: { name: toolChoiceName } }\n\t * If not provided: tool_choice = 'required' (must call some tool, but model chooses which)\n\t */\n\ttoolChoiceName?: string\n\t/**\n\t * Response normalization function.\n\t * Called before parsing the response.\n\t * Used to fix various response format errors from the model.\n\t */\n\tnormalizeResponse?: (response: any) => any\n}\n\n/**\n * LLM Client interface\n * Note: Does not use generics because each tool in the tools array has different types\n */\nexport interface LLMClient {\n\tinvoke(\n\t\tmessages: Message[],\n\t\ttools: Record<string, Tool>,\n\t\tabortSignal?: AbortSignal,\n\t\toptions?: InvokeOptions\n\t): Promise<InvokeResult>\n}\n\n/**\n * Invoke result (strict typing, supports generics)\n */\nexport interface InvokeResult<TResult = unknown> {\n\ttoolCall: {\n\t\t// id?: string // OpenAI's tool_call_id\n\t\tname: string\n\t\targs: any\n\t}\n\ttoolResult: TResult // Supports generics, but defaults to unknown\n\tusage: {\n\t\tpromptTokens: number\n\t\tcompletionTokens: number\n\t\ttotalTokens: number\n\t\tcachedTokens?: number // Prompt cache hits\n\t\treasoningTokens?: number // OpenAI o1 series reasoning tokens\n\t}\n\trawResponse?: unknown // Raw response for debugging\n\trawRequest?: unknown // Raw request for debugging\n}\n\n/**\n * LLM configuration\n */\nexport interface LLMConfig {\n\tbaseURL: string\n\tmodel: string\n\tapiKey?: string\n\n\ttemperature?: number\n\tmaxRetries?: number\n\n\t/**\n\t * remove the tool_choice field from the request.\n\t * @note fix \"Invalid tool_choice type: 'object'\" for some LLMs.\n\t */\n\tdisableNamedToolChoice?: boolean\n\n\t/**\n\t * Custom fetch function for LLM API requests.\n\t * Use this to customize headers, credentials, proxy, etc.\n\t * The response should follow OpenAI API format.\n\t */\n\tcustomFetch?: typeof globalThis.fetch\n}\n"
  },
  {
    "path": "packages/llms/src/utils.ts",
    "content": "/**\n * Utility functions for LLM integration\n */\nimport chalk from 'chalk'\nimport * as z from 'zod/v4'\n\nimport type { Tool } from './types'\n\nconst debug = console.debug.bind(console, chalk.gray('[LLM]'))\n\n/**\n * Convert Zod schema to OpenAI tool format\n * Uses Zod 4 native z.toJSONSchema()\n */\nexport function zodToOpenAITool(name: string, tool: Tool) {\n\treturn {\n\t\ttype: 'function' as const,\n\t\tfunction: {\n\t\t\tname,\n\t\t\tdescription: tool.description,\n\t\t\tparameters: z.toJSONSchema(tool.inputSchema, { target: 'openapi-3.0' }),\n\t\t},\n\t}\n}\n\n/**\n * Patch model specific parameters\n * @note in-place modification\n */\nexport function modelPatch(body: Record<string, any>) {\n\tconst model: string = body.model || ''\n\tif (!model) return body\n\n\tconst modelName = normalizeModelName(model)\n\n\tif (modelName.startsWith('qwen')) {\n\t\tdebug('Applying Qwen patch: use higher temperature for auto fixing')\n\t\tbody.temperature = Math.max(body.temperature || 0, 1.0)\n\t\tbody.enable_thinking = false\n\t}\n\n\tif (modelName.startsWith('claude')) {\n\t\tdebug('Applying Claude patch: disable thinking')\n\t\tbody.thinking = { type: 'disabled' }\n\n\t\t// Convert tool_choice to Claude format\n\t\tif (body.tool_choice === 'required') {\n\t\t\t// 'required' -> { type: 'any' } (must call some tool)\n\t\t\tdebug('Applying Claude patch: convert tool_choice \"required\" to { type: \"any\" }')\n\t\t\tbody.tool_choice = { type: 'any' }\n\t\t} else if (body.tool_choice?.function?.name) {\n\t\t\t// { type: 'function', function: { name: '...' } } -> { type: 'tool', name: '...' }\n\t\t\tdebug('Applying Claude patch: convert tool_choice format')\n\t\t\tbody.tool_choice = { type: 'tool', name: body.tool_choice.function.name }\n\t\t}\n\t}\n\n\tif (modelName.startsWith('grok')) {\n\t\tdebug('Applying Grok patch: removing tool_choice')\n\t\tdelete body.tool_choice\n\t\tdebug('Applying Grok patch: disable reasoning and thinking')\n\t\tbody.thinking = { type: 'disabled', effort: 'minimal' }\n\t\tbody.reasoning = { enabled: false, effort: 'low' }\n\t}\n\n\tif (modelName.startsWith('gpt')) {\n\t\tdebug('Applying GPT patch: set verbosity to low')\n\t\tbody.verbosity = 'low'\n\n\t\tif (modelName.startsWith('gpt-52')) {\n\t\t\tdebug('Applying GPT-52 patch: disable reasoning')\n\t\t\tbody.reasoning_effort = 'none'\n\t\t} else if (modelName.startsWith('gpt-51')) {\n\t\t\tdebug('Applying GPT-51 patch: disable reasoning')\n\t\t\tbody.reasoning_effort = 'none'\n\t\t} else if (modelName.startsWith('gpt-54')) {\n\t\t\tdebug(\n\t\t\t\t'Applying GPT-5.4 patch: skip reasoning_effort because chat/completions rejects it with function tools'\n\t\t\t)\n\t\t\tdelete body.reasoning_effort\n\t\t} else if (modelName.startsWith('gpt-5-mini')) {\n\t\t\tdebug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1')\n\t\t\tbody.reasoning_effort = 'low'\n\t\t\tbody.temperature = 1\n\t\t} else if (modelName.startsWith('gpt-5')) {\n\t\t\tdebug('Applying GPT-5 patch: set reasoning effort to low')\n\t\t\tbody.reasoning_effort = 'low'\n\t\t}\n\t}\n\n\tif (modelName.startsWith('gemini')) {\n\t\tdebug('Applying Gemini patch: set reasoning effort to minimal')\n\t\tbody.reasoning_effort = 'minimal'\n\t}\n\n\tif (modelName.startsWith('minimax')) {\n\t\tdebug('Applying MiniMax patch: clamp temperature to (0, 1]')\n\t\t// MiniMax API rejects temperature = 0; clamp to a small positive value\n\t\tbody.temperature = Math.max(body.temperature || 0, 0.01)\n\t\tif (body.temperature > 1) body.temperature = 1\n\t\t// MiniMax does not support parallel_tool_calls\n\t\tdelete body.parallel_tool_calls\n\t}\n\n\treturn body\n}\n\n/**\n * check if a given model ID fits a specific model name\n *\n * @note\n * Different model providers may use different model IDs for the same model.\n * For example, openai's `gpt-5.2` may called:\n *\n * - `gpt-5.2-version`\n * - `gpt-5_2-date`\n * - `GPT-52-version-date`\n * - `openai/gpt-5.2-chat`\n *\n * They should be treated as the same model.\n * Normalize them to `gpt-52`\n */\nfunction normalizeModelName(modelName: string): string {\n\tlet normalizedName = modelName.toLowerCase()\n\n\t// remove prefix before '/'\n\tif (normalizedName.includes('/')) {\n\t\tnormalizedName = normalizedName.split('/')[1]\n\t}\n\n\t// remove '_'\n\tnormalizedName = normalizedName.replace(/_/g, '')\n\n\t// remove '.'\n\tnormalizedName = normalizedName.replace(/\\./g, '')\n\n\treturn normalizedName\n}\n"
  },
  {
    "path": "packages/llms/tsconfig.dts.json",
    "content": "{\n    \"extends\": \"./tsconfig.json\",\n    \"compilerOptions\": {\n        // @workaround DTS bug\n        // dts do not work with monorepo path mapping\n        // disable path mapping for it\n        \"paths\": {}\n    }\n}\n"
  },
  {
    "path": "packages/llms/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.base.json\",\n    \"compilerOptions\": {\n        \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.tsbuildinfo\",\n        \"noEmit\": false,\n        \"allowImportingTsExtensions\": false,\n        \"baseUrl\": \".\",\n        \"outDir\": \"dist\"\n    },\n    \"include\": [\"**/*.ts\"],\n    \"exclude\": [\"dist\", \"node_modules\"]\n}\n"
  },
  {
    "path": "packages/llms/vite.config.js",
    "content": "// @ts-check\nimport chalk from 'chalk'\nimport { dirname, resolve } from 'path'\nimport dts from 'unplugin-dts/vite'\nimport { fileURLToPath } from 'url'\nimport { defineConfig } from 'vite'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nconsole.log(chalk.cyan(`📦 Building @page-agent/llms`))\n\nexport default defineConfig({\n\tclearScreen: false,\n\tplugins: [dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true })],\n\tpublicDir: false,\n\tesbuild: {\n\t\tkeepNames: true,\n\t},\n\tbuild: {\n\t\tlib: {\n\t\t\tentry: resolve(__dirname, 'src/index.ts'),\n\t\t\tname: 'PageAgentLLMs',\n\t\t\tfileName: 'page-agent-llms',\n\t\t\tformats: ['es'],\n\t\t},\n\t\toutDir: resolve(__dirname, 'dist', 'lib'),\n\t\trollupOptions: {\n\t\t\texternal: ['chalk', 'zod', 'zod/v4'],\n\t\t},\n\t\tminify: false,\n\t\tsourcemap: true,\n\t},\n\tdefine: {\n\t\t'process.env.NODE_ENV': '\"production\"',\n\t},\n})\n"
  },
  {
    "path": "packages/mcp/README.md",
    "content": "# @page-agent/mcp\n\nMCP server that lets AI agent clients (Claude Desktop, Copilot, etc.) control your browser through the [Page Agent](https://github.com/alibaba/page-agent) extension.\n\n## Prerequisites\n\n- Node.js >= 20\n- [Page Agent Extension](https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj) installed in Chrome\n- An LLM API key (OpenAI-compatible)\n\n## Installation\n\n### Claude Desktop\n\nAdd to `~/Library/Application Support/Claude/claude_desktop_config.json`:\n\n```json\n{\n    \"mcpServers\": {\n        \"page-agent\": {\n            \"command\": \"npx\",\n            \"args\": [\"-y\", \"@page-agent/mcp\"],\n            \"env\": {\n                \"LLM_BASE_URL\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n                \"LLM_API_KEY\": \"sk-xxx\",\n                \"LLM_MODEL_NAME\": \"qwen3.5-plus\"\n            }\n        }\n    }\n}\n```\n\n### Cursor / Copilot\n\nSame format — add the config to the MCP settings of your client.\n\n## MCP Tools\n\n| Tool           | Input              | Description                                          |\n| -------------- | ------------------ | ---------------------------------------------------- |\n| `execute_task` | `{ task: string }` | Execute a browser task in natural language. Blocking. |\n| `get_status`   | —                  | Returns `{ connected, busy }`                        |\n| `stop_task`    | —                  | Stop the currently running task.                     |\n\n## Environment Variables\n\n| Variable         | Default | Description           |\n| ---------------- | ------- | --------------------- |\n| `LLM_BASE_URL`   | —       | LLM API base URL      |\n| `LLM_API_KEY`    | —       | LLM API key           |\n| `LLM_MODEL_NAME` | —       | Model name            |\n| `PORT`           | `38401` | HTTP + WebSocket port |\n\n## How It Works\n\n```\n┌──────────────┐  stdio   ┌──────────────────┐  WebSocket   ┌──────────────┐\n│ Claude /     │◄────────►│ @page-agent/mcp  │◄────────────►│ Hub tab      │\n│ Copilot      │  (MCP)   │ (Node.js)        │  (localhost) │ (extension)  │\n└──────────────┘          └──────────────────┘              └──────┬───────┘\n                                   │                               │\n                                   │ HTTP                          │ useAgent\n                                   ▼                               ▼\n                          ┌──────────────────┐              ┌──────────────┐\n                          │ Launcher page    │              │ MultiPage    │\n                          │ (localhost:PORT) │              │ Agent        │\n                          └──────────────────┘              └──────────────┘\n```\n\n1. Agent client starts the MCP server via stdio (`npx @page-agent/mcp`).\n2. Server starts HTTP + WS on `localhost:PORT`, opens the launcher page in browser.\n3. Launcher page triggers the extension to open a **hub tab** (`hub.html?ws=PORT`).\n4. Hub connects to the WS server. MCP tools now proxy tasks to the hub.\n\nThe hub tab speaks a generic WebSocket protocol (defined in `hub-ws.ts` in the extension package) and has no knowledge of MCP. See the hub's protocol docs for message format details.\n\n## Architecture\n\nPure JS ESM, no build step. Source files are the published artifacts.\n\n```\nsrc/\n├── index.js        # CLI entry: MCP server (stdio) + opens launcher\n├── hub-bridge.js   # HTTP server + WebSocket bridge to hub tab\n└── launcher.html   # Bootstrap page: detects extension, triggers hub open\n```\n\n## Dev\n\n```bash\nnpm run build:libs\nnpm run dev:ext\nnpx @modelcontextprotocol/inspector node packages/mcp/src/index.js\n```\n"
  },
  {
    "path": "packages/mcp/package.json",
    "content": "{\n    \"name\": \"@page-agent/mcp\",\n    \"private\": false,\n    \"version\": \"1.6.0\",\n    \"type\": \"module\",\n    \"bin\": {\n        \"page-agent-mcp\": \"src/index.js\"\n    },\n    \"files\": [\n        \"src/\"\n    ],\n    \"description\": \"MCP server for controlling the browser via Page Agent extension\",\n    \"keywords\": [\n        \"page-agent\",\n        \"mcp\",\n        \"browser-automation\",\n        \"chrome-extension\"\n    ],\n    \"author\": \"Simon<gaomeng1900>\",\n    \"license\": \"MIT\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/alibaba/page-agent.git\",\n        \"directory\": \"packages/mcp\"\n    },\n    \"homepage\": \"https://alibaba.github.io/page-agent/\",\n    \"engines\": {\n        \"node\": \">=20\"\n    },\n    \"dependencies\": {\n        \"@modelcontextprotocol/sdk\": \"^1.27.1\",\n        \"ws\": \"^8.19.0\",\n        \"zod\": \"^4.3.5\"\n    }\n}\n"
  },
  {
    "path": "packages/mcp/src/hub-bridge.js",
    "content": "#!/usr/bin/env node\nimport { readFileSync } from 'node:fs'\nimport http from 'node:http'\nimport { fileURLToPath } from 'node:url'\nimport { WebSocketServer } from 'ws'\n\nconst EXT_ID = 'akldabonmimlicnjlflnapfeklbfemhj'\nconst STORE_URL = `https://chromewebstore.google.com/detail/page-agent-ext/${EXT_ID}`\n\nconst launcherTemplate = readFileSync(\n\tfileURLToPath(new URL('./launcher.html', import.meta.url)),\n\t'utf-8'\n)\n\n/**\n * HTTP + WebSocket bridge to the hub.html extension tab.\n * - HTTP serves the launcher page (triggers extension to open hub)\n * - WS carries execute/stop commands and result/error responses\n */\nexport class HubBridge {\n\t/** @type {number} */\n\tport\n\n\t/** @type {http.Server} */\n\t#httpServer\n\n\t/** @type {WebSocketServer} */\n\t#wss\n\n\t/** @type {import('ws').WebSocket | null} */\n\t#hub = null\n\n\t/** @type {{ resolve: (r: {success: boolean, data: string}) => void, reject: (e: Error) => void } | null} */\n\t#pendingTask = null\n\n\t/** @param {number} port */\n\tconstructor(port) {\n\t\tthis.port = port\n\t\tthis.#httpServer = http.createServer((_req, res) => {\n\t\t\tconst html = launcherTemplate\n\t\t\t\t.replaceAll('__EXT_ID__', EXT_ID)\n\t\t\t\t.replaceAll('__STORE_URL__', STORE_URL)\n\t\t\t\t.replaceAll('__WS_PORT__', String(port))\n\t\t\tres.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })\n\t\t\tres.end(html)\n\t\t})\n\t\tthis.#wss = new WebSocketServer({ server: this.#httpServer })\n\t\tthis.#wss.on('connection', (ws) => this.#onConnection(ws))\n\t}\n\n\t/** @returns {Promise<void>} */\n\tasync start() {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.#httpServer.on('error', (/** @type {NodeJS.ErrnoException} */ err) => {\n\t\t\t\tif (err.code === 'EADDRINUSE') {\n\t\t\t\t\treject(\n\t\t\t\t\t\tnew Error(`Port ${this.port} is in use. Another Page Agent MCP server may be running.`)\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\treject(err)\n\t\t\t\t}\n\t\t\t})\n\t\t\tthis.#httpServer.listen(this.port, () => {\n\t\t\t\tconsole.error(`[page-agent-mcp] HTTP + WS on http://localhost:${this.port}`)\n\t\t\t\tresolve()\n\t\t\t})\n\t\t})\n\t}\n\n\tget connected() {\n\t\treturn this.#hub?.readyState === 1\n\t}\n\n\tget busy() {\n\t\treturn this.#pendingTask !== null\n\t}\n\n\t/**\n\t * @param {string} task\n\t * @param {Record<string, unknown>} [config]\n\t * @returns {Promise<{success: boolean, data: string}>}\n\t */\n\tasync executeTask(task, config) {\n\t\tif (!this.connected) throw new Error('Hub is not connected. Is the extension running?')\n\t\tif (this.#pendingTask) throw new Error('Agent is already running a task.')\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.#pendingTask = { resolve, reject }\n\t\t\tthis.#hub.send(JSON.stringify({ type: 'execute', task, config }))\n\t\t})\n\t}\n\n\tstopTask() {\n\t\tif (this.connected) {\n\t\t\tthis.#hub.send(JSON.stringify({ type: 'stop' }))\n\t\t}\n\t}\n\n\t// TODO: Add version checking\n\n\t/** @param {import('ws').WebSocket} ws */\n\t#onConnection(ws) {\n\t\tif (this.#hub && this.#hub.readyState === 1) {\n\t\t\tws.close(4000, 'Another hub is already connected')\n\t\t\treturn\n\t\t}\n\n\t\tthis.#hub = ws\n\t\tconsole.error('[page-agent-mcp] Hub connected')\n\n\t\tws.on('message', (/** @type {Buffer} */ rawData) => {\n\t\t\t/** @type {{ type: string, success?: boolean, data?: string, message?: string }} */\n\t\t\tlet msg\n\t\t\ttry {\n\t\t\t\tmsg = JSON.parse(rawData.toString('utf-8'))\n\t\t\t} catch {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (msg.type === 'result') {\n\t\t\t\tthis.#pendingTask?.resolve({ success: msg.success ?? false, data: msg.data ?? '' })\n\t\t\t\tthis.#pendingTask = null\n\t\t\t} else if (msg.type === 'error') {\n\t\t\t\tthis.#pendingTask?.reject(new Error(msg.message ?? 'Unknown error from hub'))\n\t\t\t\tthis.#pendingTask = null\n\t\t\t}\n\t\t})\n\n\t\tws.on('close', () => {\n\t\t\tconsole.error('[page-agent-mcp] Hub disconnected')\n\t\t\tif (this.#hub === ws) this.#hub = null\n\t\t\tif (this.#pendingTask) {\n\t\t\t\tthis.#pendingTask.reject(new Error('Hub disconnected while task was running'))\n\t\t\t\tthis.#pendingTask = null\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "packages/mcp/src/index.js",
    "content": "#!/usr/bin/env node\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { exec } from 'node:child_process'\nimport { platform } from 'node:os'\nimport * as z from 'zod/v4'\n\nimport { HubBridge } from './hub-bridge.js'\n\nconst env = process.env\nconst port = parseInt(env.PORT || '38401')\n\n/** @type {Record<string, string>} */\nconst llmConfig = {}\nif (env.LLM_BASE_URL) llmConfig.baseURL = env.LLM_BASE_URL\nif (env.LLM_MODEL_NAME) llmConfig.model = env.LLM_MODEL_NAME\nif (env.LLM_API_KEY) llmConfig.apiKey = env.LLM_API_KEY\n\n// --- Hub bridge (HTTP + WebSocket) ---\n\nconst hub = new HubBridge(port)\nawait hub.start()\n\n// Open launcher in default browser\nconst url = `http://localhost:${port}`\nconst cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start \"\"' : 'xdg-open'\nexec(`${cmd} \"${url}\"`, (err) => {\n\tif (err) console.error(`[page-agent-mcp] Could not open browser: ${err.message}`)\n})\n\n// --- MCP server (stdio) ---\n\nconst mcpServer = new McpServer({ name: 'page-agent', version: '1.5.8' })\n\nmcpServer.registerTool(\n\t'execute_task',\n\t{\n\t\tdescription:\n\t\t\t'Execute a browser automation task described in natural language. ' +\n\t\t\t'The Page Agent extension will control the browser to complete the task. ' +\n\t\t\t'Blocks until the task is complete.',\n\t\tinputSchema: { task: z.string().describe('Task description in natural language') },\n\t},\n\tasync ({ task }) => {\n\t\ttry {\n\t\t\tconst config = Object.keys(llmConfig).length > 0 ? llmConfig : undefined\n\t\t\tconst result = await hub.executeTask(task, config)\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: 'text',\n\t\t\t\t\t\ttext: result.success\n\t\t\t\t\t\t\t? `Task completed successfully.\\n\\n${result.data}`\n\t\t\t\t\t\t\t: `Task failed.\\n\\n${result.data}`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}\n\t\t} catch (err) {\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: 'text', text: `Error: ${err.message}` }],\n\t\t\t\tisError: true,\n\t\t\t}\n\t\t}\n\t}\n)\n\nmcpServer.registerTool(\n\t'get_status',\n\t{\n\t\tdescription: 'Check the current status of the Page Agent hub connection and agent.',\n\t},\n\tasync () => ({\n\t\tcontent: [\n\t\t\t{\n\t\t\t\ttype: 'text',\n\t\t\t\ttext: JSON.stringify({ connected: hub.connected, busy: hub.busy }, null, 2),\n\t\t\t},\n\t\t],\n\t})\n)\n\nmcpServer.registerTool(\n\t'stop_task',\n\t{\n\t\tdescription: 'Stop the currently running browser automation task.',\n\t},\n\tasync () => {\n\t\thub.stopTask()\n\t\treturn { content: [{ type: 'text', text: 'Stop signal sent.' }] }\n\t}\n)\n\nconst transport = new StdioServerTransport()\nawait mcpServer.connect(transport)\nconsole.error('[page-agent-mcp] MCP server ready (stdio)')\n"
  },
  {
    "path": "packages/mcp/src/launcher.html",
    "content": "<!doctype html>\n<html>\n\t<head>\n\t\t<meta charset=\"UTF-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\t\t<link rel=\"icon\" href=\"https://img.alicdn.com/imgextra/i1/O1CN01mRGret1QrKiu7CFJI_!!6000000002029-2-tps-64-64.png\" />\n\t\t<title>Page Agent MCP Launcher</title>\n\t\t<style>\n\t\t\t* {\n\t\t\t\tmargin: 0;\n\t\t\t\tpadding: 0;\n\t\t\t\tbox-sizing: border-box;\n\t\t\t}\n\t\t\tbody {\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tsans-serif;\n\t\t\t\tbackground: #09090b;\n\t\t\t\tcolor: #e5e5e5;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\tmin-height: 100vh;\n\t\t\t}\n\t\t\t.card {\n\t\t\t\ttext-align: center;\n\t\t\t\tmax-width: 480px;\n\t\t\t\tpadding: 3rem 2rem;\n\t\t\t}\n\t\t\t.logo {\n\t\t\t\twidth: 72px;\n\t\t\t\theight: 72px;\n\t\t\t\tborder-radius: 18px;\n\t\t\t\tmargin-bottom: 1rem;\n\t\t\t}\n\t\t\t.badge {\n\t\t\t\tfont-size: 0.6875rem;\n\t\t\t\tfont-weight: 500;\n\t\t\t\tcolor: #52525b;\n\t\t\t\tletter-spacing: 0.05em;\n\t\t\t\ttext-transform: uppercase;\n\t\t\t\tmargin-bottom: 1.5rem;\n\t\t\t}\n\t\t\th1 {\n\t\t\t\tfont-size: 1.35rem;\n\t\t\t\tfont-weight: 600;\n\t\t\t\tmargin-bottom: 0.5rem;\n\t\t\t}\n\t\t\t.sub {\n\t\t\t\tfont-size: 0.875rem;\n\t\t\t\tcolor: #a1a1aa;\n\t\t\t\tline-height: 1.7;\n\t\t\t}\n\t\t\t.spinner {\n\t\t\t\twidth: 32px;\n\t\t\t\theight: 32px;\n\t\t\t\tborder: 3px solid #27272a;\n\t\t\t\tborder-top-color: #fff;\n\t\t\t\tborder-radius: 50%;\n\t\t\t\tanimation: spin 0.8s linear infinite;\n\t\t\t\tmargin: 0 auto 1.5rem;\n\t\t\t}\n\t\t\t@keyframes spin {\n\t\t\t\tto {\n\t\t\t\t\ttransform: rotate(360deg);\n\t\t\t\t}\n\t\t\t}\n\t\t\ta {\n\t\t\t\tcolor: #60a5fa;\n\t\t\t\ttext-decoration: none;\n\t\t\t}\n\t\t\ta:hover {\n\t\t\t\ttext-decoration: underline;\n\t\t\t}\n\n\t\t\t.install {\n\t\t\t\tdisplay: none;\n\t\t\t}\n\t\t\t.install.show {\n\t\t\t\tdisplay: block;\n\t\t\t}\n\n\t\t\t.tips {\n\t\t\t\tmargin-top: 1.75rem;\n\t\t\t\ttext-align: left;\n\t\t\t\tbackground: #18181b;\n\t\t\t\tborder: 1px solid #27272a;\n\t\t\t\tborder-radius: 12px;\n\t\t\t\tpadding: 1.25rem 1.5rem;\n\t\t\t}\n\t\t\t.tips li {\n\t\t\t\tfont-size: 0.8125rem;\n\t\t\t\tcolor: #a1a1aa;\n\t\t\t\tline-height: 1.7;\n\t\t\t\tmargin-left: 1rem;\n\t\t\t}\n\t\t\t.tips li + li {\n\t\t\t\tmargin-top: 0.35rem;\n\t\t\t}\n\n\t\t\t.store-btn {\n\t\t\t\tdisplay: inline-flex;\n\t\t\t\talign-items: center;\n\t\t\t\tgap: 0.625rem;\n\t\t\t\tmargin-top: 1.5rem;\n\t\t\t\tpadding: 0.625rem 1.5rem;\n\t\t\t\tbackground: #2563eb;\n\t\t\t\tcolor: #fff;\n\t\t\t\tborder-radius: 10px;\n\t\t\t\tfont-size: 0.875rem;\n\t\t\t\tfont-weight: 500;\n\t\t\t\ttransition: background 0.15s;\n\t\t\t}\n\t\t\t.store-btn:hover {\n\t\t\t\tbackground: #1d4ed8;\n\t\t\t\ttext-decoration: none;\n\t\t\t}\n\t\t\t.store-btn img {\n\t\t\t\twidth: 20px;\n\t\t\t\theight: 20px;\n\t\t\t}\n\n\t\t\t.links {\n\t\t\t\tmargin-top: 1.75rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\tjustify-content: center;\n\t\t\t\tgap: 1.5rem;\n\t\t\t\tfont-size: 0.8125rem;\n\t\t\t}\n\t\t\t.links a {\n\t\t\t\tcolor: #71717a;\n\t\t\t\ttransition: color 0.15s;\n\t\t\t}\n\t\t\t.links a:hover {\n\t\t\t\tcolor: #a1a1aa;\n\t\t\t\ttext-decoration: none;\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"card\">\n\t\t\t<img\n\t\t\t\tclass=\"logo\"\n\t\t\t\tsrc=\"https://img.alicdn.com/imgextra/i3/O1CN01JPT4Fj1FJTfmHfNxO_!!6000000000466-49-tps-512-512.webp\"\n\t\t\t\talt=\"Page Agent\"\n\t\t\t/>\n\t\t\t<div class=\"badge\">Page Agent MCP Launcher</div>\n\n\t\t\t<div id=\"connecting\">\n\t\t\t\t<div class=\"spinner\"></div>\n\t\t\t\t<h1 data-i18n=\"connecting_title\">Connecting to Page Agent</h1>\n\t\t\t\t<p class=\"sub\" data-i18n=\"connecting_sub\">Opening the hub in your browser…</p>\n\t\t\t</div>\n\n\t\t\t<div id=\"install\" class=\"install\">\n\t\t\t\t<h1 data-i18n=\"install_title\">Extension Required</h1>\n\t\t\t\t<p class=\"sub\" data-i18n=\"install_sub\">\n\t\t\t\t\tPage Agent requires the latest browser extension to work.\n\t\t\t\t</p>\n\n\t\t\t\t<a class=\"store-btn\" href=\"__STORE_URL__\" target=\"_blank\">\n\t\t\t\t\t<img\n\t\t\t\t\t\tsrc=\"https://img.alicdn.com/imgextra/i3/O1CN01JpW0Vo1sR3FpiZKFM_!!6000000005762-55-tps-192-192.svg\"\n\t\t\t\t\t\talt=\"\"\n\t\t\t\t\t/>\n\t\t\t\t\t<span data-i18n=\"install_btn\">Install from Chrome Web Store</span>\n\t\t\t\t</a>\n\n\t\t\t\t<ul class=\"tips\">\n\t\t\t\t\t<li data-i18n=\"tip_outdated\">\n\t\t\t\t\t\tIf the extension is outdated, please update it to the latest version.\n\t\t\t\t\t</li>\n\t\t\t\t\t<li data-i18n=\"tip_other_browser\">\n\t\t\t\t\t\tIf the extension is not installed in this browser, open this page from the\n\t\t\t\t\t\tbrowser that has it installed.\n\t\t\t\t\t</li>\n\t\t\t\t\t<li data-i18n=\"tip_refresh\">Refresh this page after installing or updating.</li>\n\t\t\t\t</ul>\n\t\t\t</div>\n\n\t\t\t<div class=\"links\">\n\t\t\t\t<a href=\"https://alibaba.github.io/page-agent/docs/introduction/overview\" target=\"_blank\" data-i18n=\"link_docs\">Docs</a>\n\t\t\t\t<a href=\"https://github.com/alibaba/page-agent/issues\" target=\"_blank\" data-i18n=\"link_issues\">Report an Issue</a>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<script>\n\t\t\t{\n\t\t\t\tconst EXT_ID = '__EXT_ID__'\n\t\t\t\tconst wsPort = __WS_PORT__\n\n\t\t\t\tconst zh = {\n\t\t\t\t\tconnecting_title: '正在连接 Page Agent',\n\t\t\t\t\tconnecting_sub: '正在浏览器中打开 Hub…',\n\t\t\t\t\tinstall_title: '需要安装浏览器插件',\n\t\t\t\t\tinstall_sub: 'Page Agent 需要安装最新版浏览器插件才能运行。',\n\t\t\t\t\tinstall_btn: '从 Chrome 应用商店安装',\n\t\t\t\t\ttip_outdated: '如果插件版本过旧，请更新到最新版本。',\n\t\t\t\t\ttip_other_browser:\n\t\t\t\t\t\t'如果该浏览器中未安装插件，请从装有插件的浏览器打开此页面。',\n\t\t\t\t\ttip_refresh: '安装或更新后，请刷新此页面。',\n\t\t\t\t\tlink_docs: '文档',\n\t\t\t\t\tlink_issues: '问题反馈',\n\t\t\t\t}\n\n\t\t\t\tif (/^zh\\b/i.test(navigator.language)) {\n\t\t\t\t\tdocument.querySelectorAll('[data-i18n]').forEach((el) => {\n\t\t\t\t\t\tconst key = el.getAttribute('data-i18n')\n\t\t\t\t\t\tif (zh[key]) el.textContent = zh[key]\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tconst showInstall = () => {\n\t\t\t\t\tdocument.getElementById('connecting').style.display = 'none'\n\t\t\t\t\tdocument.getElementById('install').classList.add('show')\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tif (!globalThis.chrome?.runtime?.sendMessage) {\n\t\t\t\t\t\tshowInstall()\n\t\t\t\t\t} else {\n\t\t\t\t\t\tchrome.runtime.sendMessage(\n\t\t\t\t\t\t\tEXT_ID,\n\t\t\t\t\t\t\t{ type: 'OPEN_HUB', wsPort },\n\t\t\t\t\t\t\t(response) => {\n\t\t\t\t\t\t\t\tif (chrome.runtime.lastError || !response?.ok) showInstall()\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\tshowInstall()\n\t\t\t\t}\n\t\t\t}\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "packages/page-agent/package.json",
    "content": "{\n    \"name\": \"page-agent\",\n    \"private\": false,\n    \"version\": \"1.6.0\",\n    \"type\": \"module\",\n    \"main\": \"./dist/esm/page-agent.js\",\n    \"module\": \"./dist/esm/page-agent.js\",\n    \"types\": \"./dist/esm/PageAgent.d.ts\",\n    \"exports\": {\n        \".\": {\n            \"types\": \"./dist/esm/PageAgent.d.ts\",\n            \"import\": \"./dist/esm/page-agent.js\",\n            \"default\": \"./dist/esm/page-agent.js\"\n        }\n    },\n    \"files\": [\n        \"dist/\"\n    ],\n    \"description\": \"GUI agent for web applications - add intelligent automation to any webpage with a single script\",\n    \"keywords\": [\n        \"ai\",\n        \"automation\",\n        \"ui-agent\",\n        \"GUI-agent\",\n        \"browser-automation\",\n        \"web-agent\",\n        \"llm\",\n        \"dom-interaction\",\n        \"web-automation\",\n        \"GUI-simulation\"\n    ],\n    \"author\": \"Simon<gaomeng1900>\",\n    \"license\": \"MIT\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/alibaba/page-agent.git\"\n    },\n    \"homepage\": \"https://alibaba.github.io/page-agent/\",\n    \"scripts\": {\n        \"build\": \"vite build && npm run build:demo\",\n        \"build:demo\": \"vite build --config vite.iife.config.js\",\n        \"dev:demo\": \"concurrently \\\"vite build --config vite.iife.config.js --watch\\\" \\\"npx serve dist/iife -p 5174\\\"\",\n        \"prepublishOnly\": \"node -e \\\"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\\\"\",\n        \"postpublish\": \"node -e \\\"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\\\"\"\n    },\n    \"dependencies\": {\n        \"@page-agent/core\": \"1.6.0\",\n        \"@page-agent/llms\": \"1.6.0\",\n        \"@page-agent/page-controller\": \"1.6.0\",\n        \"@page-agent/ui\": \"1.6.0\",\n        \"chalk\": \"^5.6.2\"\n    },\n    \"peerDependencies\": {\n        \"zod\": \"^3.25.0 || ^4.0.0\"\n    },\n    \"devDependencies\": {\n        \"zod\": \"^4.3.5\"\n    }\n}\n"
  },
  {
    "path": "packages/page-agent/src/PageAgent.ts",
    "content": "/**\n * Copyright (C) 2025 Alibaba Group Holding Limited\n * All rights reserved.\n */\nimport { type AgentConfig, PageAgentCore } from '@page-agent/core'\nimport { PageController, type PageControllerConfig } from '@page-agent/page-controller'\nimport { Panel } from '@page-agent/ui'\n\nexport * from '@page-agent/core'\n\nexport type PageAgentConfig = AgentConfig & PageControllerConfig\n\nexport class PageAgent extends PageAgentCore {\n\tpanel: Panel\n\n\tconstructor(config: PageAgentConfig) {\n\t\tconst pageController = new PageController({\n\t\t\t...config,\n\t\t\tenableMask: config.enableMask ?? true,\n\t\t})\n\n\t\tsuper({ ...config, pageController })\n\n\t\tthis.panel = new Panel(this, {\n\t\t\tlanguage: config.language,\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "packages/page-agent/src/demo.ts",
    "content": "/**\n * IIFE demo entry - auto-initializes with built-in demo API for testing\n */\nimport { PageAgent, type PageAgentConfig } from './PageAgent'\n\n// Clean up existing instances to prevent multiple injections from bookmarklet\nif (window.pageAgent) {\n\twindow.pageAgent.dispose()\n}\n\n// Mount to global window object\nwindow.PageAgent = PageAgent\n\nconsole.log('🚀 page-agent.js loaded!')\n\nconst DEMO_MODEL = 'qwen3.5-plus'\nconst DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'\nconst DEMO_API_KEY = 'NA'\n\n// in case document.x is not ready yet\nsetTimeout(() => {\n\tconst currentScript = document.currentScript as HTMLScriptElement | null\n\tlet config: PageAgentConfig\n\n\tif (currentScript) {\n\t\tconsole.log('🚀 page-agent.js detected current script:', currentScript.src)\n\t\tconst url = new URL(currentScript.src)\n\t\tconst model = url.searchParams.get('model') || DEMO_MODEL\n\t\tconst baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL\n\t\tconst apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY\n\t\tconst language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'\n\t\tconfig = { model, baseURL, apiKey, language }\n\t} else {\n\t\tconsole.log('🚀 page-agent.js no current script detected, using default demo config')\n\t\tconfig = {\n\t\t\tmodel: import.meta.env.LLM_MODEL_NAME ? import.meta.env.LLM_MODEL_NAME : DEMO_MODEL,\n\t\t\tbaseURL: import.meta.env.LLM_BASE_URL ? import.meta.env.LLM_BASE_URL : DEMO_BASE_URL,\n\t\t\tapiKey: import.meta.env.LLM_API_KEY ? import.meta.env.LLM_API_KEY : DEMO_API_KEY,\n\t\t}\n\t}\n\n\t// Create agent\n\twindow.pageAgent = new PageAgent(config)\n\twindow.pageAgent.panel.show()\n\n\tconsole.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)\n})\n"
  },
  {
    "path": "packages/page-agent/src/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\nimport type { PageAgent } from './PageAgent'\n\ndeclare global {\n\tinterface Window {\n\t\tpageAgent?: PageAgent\n\t\tPageAgent: typeof PageAgent\n\t}\n}\n"
  },
  {
    "path": "packages/page-agent/tsconfig.dts.json",
    "content": "{\n    \"extends\": \"./tsconfig.json\",\n    \"compilerOptions\": {\n        // @workaround DTS bug\n        // dts do not work with monorepo path mapping\n        // disable path mapping for it\n        \"paths\": {}\n    }\n}\n"
  },
  {
    "path": "packages/page-agent/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.base.json\",\n    \"compilerOptions\": {\n        \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.tsbuildinfo\",\n        \"noEmit\": false,\n        \"allowImportingTsExtensions\": false,\n        \"baseUrl\": \".\",\n        \"outDir\": \"dist\",\n        \"paths\": {\n            //\n            \"@page-agent/llms\": [\"../llms/src/index.ts\"],\n            \"@page-agent/page-controller\": [\"../page-controller/src/PageController.ts\"],\n            \"@page-agent/core\": [\"../core/src/PageAgentCore.ts\"],\n            \"@page-agent/ui\": [\"../ui/src/index.ts\"]\n        }\n    },\n    \"include\": [\"**/*.ts\"],\n    \"exclude\": [\"dist\", \"node_modules\"],\n    \"references\": [\n        //\n        { \"path\": \"../llms\" },\n        { \"path\": \"../page-controller\" },\n        { \"path\": \"../core\" },\n        { \"path\": \"../ui\" }\n    ]\n}\n"
  },
  {
    "path": "packages/page-agent/vite.config.js",
    "content": "// @ts-check\nimport { dirname, resolve } from 'path'\nimport dts from 'unplugin-dts/vite'\nimport { fileURLToPath } from 'url'\nimport { defineConfig } from 'vite'\nimport cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n// ES Module for NPM Package\nexport default defineConfig({\n\tclearScreen: false,\n\tplugins: [\n\t\tdts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),\n\t\tcssInjectedByJsPlugin({ relativeCSSInjection: true }),\n\t],\n\tpublicDir: false,\n\tesbuild: {\n\t\tkeepNames: true,\n\t},\n\tbuild: {\n\t\tlib: {\n\t\t\tentry: resolve(__dirname, 'src/PageAgent.ts'),\n\t\t\tname: 'PageAgent',\n\t\t\tfileName: 'page-agent',\n\t\t\tformats: ['es'],\n\t\t},\n\t\toutDir: resolve(__dirname, 'dist', 'esm'),\n\t\trollupOptions: {\n\t\t\texternal: [\n\t\t\t\t'chalk',\n\t\t\t\t'zod',\n\t\t\t\t'zod/v4',\n\t\t\t\t// all the internal packages\n\t\t\t\t/^@page-agent\\//,\n\t\t\t],\n\t\t},\n\t\tminify: false,\n\t\tsourcemap: true,\n\t\tcssCodeSplit: true,\n\t},\n\tdefine: {\n\t\t'process.env.NODE_ENV': '\"production\"',\n\t},\n})\n"
  },
  {
    "path": "packages/page-agent/vite.iife.config.js",
    "content": "// @ts-check\nimport { config as dotenvConfig } from 'dotenv'\nimport { dirname, resolve } from 'path'\nimport { fileURLToPath } from 'url'\nimport { defineConfig } from 'vite'\n// import { analyzer } from 'vite-bundle-analyzer'\nimport cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n// Load .env from repo root\ndotenvConfig({ path: resolve(__dirname, '../../.env'), quiet: true })\n\n// UMD Bundle for CDN\n// - alias all local packages so that they can be build in\n// - no external\n// - no d.ts. dts does not work with monorepo aliasing\nexport default defineConfig(() => ({\n\tplugins: [\n\t\tcssInjectedByJsPlugin({ relativeCSSInjection: true }),\n\t\t// analyzer()\n\t],\n\tpublicDir: false,\n\tesbuild: {\n\t\tkeepNames: true,\n\t},\n\tresolve: {\n\t\talias: {\n\t\t\t'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),\n\t\t\t'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),\n\t\t\t'@page-agent/core': resolve(__dirname, '../core/src/PageAgentCore.ts'),\n\t\t\t'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),\n\t\t},\n\t},\n\tbuild: {\n\t\tlib: {\n\t\t\tentry: resolve(__dirname, 'src/demo.ts'),\n\t\t\tname: 'PageAgent',\n\t\t\tfileName: () => `page-agent.demo.js`,\n\t\t\tformats: ['iife'],\n\t\t},\n\t\toutDir: resolve(__dirname, 'dist', 'iife'),\n\t\tcssCodeSplit: true,\n\t\t// minify: false,\n\t\trollupOptions: {\n\t\t\t// output: {\n\t\t\t// \t// force use .js as extension\n\t\t\t// \tentryFileNames: 'page-agent.js',\n\t\t\t// },\n\t\t\tonwarn: function (message, handler) {\n\t\t\t\tif (message.code === 'EVAL') return\n\t\t\t\thandler(message)\n\t\t\t},\n\t\t},\n\t},\n\tdefine: {\n\t\t'import.meta.env.LLM_MODEL_NAME': JSON.stringify(process.env.LLM_MODEL_NAME),\n\t\t'import.meta.env.LLM_API_KEY': JSON.stringify(process.env.LLM_API_KEY),\n\t\t'import.meta.env.LLM_BASE_URL': JSON.stringify(process.env.LLM_BASE_URL),\n\t},\n}))\n"
  },
  {
    "path": "packages/page-controller/package.json",
    "content": "{\n    \"name\": \"@page-agent/page-controller\",\n    \"version\": \"1.6.0\",\n    \"type\": \"module\",\n    \"main\": \"./dist/lib/page-controller.js\",\n    \"module\": \"./dist/lib/page-controller.js\",\n    \"types\": \"./dist/lib/PageController.d.ts\",\n    \"exports\": {\n        \".\": {\n            \"types\": \"./dist/lib/PageController.d.ts\",\n            \"import\": \"./dist/lib/page-controller.js\",\n            \"default\": \"./dist/lib/page-controller.js\"\n        }\n    },\n    \"files\": [\n        \"dist/\"\n    ],\n    \"description\": \"Page controller for page-agent - DOM operations and element interactions\",\n    \"keywords\": [\n        \"page-agent\",\n        \"dom\",\n        \"browser-automation\",\n        \"web-automation\"\n    ],\n    \"author\": \"Simon<gaomeng1900>\",\n    \"license\": \"MIT\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/alibaba/page-agent.git\",\n        \"directory\": \"packages/page-controller\"\n    },\n    \"homepage\": \"https://alibaba.github.io/page-agent/\",\n    \"scripts\": {\n        \"build\": \"vite build\",\n        \"prepublishOnly\": \"node -e \\\"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\\\"\",\n        \"postpublish\": \"node -e \\\"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\\\"\"\n    },\n    \"dependencies\": {\n        \"ai-motion\": \"^0.4.8\"\n    }\n}\n"
  },
  {
    "path": "packages/page-controller/src/PageController.ts",
    "content": "/**\n * Copyright (C) 2025 Alibaba Group Holding Limited\n * All rights reserved.\n *\n * PageController - Manages DOM operations and element interactions.\n * Designed to be independent of LLM and can be tested in unit tests.\n * All public methods are async for potential remote calling support.\n */\nimport {\n\tclickElement,\n\tgetElementByIndex,\n\tinputTextElement,\n\tscrollHorizontally,\n\tscrollVertically,\n\tselectOptionElement,\n} from './actions'\nimport * as dom from './dom'\nimport type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type'\nimport { getPageInfo } from './dom/getPageInfo'\nimport { patchReact } from './patches/react'\nimport { isAnchorElement } from './utils'\n\n/**\n * Configuration for PageController\n */\nexport interface PageControllerConfig extends dom.DomConfig {\n\t/** Enable visual mask overlay during operations (default: false) */\n\tenableMask?: boolean\n}\n\n/**\n * Structured browser state for LLM consumption\n */\nexport interface BrowserState {\n\turl: string\n\ttitle: string\n\t/** Page info + scroll position hint (e.g. \"Page info: 1920x1080px...\\n[Start of page]\") */\n\theader: string\n\t/** Simplified HTML of interactive elements */\n\tcontent: string\n\t/** Page footer hint (e.g. \"... 300 pixels below ...\" or \"[End of page]\") */\n\tfooter: string\n}\n\ninterface ActionResult {\n\tsuccess: boolean\n\tmessage: string\n}\n\n/**\n * PageController manages DOM state and element interactions.\n * It provides async methods for all DOM operations, keeping state isolated.\n *\n * @lifecycle\n * - beforeUpdate: Emitted before the DOM tree is updated.\n * - afterUpdate: Emitted after the DOM tree is updated.\n */\nexport class PageController extends EventTarget {\n\tprivate config: PageControllerConfig\n\n\t/** Corresponds to eval_page in browser-use */\n\tprivate flatTree: FlatDomTree | null = null\n\n\t/**\n\t * All highlighted index-mapped interactive elements\n\t * Corresponds to DOMState.selector_map in browser-use\n\t */\n\tprivate selectorMap = new Map<number, InteractiveElementDomNode>()\n\n\t/** Index -> element text description mapping */\n\tprivate elementTextMap = new Map<number, string>()\n\n\t/**\n\t * Simplified HTML for LLM consumption.\n\t * Corresponds to clickable_elements_to_string in browser-use\n\t */\n\tprivate simplifiedHTML = '<EMPTY>'\n\n\t/** last time the tree was updated */\n\tprivate lastTimeUpdate = 0\n\n\t/** Whether the tree has been indexed at least once */\n\tprivate isIndexed = false\n\n\t/** Visual mask overlay for blocking user interaction during automation */\n\tprivate mask: InstanceType<typeof import('./mask/SimulatorMask').SimulatorMask> | null = null\n\tprivate maskReady: Promise<void> | null = null\n\n\tconstructor(config: PageControllerConfig = {}) {\n\t\tsuper()\n\n\t\tthis.config = config\n\n\t\tpatchReact(this)\n\n\t\tif (config.enableMask) this.initMask()\n\t}\n\n\t/**\n\t * Initialize mask asynchronously (dynamic import to avoid CSS loading in Node)\n\t */\n\tinitMask() {\n\t\tif (this.maskReady !== null) return\n\t\tthis.maskReady = (async () => {\n\t\t\tconst { SimulatorMask } = await import('./mask/SimulatorMask')\n\t\t\tthis.mask = new SimulatorMask()\n\t\t})()\n\t}\n\t// ======= State Queries =======\n\n\t/**\n\t * Get current page URL\n\t */\n\tasync getCurrentUrl(): Promise<string> {\n\t\treturn window.location.href\n\t}\n\n\t/**\n\t * Get last tree update timestamp\n\t */\n\tasync getLastUpdateTime(): Promise<number> {\n\t\treturn this.lastTimeUpdate\n\t}\n\n\t/**\n\t * Get structured browser state for LLM consumption.\n\t * Automatically calls updateTree() to refresh the DOM state.\n\t */\n\tasync getBrowserState(): Promise<BrowserState> {\n\t\tconst url = window.location.href\n\t\tconst title = document.title\n\t\tconst pi = getPageInfo()\n\t\tconst viewportExpansion = dom.resolveViewportExpansion(this.config.viewportExpansion)\n\n\t\tawait this.updateTree()\n\n\t\tconst content = this.simplifiedHTML\n\n\t\t// Build header: page info + scroll position hint\n\t\tconst titleLine = `Current Page: [${title}](${url})`\n\n\t\tconst pageInfoLine = `Page info: ${pi.viewport_width}x${pi.viewport_height}px viewport, ${pi.page_width}x${pi.page_height}px total page size, ${pi.pages_above.toFixed(1)} pages above, ${pi.pages_below.toFixed(1)} pages below, ${pi.total_pages.toFixed(1)} total pages, at ${(pi.current_page_position * 100).toFixed(0)}% of page`\n\n\t\tconst elementsLabel =\n\t\t\tviewportExpansion === -1\n\t\t\t\t? 'Interactive elements from top layer of the current page (full page):'\n\t\t\t\t: 'Interactive elements from top layer of the current page inside the viewport:'\n\n\t\tconst hasContentAbove = pi.pixels_above > 4\n\t\tconst scrollHintAbove =\n\t\t\thasContentAbove && viewportExpansion !== -1\n\t\t\t\t? `... ${pi.pixels_above} pixels above (${pi.pages_above.toFixed(1)} pages) - scroll to see more ...`\n\t\t\t\t: '[Start of page]'\n\n\t\tconst header = `${titleLine}\\n${pageInfoLine}\\n\\n${elementsLabel}\\n\\n${scrollHintAbove}`\n\n\t\t// Build footer: scroll position hint\n\t\tconst hasContentBelow = pi.pixels_below > 4\n\t\tconst footer =\n\t\t\thasContentBelow && viewportExpansion !== -1\n\t\t\t\t? `... ${pi.pixels_below} pixels below (${pi.pages_below.toFixed(1)} pages) - scroll to see more ...`\n\t\t\t\t: '[End of page]'\n\n\t\treturn { url, title, header, content, footer }\n\t}\n\n\t// ======= DOM Tree Operations =======\n\n\t/**\n\t * Update DOM tree, returns simplified HTML for LLM.\n\t * This is the main method to refresh the page state.\n\t * Automatically bypasses mask during DOM extraction if enabled.\n\t */\n\tasync updateTree(): Promise<string> {\n\t\tthis.dispatchEvent(new Event('beforeUpdate'))\n\n\t\tthis.lastTimeUpdate = Date.now()\n\n\t\t// Temporarily bypass mask to allow DOM extraction\n\t\tif (this.mask) {\n\t\t\tthis.mask.wrapper.style.pointerEvents = 'none'\n\t\t}\n\n\t\tdom.cleanUpHighlights()\n\n\t\tconst blacklist = [\n\t\t\t...(this.config.interactiveBlacklist || []),\n\t\t\t...document.querySelectorAll('[data-page-agent-not-interactive]').values(),\n\t\t]\n\n\t\tthis.flatTree = dom.getFlatTree({\n\t\t\t...this.config,\n\t\t\tinteractiveBlacklist: blacklist,\n\t\t})\n\n\t\tthis.simplifiedHTML = dom.flatTreeToString(this.flatTree, this.config.includeAttributes)\n\n\t\tthis.selectorMap.clear()\n\t\tthis.selectorMap = dom.getSelectorMap(this.flatTree)\n\n\t\tthis.elementTextMap.clear()\n\t\tthis.elementTextMap = dom.getElementTextMap(this.simplifiedHTML)\n\n\t\t// Mark as indexed - now element actions are allowed\n\t\tthis.isIndexed = true\n\n\t\t// Restore mask blocking\n\t\tif (this.mask) {\n\t\t\tthis.mask.wrapper.style.pointerEvents = 'auto'\n\t\t}\n\n\t\tthis.dispatchEvent(new Event('afterUpdate'))\n\n\t\treturn this.simplifiedHTML\n\t}\n\n\t/**\n\t * Clean up all element highlights\n\t */\n\tasync cleanUpHighlights(): Promise<void> {\n\t\tdom.cleanUpHighlights()\n\t}\n\n\t// ======= Element Actions =======\n\n\t/**\n\t * Ensure the tree has been indexed before any index-based operation.\n\t * Throws if updateTree() hasn't been called yet.\n\t */\n\tprivate assertIndexed(): void {\n\t\tif (!this.isIndexed) {\n\t\t\tthrow new Error('DOM tree not indexed yet. Can not perform actions on elements.')\n\t\t}\n\t}\n\n\t/**\n\t * Click element by index\n\t */\n\tasync clickElement(index: number): Promise<ActionResult> {\n\t\ttry {\n\t\t\tthis.assertIndexed()\n\t\t\tconst element = getElementByIndex(this.selectorMap, index)\n\t\t\tconst elemText = this.elementTextMap.get(index)\n\t\t\tawait clickElement(element)\n\n\t\t\t// Handle links that open in new tabs\n\t\t\tif (isAnchorElement(element) && element.target === '_blank') {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tmessage: `✅ Clicked element (${elemText ?? index}). ⚠️ Link opened in a new tab.`,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tmessage: `✅ Clicked element (${elemText ?? index}).`,\n\t\t\t}\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\tmessage: `❌ Failed to click element: ${error}`,\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Input text into element by index\n\t */\n\tasync inputText(index: number, text: string): Promise<ActionResult> {\n\t\ttry {\n\t\t\tthis.assertIndexed()\n\t\t\tconst element = getElementByIndex(this.selectorMap, index)\n\t\t\tconst elemText = this.elementTextMap.get(index)\n\t\t\tawait inputTextElement(element, text)\n\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tmessage: `✅ Input text (${text}) into element (${elemText ?? index}).`,\n\t\t\t}\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\tmessage: `❌ Failed to input text: ${error}`,\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Select dropdown option by index and option text\n\t */\n\tasync selectOption(index: number, optionText: string): Promise<ActionResult> {\n\t\ttry {\n\t\t\tthis.assertIndexed()\n\t\t\tconst element = getElementByIndex(this.selectorMap, index)\n\t\t\tconst elemText = this.elementTextMap.get(index)\n\t\t\tawait selectOptionElement(element as HTMLSelectElement, optionText)\n\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tmessage: `✅ Selected option (${optionText}) in element (${elemText ?? index}).`,\n\t\t\t}\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\tmessage: `❌ Failed to select option: ${error}`,\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Scroll vertically\n\t */\n\tasync scroll(options: {\n\t\tdown: boolean\n\t\tnumPages: number\n\t\tpixels?: number\n\t\tindex?: number\n\t}): Promise<ActionResult> {\n\t\ttry {\n\t\t\tconst { down, numPages, pixels, index } = options\n\n\t\t\tthis.assertIndexed()\n\n\t\t\tconst scrollAmount = pixels ?? numPages * (down ? 1 : -1) * window.innerHeight\n\n\t\t\tconst element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null\n\n\t\t\tconst message = await scrollVertically(down, scrollAmount, element)\n\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tmessage,\n\t\t\t}\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\tmessage: `❌ Failed to scroll: ${error}`,\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Scroll horizontally\n\t */\n\tasync scrollHorizontally(options: {\n\t\tright: boolean\n\t\tpixels: number\n\t\tindex?: number\n\t}): Promise<ActionResult> {\n\t\ttry {\n\t\t\tconst { right, pixels, index } = options\n\n\t\t\tthis.assertIndexed()\n\n\t\t\tconst scrollAmount = pixels * (right ? 1 : -1)\n\n\t\t\tconst element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null\n\n\t\t\tconst message = await scrollHorizontally(right, scrollAmount, element)\n\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tmessage,\n\t\t\t}\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\tmessage: `❌ Failed to scroll horizontally: ${error}`,\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Execute arbitrary JavaScript on the page\n\t */\n\tasync executeJavascript(script: string): Promise<ActionResult> {\n\t\ttry {\n\t\t\t// Wrap script in async function to support await\n\t\t\tconst asyncFunction = eval(`(async () => { ${script} })`)\n\t\t\tconst result = await asyncFunction()\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tmessage: `✅ Executed JavaScript. Result: ${result}`,\n\t\t\t}\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\tmessage: `❌ Error executing JavaScript: ${error}`,\n\t\t\t}\n\t\t}\n\t}\n\n\t// ======= Mask Operations =======\n\n\t/**\n\t * Show the visual mask overlay.\n\t * Only works after mask is setup.\n\t */\n\tasync showMask(): Promise<void> {\n\t\tawait this.maskReady\n\t\tthis.mask?.show()\n\t}\n\n\t/**\n\t * Hide the visual mask overlay.\n\t * Only works after mask is setup.\n\t */\n\tasync hideMask(): Promise<void> {\n\t\tawait this.maskReady\n\t\tthis.mask?.hide()\n\t}\n\n\t/**\n\t * Dispose and clean up resources\n\t */\n\tdispose(): void {\n\t\tdom.cleanUpHighlights()\n\t\tthis.flatTree = null\n\t\tthis.selectorMap.clear()\n\t\tthis.elementTextMap.clear()\n\t\tthis.simplifiedHTML = '<EMPTY>'\n\t\tthis.isIndexed = false\n\t\tthis.mask?.dispose()\n\t\tthis.mask = null\n\t}\n}\n"
  },
  {
    "path": "packages/page-controller/src/actions.ts",
    "content": "/**\n * Copyright (C) 2025 Alibaba Group Holding Limited\n * All rights reserved.\n */\nimport type { InteractiveElementDomNode } from './dom/dom_tree/type'\nimport {\n\tgetNativeValueSetter,\n\tisHTMLElement,\n\tisInputElement,\n\tisSelectElement,\n\tisTextAreaElement,\n\tmovePointerToElement,\n\twaitFor,\n} from './utils'\n\n/**\n * Get the HTMLElement by index from a selectorMap.\n */\nexport function getElementByIndex(\n\tselectorMap: Map<number, InteractiveElementDomNode>,\n\tindex: number\n): HTMLElement {\n\tconst interactiveNode = selectorMap.get(index)\n\tif (!interactiveNode) {\n\t\tthrow new Error(`No interactive element found at index ${index}`)\n\t}\n\n\tconst element = interactiveNode.ref\n\tif (!element) {\n\t\tthrow new Error(`Element at index ${index} does not have a reference`)\n\t}\n\n\tif (!isHTMLElement(element)) {\n\t\tthrow new Error(`Element at index ${index} is not an HTMLElement`)\n\t}\n\n\treturn element\n}\n\nlet lastClickedElement: HTMLElement | null = null\n\nfunction blurLastClickedElement() {\n\tif (lastClickedElement) {\n\t\tlastClickedElement.blur()\n\t\tlastClickedElement.dispatchEvent(\n\t\t\tnew MouseEvent('mouseout', { bubbles: true, cancelable: true })\n\t\t)\n\t\tlastClickedElement.dispatchEvent(\n\t\t\tnew MouseEvent('mouseleave', { bubbles: false, cancelable: true })\n\t\t)\n\t\tlastClickedElement = null\n\t}\n}\n\n/**\n * Simulate a click on the element\n */\nexport async function clickElement(element: HTMLElement) {\n\tblurLastClickedElement()\n\n\tlastClickedElement = element\n\n\tawait scrollIntoViewIfNeeded(element)\n\t// Scroll the iframe element itself into view if needed\n\tconst frame = element.ownerDocument.defaultView?.frameElement\n\tif (frame) await scrollIntoViewIfNeeded(frame)\n\n\tawait movePointerToElement(element)\n\twindow.dispatchEvent(new CustomEvent('PageAgent::ClickPointer'))\n\n\tawait waitFor(0.1)\n\n\t// hover it\n\telement.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true }))\n\telement.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true }))\n\n\t// dispatch a sequence of events to ensure all listeners are triggered\n\telement.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }))\n\n\t// focus it to ensure it gets the click event\n\telement.focus()\n\n\telement.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }))\n\telement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))\n\n\t// dispatch a click event\n\t// element.click()\n\n\tawait waitFor(0.2) // Wait to ensure click event processing completes\n}\n\nexport async function inputTextElement(element: HTMLElement, text: string) {\n\tconst isContentEditable = element.isContentEditable\n\tif (!isInputElement(element) && !isTextAreaElement(element) && !isContentEditable) {\n\t\tthrow new Error('Element is not an input, textarea, or contenteditable')\n\t}\n\n\tawait clickElement(element)\n\n\tif (isContentEditable) {\n\t\t// Contenteditable support (partial)\n\t\t// Not supported:\n\t\t// - Monaco/CodeMirror: Require direct JS instance access. No universal way to obtain.\n\t\t// - Draft.js: Not responsive to synthetic/execCommand/Range/DataTransfer. Unmaintained.\n\t\t//\n\t\t// Strategy: Try Plan A (synthetic events) first, then verify and fall back\n\t\t// to Plan B (execCommand) if the text wasn't actually inserted.\n\t\t//\n\t\t// Plan A: Dispatch synthetic events\n\t\t// Works: React contenteditable, Quill.\n\t\t// Fails: Slate.js, some contenteditable editors that ignore synthetic events.\n\t\t// Sequence: beforeinput -> mutation -> input -> change -> blur\n\n\t\t// Dispatch beforeinput + mutation + input for clearing\n\t\tif (\n\t\t\telement.dispatchEvent(\n\t\t\t\tnew InputEvent('beforeinput', {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tcancelable: true,\n\t\t\t\t\tinputType: 'deleteContent',\n\t\t\t\t})\n\t\t\t)\n\t\t) {\n\t\t\telement.innerText = ''\n\t\t\telement.dispatchEvent(\n\t\t\t\tnew InputEvent('input', {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tinputType: 'deleteContent',\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\n\t\t// Dispatch beforeinput + mutation + input for insertion (important for React apps)\n\t\tif (\n\t\t\telement.dispatchEvent(\n\t\t\t\tnew InputEvent('beforeinput', {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tcancelable: true,\n\t\t\t\t\tinputType: 'insertText',\n\t\t\t\t\tdata: text,\n\t\t\t\t})\n\t\t\t)\n\t\t) {\n\t\t\telement.innerText = text\n\t\t\telement.dispatchEvent(\n\t\t\t\tnew InputEvent('input', {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tinputType: 'insertText',\n\t\t\t\t\tdata: text,\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\n\t\t// Verify Plan A worked by checking if the text was actually inserted\n\t\tconst planASucceeded = element.innerText.trim() === text.trim()\n\n\t\tif (!planASucceeded) {\n\t\t\t// Plan B: execCommand fallback (deprecated but widely supported)\n\t\t\t// Works: Quill, Slate.js, react contenteditable components.\n\t\t\t// This approach integrates with the browser's undo stack and is handled\n\t\t\t// natively by most rich-text editors.\n\t\t\telement.focus()\n\n\t\t\t// Select all existing content and delete it\n\t\t\tconst doc = element.ownerDocument\n\t\t\tconst selection = (doc.defaultView || window).getSelection()\n\t\t\tconst range = doc.createRange()\n\t\t\trange.selectNodeContents(element)\n\t\t\tselection?.removeAllRanges()\n\t\t\tselection?.addRange(range)\n\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\tdoc.execCommand('delete', false)\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\tdoc.execCommand('insertText', false, text)\n\t\t}\n\n\t\t// Dispatch change event (for good measure)\n\t\telement.dispatchEvent(new Event('change', { bubbles: true }))\n\n\t\t// Trigger blur for validation\n\t\telement.blur()\n\t} else {\n\t\tgetNativeValueSetter(element as HTMLInputElement | HTMLTextAreaElement).call(element, text)\n\t}\n\n\t// Only dispatch shared input event for non-contenteditable (contenteditable has its own)\n\tif (!isContentEditable) {\n\t\telement.dispatchEvent(new Event('input', { bubbles: true }))\n\t}\n\n\tawait waitFor(0.1)\n\n\tblurLastClickedElement()\n}\n\n/**\n * @todo browser-use version is very complex and supports menu tags, need to follow up\n */\nexport async function selectOptionElement(selectElement: HTMLSelectElement, optionText: string) {\n\tif (!isSelectElement(selectElement)) {\n\t\tthrow new Error('Element is not a select element')\n\t}\n\n\tconst options = Array.from(selectElement.options)\n\tconst option = options.find((opt) => opt.textContent?.trim() === optionText.trim())\n\n\tif (!option) {\n\t\tthrow new Error(`Option with text \"${optionText}\" not found in select element`)\n\t}\n\n\tselectElement.value = option.value\n\tselectElement.dispatchEvent(new Event('change', { bubbles: true }))\n\n\tawait waitFor(0.1) // Wait to ensure change event processing completes\n}\n\ninterface ScrollableElement extends Element {\n\tscrollIntoViewIfNeeded?: (centerIfNeeded?: boolean) => void\n}\n\nexport async function scrollIntoViewIfNeeded(element: Element) {\n\tconst el = element as ScrollableElement\n\tif (typeof el.scrollIntoViewIfNeeded === 'function') {\n\t\tel.scrollIntoViewIfNeeded()\n\t\t// await waitFor(0.5) // Animation playback\n\t} else {\n\t\t// @todo visibility check\n\t\telement.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' })\n\t\t// await waitFor(0.5) // Animation playback\n\t}\n}\n\nexport async function scrollVertically(\n\tdown: boolean,\n\tscroll_amount: number,\n\telement?: HTMLElement | null\n) {\n\t// Element-specific scrolling if element is provided\n\tif (element) {\n\t\tconst targetElement = element\n\t\tlet currentElement = targetElement as HTMLElement | null\n\t\tlet scrollSuccess = false\n\t\tlet scrolledElement: HTMLElement | null = null\n\t\tlet scrollDelta = 0\n\t\tlet attempts = 0\n\t\tconst dy = scroll_amount\n\n\t\twhile (currentElement && attempts < 10) {\n\t\t\tconst computedStyle = window.getComputedStyle(currentElement)\n\t\t\tconst hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY)\n\t\t\tconst canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight\n\n\t\t\tif (hasScrollableY && canScrollVertically) {\n\t\t\t\tconst beforeScroll = currentElement.scrollTop\n\t\t\t\tconst maxScroll = currentElement.scrollHeight - currentElement.clientHeight\n\n\t\t\t\tlet scrollAmount = dy / 3\n\n\t\t\t\tif (scrollAmount > 0) {\n\t\t\t\t\tscrollAmount = Math.min(scrollAmount, maxScroll - beforeScroll)\n\t\t\t\t} else {\n\t\t\t\t\tscrollAmount = Math.max(scrollAmount, -beforeScroll)\n\t\t\t\t}\n\n\t\t\t\tcurrentElement.scrollTop = beforeScroll + scrollAmount\n\n\t\t\t\tconst afterScroll = currentElement.scrollTop\n\t\t\t\tconst actualScrollDelta = afterScroll - beforeScroll\n\n\t\t\t\tif (Math.abs(actualScrollDelta) > 0.5) {\n\t\t\t\t\tscrollSuccess = true\n\t\t\t\t\tscrolledElement = currentElement\n\t\t\t\t\tscrollDelta = actualScrollDelta\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (currentElement === document.body || currentElement === document.documentElement) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcurrentElement = currentElement.parentElement\n\t\t\tattempts++\n\t\t}\n\n\t\tif (scrollSuccess) {\n\t\t\treturn `Scrolled container (${scrolledElement?.tagName}) by ${scrollDelta}px`\n\t\t} else {\n\t\t\treturn `No scrollable container found for element (${targetElement.tagName})`\n\t\t}\n\t}\n\n\t// Page-level scrolling (default or fallback)\n\n\tconst dy = scroll_amount\n\tconst bigEnough = (el: HTMLElement) => el.clientHeight >= window.innerHeight * 0.5\n\tconst canScroll = (el: HTMLElement | null) =>\n\t\tel &&\n\t\t/(auto|scroll|overlay)/.test(getComputedStyle(el).overflowY) &&\n\t\tel.scrollHeight > el.clientHeight &&\n\t\tbigEnough(el)\n\n\tlet el: HTMLElement | null = document.activeElement as HTMLElement | null\n\twhile (el && !canScroll(el) && el !== document.body) el = el.parentElement\n\n\tel = canScroll(el)\n\t\t? el\n\t\t: Array.from(document.querySelectorAll<HTMLElement>('*')).find(canScroll) ||\n\t\t\t(document.scrollingElement as HTMLElement) ||\n\t\t\t(document.documentElement as HTMLElement)\n\n\tif (el === document.scrollingElement || el === document.documentElement || el === document.body) {\n\t\t// Page-level scroll\n\t\tconst scrollBefore = window.scrollY\n\t\tconst scrollMax = document.documentElement.scrollHeight - window.innerHeight\n\n\t\twindow.scrollBy(0, dy)\n\n\t\tconst scrollAfter = window.scrollY\n\t\tconst scrolled = scrollAfter - scrollBefore\n\n\t\tif (Math.abs(scrolled) < 1) {\n\t\t\treturn dy > 0\n\t\t\t\t? `⚠️ Already at the bottom of the page, cannot scroll down further.`\n\t\t\t\t: `⚠️ Already at the top of the page, cannot scroll up further.`\n\t\t}\n\n\t\tconst reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1\n\t\tconst reachedTop = dy < 0 && scrollAfter <= 1\n\n\t\tif (reachedBottom) return `✅ Scrolled page by ${scrolled}px. Reached the bottom of the page.`\n\t\tif (reachedTop) return `✅ Scrolled page by ${scrolled}px. Reached the top of the page.`\n\t\treturn `✅ Scrolled page by ${scrolled}px.`\n\t} else {\n\t\t// Container scroll\n\t\tconst scrollBefore = el!.scrollTop\n\t\tconst scrollMax = el!.scrollHeight - el!.clientHeight\n\n\t\tel!.scrollBy({ top: dy, behavior: 'smooth' })\n\t\tawait waitFor(0.1)\n\n\t\tconst scrollAfter = el!.scrollTop\n\t\tconst scrolled = scrollAfter - scrollBefore\n\n\t\tif (Math.abs(scrolled) < 1) {\n\t\t\treturn dy > 0\n\t\t\t\t? `⚠️ Already at the bottom of container (${el!.tagName}), cannot scroll down further.`\n\t\t\t\t: `⚠️ Already at the top of container (${el!.tagName}), cannot scroll up further.`\n\t\t}\n\n\t\tconst reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1\n\t\tconst reachedTop = dy < 0 && scrollAfter <= 1\n\n\t\tif (reachedBottom)\n\t\t\treturn `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the bottom.`\n\t\tif (reachedTop)\n\t\t\treturn `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the top.`\n\t\treturn `✅ Scrolled container (${el!.tagName}) by ${scrolled}px.`\n\t}\n}\n\nexport async function scrollHorizontally(\n\tright: boolean,\n\tscroll_amount: number,\n\telement?: HTMLElement | null\n) {\n\t// Element-specific scrolling if element is provided\n\tif (element) {\n\t\tconst targetElement = element\n\t\tlet currentElement = targetElement as HTMLElement | null\n\t\tlet scrollSuccess = false\n\t\tlet scrolledElement: HTMLElement | null = null\n\t\tlet scrollDelta = 0\n\t\tlet attempts = 0\n\t\tconst dx = right ? scroll_amount : -scroll_amount\n\n\t\twhile (currentElement && attempts < 10) {\n\t\t\tconst computedStyle = window.getComputedStyle(currentElement)\n\t\t\tconst hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX)\n\t\t\tconst canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth\n\n\t\t\tif (hasScrollableX && canScrollHorizontally) {\n\t\t\t\tconst beforeScroll = currentElement.scrollLeft\n\t\t\t\tconst maxScroll = currentElement.scrollWidth - currentElement.clientWidth\n\n\t\t\t\tlet scrollAmount = dx / 3\n\n\t\t\t\tif (scrollAmount > 0) {\n\t\t\t\t\tscrollAmount = Math.min(scrollAmount, maxScroll - beforeScroll)\n\t\t\t\t} else {\n\t\t\t\t\tscrollAmount = Math.max(scrollAmount, -beforeScroll)\n\t\t\t\t}\n\n\t\t\t\tcurrentElement.scrollLeft = beforeScroll + scrollAmount\n\n\t\t\t\tconst afterScroll = currentElement.scrollLeft\n\t\t\t\tconst actualScrollDelta = afterScroll - beforeScroll\n\n\t\t\t\tif (Math.abs(actualScrollDelta) > 0.5) {\n\t\t\t\t\tscrollSuccess = true\n\t\t\t\t\tscrolledElement = currentElement\n\t\t\t\t\tscrollDelta = actualScrollDelta\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (currentElement === document.body || currentElement === document.documentElement) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcurrentElement = currentElement.parentElement\n\t\t\tattempts++\n\t\t}\n\n\t\tif (scrollSuccess) {\n\t\t\treturn `Scrolled container (${scrolledElement?.tagName}) horizontally by ${scrollDelta}px`\n\t\t} else {\n\t\t\treturn `No horizontally scrollable container found for element (${targetElement.tagName})`\n\t\t}\n\t}\n\n\t// Page-level scrolling (default or fallback)\n\n\tconst dx = right ? scroll_amount : -scroll_amount\n\tconst bigEnough = (el: HTMLElement) => el.clientWidth >= window.innerWidth * 0.5\n\tconst canScroll = (el: HTMLElement | null) =>\n\t\tel &&\n\t\t/(auto|scroll|overlay)/.test(getComputedStyle(el).overflowX) &&\n\t\tel.scrollWidth > el.clientWidth &&\n\t\tbigEnough(el)\n\n\tlet el: HTMLElement | null = document.activeElement as HTMLElement | null\n\twhile (el && !canScroll(el) && el !== document.body) el = el.parentElement\n\n\tel = canScroll(el)\n\t\t? el\n\t\t: Array.from(document.querySelectorAll<HTMLElement>('*')).find(canScroll) ||\n\t\t\t(document.scrollingElement as HTMLElement) ||\n\t\t\t(document.documentElement as HTMLElement)\n\n\tif (el === document.scrollingElement || el === document.documentElement || el === document.body) {\n\t\t// Page-level scroll\n\t\tconst scrollBefore = window.scrollX\n\t\tconst scrollMax = document.documentElement.scrollWidth - window.innerWidth\n\n\t\twindow.scrollBy(dx, 0)\n\n\t\tconst scrollAfter = window.scrollX\n\t\tconst scrolled = scrollAfter - scrollBefore\n\n\t\tif (Math.abs(scrolled) < 1) {\n\t\t\treturn dx > 0\n\t\t\t\t? `⚠️ Already at the right edge of the page, cannot scroll right further.`\n\t\t\t\t: `⚠️ Already at the left edge of the page, cannot scroll left further.`\n\t\t}\n\n\t\tconst reachedRight = dx > 0 && scrollAfter >= scrollMax - 1\n\t\tconst reachedLeft = dx < 0 && scrollAfter <= 1\n\n\t\tif (reachedRight)\n\t\t\treturn `✅ Scrolled page by ${scrolled}px. Reached the right edge of the page.`\n\t\tif (reachedLeft) return `✅ Scrolled page by ${scrolled}px. Reached the left edge of the page.`\n\t\treturn `✅ Scrolled page horizontally by ${scrolled}px.`\n\t} else {\n\t\t// Container scroll\n\t\tconst scrollBefore = el!.scrollLeft\n\t\tconst scrollMax = el!.scrollWidth - el!.clientWidth\n\n\t\tel!.scrollBy({ left: dx, behavior: 'smooth' })\n\t\tawait waitFor(0.1)\n\n\t\tconst scrollAfter = el!.scrollLeft\n\t\tconst scrolled = scrollAfter - scrollBefore\n\n\t\tif (Math.abs(scrolled) < 1) {\n\t\t\treturn dx > 0\n\t\t\t\t? `⚠️ Already at the right edge of container (${el!.tagName}), cannot scroll right further.`\n\t\t\t\t: `⚠️ Already at the left edge of container (${el!.tagName}), cannot scroll left further.`\n\t\t}\n\n\t\tconst reachedRight = dx > 0 && scrollAfter >= scrollMax - 1\n\t\tconst reachedLeft = dx < 0 && scrollAfter <= 1\n\n\t\tif (reachedRight)\n\t\t\treturn `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the right edge.`\n\t\tif (reachedLeft)\n\t\t\treturn `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the left edge.`\n\t\treturn `✅ Scrolled container (${el!.tagName}) horizontally by ${scrolled}px.`\n\t}\n}\n"
  },
  {
    "path": "packages/page-controller/src/dom/dom_tree/index.js",
    "content": "/**\n * @file port from browser-use\n * @see https://github.com/browser-use/browser-use/commits/main/browser_use/dom/dom_tree/index.js\n * @match 0.5.9 d51b6e73daff7165fdd3e44debd667e7f5f7fdc5\n *\n * search @edit for all the changed lines.\n *\n * @edit export\n * @edit add interactiveBlacklist interactiveWhitelist\n * @edit adjustable opacity\n * @edit direct dom ref\n * @edit @workaround input.checked\n * @edit smaller zIndex for highlight\n * @edit no need for xpath\n * @edit add `extra` field for extra data\n * @edit scrollable element detection\n * @edit add `data-browser-use-ignore` attribute\n * @edit improve `sampleRect`, filter out rects with 0 area\n * @edit exclude aria-hidden elements\n * @edit make sure attributes exist for interactive candidates.\n */\n\nexport default (\n\targs = {\n\t\tdoHighlightElements: true,\n\t\tfocusHighlightIndex: -1,\n\t\tviewportExpansion: 0,\n\t\tdebugMode: false,\n\n\t\t/**\n\t\t * @edit\n\t\t */\n\t\t/** @type {Element[]} */\n\t\tinteractiveBlacklist: [],\n\t\t/** @type {Element[]} */\n\t\tinteractiveWhitelist: [],\n\t\thighlightOpacity: 0.1,\n\t\thighlightLabelOpacity: 0.5,\n\t}\n) => {\n\t/**\n\t * @edit\n\t */\n\tconst { interactiveBlacklist, interactiveWhitelist, highlightOpacity, highlightLabelOpacity } =\n\t\targs\n\n\tconst { doHighlightElements, focusHighlightIndex, viewportExpansion, debugMode } = args\n\tlet highlightIndex = 0 // Reset highlight index\n\n\t/**\n\t * @edit add `extra` field for extra data\n\t */\n\tconst extraData = new WeakMap()\n\tfunction addExtraData(element, data) {\n\t\tif (!element || element.nodeType !== Node.ELEMENT_NODE) return\n\t\textraData.set(element, { ...extraData.get(element), ...data })\n\t}\n\n\t// Add caching mechanisms at the top level\n\tconst DOM_CACHE = {\n\t\tboundingRects: new WeakMap(),\n\t\tclientRects: new WeakMap(),\n\t\tcomputedStyles: new WeakMap(),\n\t\tclearCache: () => {\n\t\t\tDOM_CACHE.boundingRects = new WeakMap()\n\t\t\tDOM_CACHE.clientRects = new WeakMap()\n\t\t\tDOM_CACHE.computedStyles = new WeakMap()\n\t\t},\n\t}\n\n\t/**\n\t * Gets the cached bounding rect for an element.\n\t *\n\t * @param {HTMLElement} element - The element to get the bounding rect for.\n\t * @returns {DOMRect | null} The cached bounding rect, or null if the element is not found.\n\t */\n\tfunction getCachedBoundingRect(element) {\n\t\tif (!element) return null\n\n\t\tif (DOM_CACHE.boundingRects.has(element)) {\n\t\t\treturn DOM_CACHE.boundingRects.get(element)\n\t\t}\n\n\t\tconst rect = element.getBoundingClientRect()\n\n\t\tif (rect) {\n\t\t\tDOM_CACHE.boundingRects.set(element, rect)\n\t\t}\n\t\treturn rect\n\t}\n\n\t/**\n\t * Gets the cached computed style for an element.\n\t *\n\t * @param {HTMLElement} element - The element to get the computed style for.\n\t * @returns {CSSStyleDeclaration | null} The cached computed style, or null if the element is not found.\n\t */\n\tfunction getCachedComputedStyle(element) {\n\t\tif (!element) return null\n\n\t\tif (DOM_CACHE.computedStyles.has(element)) {\n\t\t\treturn DOM_CACHE.computedStyles.get(element)\n\t\t}\n\n\t\tconst style = window.getComputedStyle(element)\n\n\t\tif (style) {\n\t\t\tDOM_CACHE.computedStyles.set(element, style)\n\t\t}\n\t\treturn style\n\t}\n\n\t/**\n\t * Gets the cached client rects for an element.\n\t *\n\t * @param {HTMLElement} element - The element to get the client rects for.\n\t * @returns {DOMRectList | null} The cached client rects, or null if the element is not found.\n\t */\n\tfunction getCachedClientRects(element) {\n\t\tif (!element) return null\n\n\t\tif (DOM_CACHE.clientRects.has(element)) {\n\t\t\treturn DOM_CACHE.clientRects.get(element)\n\t\t}\n\n\t\tconst rects = element.getClientRects()\n\n\t\tif (rects) {\n\t\t\tDOM_CACHE.clientRects.set(element, rects)\n\t\t}\n\t\treturn rects\n\t}\n\n\t/**\n\t * Hash map of DOM nodes indexed by their highlight index.\n\t *\n\t * @type {Object<string, any>}\n\t */\n\tconst DOM_HASH_MAP = {}\n\n\tconst ID = { current: 0 }\n\n\tconst HIGHLIGHT_CONTAINER_ID = 'playwright-highlight-container'\n\n\t// Add a WeakMap cache for XPath strings\n\tconst xpathCache = new WeakMap()\n\n\t// // Initialize once and reuse\n\t// const viewportObserver = new IntersectionObserver(\n\t//   (entries) => {\n\t//     entries.forEach(entry => {\n\t//       elementVisibilityMap.set(entry.target, entry.isIntersecting);\n\t//     });\n\t//   },\n\t//   { rootMargin: `${viewportExpansion}px` }\n\t// );\n\n\t/**\n\t * Highlights an element in the DOM and returns the index of the next element.\n\t *\n\t * @param {HTMLElement} element - The element to highlight.\n\t * @param {number} index - The index of the element.\n\t * @param {HTMLElement | null} parentIframe - The parent iframe node.\n\t * @returns {number} The index of the next element.\n\t */\n\tfunction highlightElement(element, index, parentIframe = null) {\n\t\tif (!element) return index\n\n\t\tconst overlays = []\n\t\t/**\n\t\t * @type {HTMLElement | null}\n\t\t */\n\t\tlet label = null\n\t\tlet labelWidth = 20\n\t\tlet labelHeight = 16\n\t\tlet cleanupFn = null\n\n\t\ttry {\n\t\t\t// Create or get highlight container\n\t\t\tlet container = document.getElementById(HIGHLIGHT_CONTAINER_ID)\n\t\t\tif (!container) {\n\t\t\t\tcontainer = document.createElement('div')\n\t\t\t\tcontainer.id = HIGHLIGHT_CONTAINER_ID\n\t\t\t\tcontainer.style.position = 'fixed'\n\t\t\t\tcontainer.style.pointerEvents = 'none'\n\t\t\t\tcontainer.style.top = '0'\n\t\t\t\tcontainer.style.left = '0'\n\t\t\t\tcontainer.style.width = '100%'\n\t\t\t\tcontainer.style.height = '100%'\n\n\t\t\t\t/**\n\t\t\t\t * @edit smaller zIndex for highlight\n\t\t\t\t */\n\t\t\t\t// Use the maximum valid value in zIndex to ensure the element is not blocked by overlapping elements.\n\t\t\t\t// container.style.zIndex = \"2147483647\";\n\t\t\t\tcontainer.style.zIndex = '2147483640'\n\n\t\t\t\tcontainer.style.backgroundColor = 'transparent'\n\t\t\t\tdocument.body.appendChild(container)\n\t\t\t}\n\n\t\t\t// Get element client rects\n\t\t\tconst rects = element.getClientRects() // Use getClientRects()\n\n\t\t\tif (!rects || rects.length === 0) return index // Exit if no rects\n\n\t\t\t// Generate a color based on the index\n\t\t\tconst colors = [\n\t\t\t\t'#FF0000',\n\t\t\t\t'#00FF00',\n\t\t\t\t'#0000FF',\n\t\t\t\t'#FFA500',\n\t\t\t\t'#800080',\n\t\t\t\t'#008080',\n\t\t\t\t'#FF69B4',\n\t\t\t\t'#4B0082',\n\t\t\t\t'#FF4500',\n\t\t\t\t'#2E8B57',\n\t\t\t\t'#DC143C',\n\t\t\t\t'#4682B4',\n\t\t\t]\n\t\t\tconst colorIndex = index % colors.length\n\t\t\tlet baseColor = colors[colorIndex]\n\n\t\t\t/**\n\t\t\t * @edit adjustable opacity\n\t\t\t */\n\t\t\t// const backgroundColor = baseColor + \"1A\"; // 10% opacity version of the color\n\t\t\tconst backgroundColor =\n\t\t\t\tbaseColor +\n\t\t\t\tMath.floor(highlightOpacity * 255)\n\t\t\t\t\t.toString(16)\n\t\t\t\t\t.padStart(2, '0')\n\t\t\tbaseColor =\n\t\t\t\tbaseColor +\n\t\t\t\tMath.floor(highlightLabelOpacity * 255)\n\t\t\t\t\t.toString(16)\n\t\t\t\t\t.padStart(2, '0')\n\n\t\t\t// Get iframe offset if necessary\n\t\t\tlet iframeOffset = { x: 0, y: 0 }\n\t\t\tif (parentIframe) {\n\t\t\t\tconst iframeRect = parentIframe.getBoundingClientRect() // Keep getBoundingClientRect for iframe offset\n\t\t\t\tiframeOffset.x = iframeRect.left\n\t\t\t\tiframeOffset.y = iframeRect.top\n\t\t\t}\n\n\t\t\t// Create fragment to hold overlay elements\n\t\t\tconst fragment = document.createDocumentFragment()\n\n\t\t\t// Create highlight overlays for each client rect\n\t\t\tfor (const rect of rects) {\n\t\t\t\tif (rect.width === 0 || rect.height === 0) continue // Skip empty rects\n\n\t\t\t\tconst overlay = document.createElement('div')\n\t\t\t\toverlay.style.position = 'fixed'\n\t\t\t\toverlay.style.border = `2px solid ${baseColor}`\n\t\t\t\toverlay.style.backgroundColor = backgroundColor\n\t\t\t\toverlay.style.pointerEvents = 'none'\n\t\t\t\toverlay.style.boxSizing = 'border-box'\n\n\t\t\t\tconst top = rect.top + iframeOffset.y\n\t\t\t\tconst left = rect.left + iframeOffset.x\n\n\t\t\t\toverlay.style.top = `${top}px`\n\t\t\t\toverlay.style.left = `${left}px`\n\t\t\t\toverlay.style.width = `${rect.width}px`\n\t\t\t\toverlay.style.height = `${rect.height}px`\n\n\t\t\t\tfragment.appendChild(overlay)\n\t\t\t\toverlays.push({ element: overlay, initialRect: rect }) // Store overlay and its rect\n\t\t\t}\n\n\t\t\t// Create and position a single label relative to the first rect\n\t\t\tconst firstRect = rects[0]\n\t\t\tlabel = document.createElement('div')\n\t\t\tlabel.className = 'playwright-highlight-label'\n\t\t\tlabel.style.position = 'fixed'\n\t\t\tlabel.style.background = baseColor\n\t\t\tlabel.style.color = 'white'\n\t\t\tlabel.style.padding = '1px 4px'\n\t\t\tlabel.style.borderRadius = '4px'\n\t\t\tlabel.style.fontSize = `${Math.min(12, Math.max(8, firstRect.height / 2))}px`\n\t\t\tlabel.textContent = index.toString()\n\n\t\t\tlabelWidth = label.offsetWidth > 0 ? label.offsetWidth : labelWidth // Update actual width if possible\n\t\t\tlabelHeight = label.offsetHeight > 0 ? label.offsetHeight : labelHeight // Update actual height if possible\n\n\t\t\tconst firstRectTop = firstRect.top + iframeOffset.y\n\t\t\tconst firstRectLeft = firstRect.left + iframeOffset.x\n\n\t\t\tlet labelTop = firstRectTop + 2\n\t\t\tlet labelLeft = firstRectLeft + firstRect.width - labelWidth - 2\n\n\t\t\t// Adjust label position if first rect is too small\n\t\t\tif (firstRect.width < labelWidth + 4 || firstRect.height < labelHeight + 4) {\n\t\t\t\tlabelTop = firstRectTop - labelHeight - 2\n\t\t\t\tlabelLeft = firstRectLeft + firstRect.width - labelWidth // Align with right edge\n\t\t\t\tif (labelLeft < iframeOffset.x) labelLeft = firstRectLeft // Prevent going off-left\n\t\t\t}\n\n\t\t\t// Ensure label stays within viewport bounds slightly better\n\t\t\tlabelTop = Math.max(0, Math.min(labelTop, window.innerHeight - labelHeight))\n\t\t\tlabelLeft = Math.max(0, Math.min(labelLeft, window.innerWidth - labelWidth))\n\n\t\t\tlabel.style.top = `${labelTop}px`\n\t\t\tlabel.style.left = `${labelLeft}px`\n\n\t\t\tfragment.appendChild(label)\n\n\t\t\t// Update positions on scroll/resize\n\t\t\tconst updatePositions = () => {\n\t\t\t\tconst newRects = element.getClientRects() // Get fresh rects\n\t\t\t\tlet newIframeOffset = { x: 0, y: 0 }\n\n\t\t\t\tif (parentIframe) {\n\t\t\t\t\tconst iframeRect = parentIframe.getBoundingClientRect() // Keep getBoundingClientRect for iframe\n\t\t\t\t\tnewIframeOffset.x = iframeRect.left\n\t\t\t\t\tnewIframeOffset.y = iframeRect.top\n\t\t\t\t}\n\n\t\t\t\t// Update each overlay\n\t\t\t\toverlays.forEach((overlayData, i) => {\n\t\t\t\t\tif (i < newRects.length) {\n\t\t\t\t\t\t// Check if rect still exists\n\t\t\t\t\t\tconst newRect = newRects[i]\n\t\t\t\t\t\tconst newTop = newRect.top + newIframeOffset.y\n\t\t\t\t\t\tconst newLeft = newRect.left + newIframeOffset.x\n\n\t\t\t\t\t\toverlayData.element.style.top = `${newTop}px`\n\t\t\t\t\t\toverlayData.element.style.left = `${newLeft}px`\n\t\t\t\t\t\toverlayData.element.style.width = `${newRect.width}px`\n\t\t\t\t\t\toverlayData.element.style.height = `${newRect.height}px`\n\t\t\t\t\t\toverlayData.element.style.display =\n\t\t\t\t\t\t\tnewRect.width === 0 || newRect.height === 0 ? 'none' : 'block'\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// If fewer rects now, hide extra overlays\n\t\t\t\t\t\toverlayData.element.style.display = 'none'\n\t\t\t\t\t}\n\t\t\t\t})\n\n\t\t\t\t// If there are fewer new rects than overlays, hide the extras\n\t\t\t\tif (newRects.length < overlays.length) {\n\t\t\t\t\tfor (let i = newRects.length; i < overlays.length; i++) {\n\t\t\t\t\t\toverlays[i].element.style.display = 'none'\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Update label position based on the first new rect\n\t\t\t\tif (label && newRects.length > 0) {\n\t\t\t\t\tconst firstNewRect = newRects[0]\n\t\t\t\t\tconst firstNewRectTop = firstNewRect.top + newIframeOffset.y\n\t\t\t\t\tconst firstNewRectLeft = firstNewRect.left + newIframeOffset.x\n\n\t\t\t\t\tlet newLabelTop = firstNewRectTop + 2\n\t\t\t\t\tlet newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth - 2\n\n\t\t\t\t\tif (firstNewRect.width < labelWidth + 4 || firstNewRect.height < labelHeight + 4) {\n\t\t\t\t\t\tnewLabelTop = firstNewRectTop - labelHeight - 2\n\t\t\t\t\t\tnewLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth\n\t\t\t\t\t\tif (newLabelLeft < newIframeOffset.x) newLabelLeft = firstNewRectLeft\n\t\t\t\t\t}\n\n\t\t\t\t\t// Ensure label stays within viewport bounds\n\t\t\t\t\tnewLabelTop = Math.max(0, Math.min(newLabelTop, window.innerHeight - labelHeight))\n\t\t\t\t\tnewLabelLeft = Math.max(0, Math.min(newLabelLeft, window.innerWidth - labelWidth))\n\n\t\t\t\t\tlabel.style.top = `${newLabelTop}px`\n\t\t\t\t\tlabel.style.left = `${newLabelLeft}px`\n\t\t\t\t\tlabel.style.display = 'block'\n\t\t\t\t} else if (label) {\n\t\t\t\t\t// Hide label if element has no rects anymore\n\t\t\t\t\tlabel.style.display = 'none'\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst throttleFunction = (func, delay) => {\n\t\t\t\tlet lastCall = 0\n\t\t\t\treturn (...args) => {\n\t\t\t\t\tconst now = performance.now()\n\t\t\t\t\tif (now - lastCall < delay) return\n\t\t\t\t\tlastCall = now\n\t\t\t\t\treturn func(...args)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst throttledUpdatePositions = throttleFunction(updatePositions, 16) // ~60fps\n\t\t\twindow.addEventListener('scroll', throttledUpdatePositions, true)\n\t\t\twindow.addEventListener('resize', throttledUpdatePositions)\n\n\t\t\t// Add cleanup function\n\t\t\tcleanupFn = () => {\n\t\t\t\twindow.removeEventListener('scroll', throttledUpdatePositions, true)\n\t\t\t\twindow.removeEventListener('resize', throttledUpdatePositions)\n\t\t\t\t// Remove overlay elements if needed\n\t\t\t\toverlays.forEach((overlay) => overlay.element.remove())\n\t\t\t\tif (label) label.remove()\n\t\t\t}\n\n\t\t\t// Then add fragment to container in one operation\n\t\t\tcontainer.appendChild(fragment)\n\n\t\t\treturn index + 1\n\t\t} finally {\n\t\t\t// Store cleanup function for later use\n\t\t\tif (cleanupFn) {\n\t\t\t\t// Keep a reference to cleanup functions in a global array\n\t\t\t\t;(window._highlightCleanupFunctions = window._highlightCleanupFunctions || []).push(\n\t\t\t\t\tcleanupFn\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\t// // Add this function to perform cleanup when needed\n\t// function cleanupHighlights() {\n\t//   if (window._highlightCleanupFunctions && window._highlightCleanupFunctions.length) {\n\t//     window._highlightCleanupFunctions.forEach(fn => fn());\n\t//     window._highlightCleanupFunctions = [];\n\t//   }\n\n\t//   // Also remove the container\n\t//   const container = document.getElementById(HIGHLIGHT_CONTAINER_ID);\n\t//   if (container) container.remove();\n\t// }\n\n\t/**\n\t * Gets the position of an element in its parent.\n\t *\n\t * @param {HTMLElement} currentElement - The element to get the position for.\n\t * @returns {number} The position of the element in its parent.\n\t */\n\tfunction getElementPosition(currentElement) {\n\t\tif (!currentElement.parentElement) {\n\t\t\treturn 0 // No parent means no siblings\n\t\t}\n\n\t\tconst tagName = currentElement.nodeName.toLowerCase()\n\n\t\tconst siblings = Array.from(currentElement.parentElement.children).filter(\n\t\t\t(sib) => sib.nodeName.toLowerCase() === tagName\n\t\t)\n\n\t\tif (siblings.length === 1) {\n\t\t\treturn 0 // Only element of its type\n\t\t}\n\n\t\tconst index = siblings.indexOf(currentElement) + 1 // 1-based index\n\t\treturn index\n\t}\n\n\tfunction getXPathTree(element, stopAtBoundary = true) {\n\t\tif (xpathCache.has(element)) return xpathCache.get(element)\n\n\t\tconst segments = []\n\t\tlet currentElement = element\n\n\t\twhile (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {\n\t\t\t// Stop if we hit a shadow root or iframe\n\t\t\tif (\n\t\t\t\tstopAtBoundary &&\n\t\t\t\t(currentElement.parentNode instanceof ShadowRoot ||\n\t\t\t\t\tcurrentElement.parentNode instanceof HTMLIFrameElement)\n\t\t\t) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tconst position = getElementPosition(currentElement)\n\t\t\tconst tagName = currentElement.nodeName.toLowerCase()\n\t\t\tconst xpathIndex = position > 0 ? `[${position}]` : ''\n\t\t\tsegments.unshift(`${tagName}${xpathIndex}`)\n\n\t\t\tcurrentElement = currentElement.parentNode\n\t\t}\n\n\t\tconst result = segments.join('/')\n\t\txpathCache.set(element, result)\n\t\treturn result\n\t}\n\n\t/**\n\t * @edit scrollable element detection\n\t * Checks if an element is scrollable. if so, return the scrollable distance on each direction (left right top bottom). if not return null.\n\t * @note distance smaller than 4 will be considered as not scrollable.\n\t * @note only check block elements, not inline elements.\n\t */\n\tfunction isScrollableElement(element) {\n\t\tif (!element || element.nodeType !== Node.ELEMENT_NODE) {\n\t\t\treturn null // Not a valid element\n\t\t}\n\n\t\tconst style = getCachedComputedStyle(element)\n\t\tif (!style) return null\n\n\t\t// Check if the element is a block-level element\n\t\tconst display = style.display\n\t\tif (display === 'inline' || display === 'inline-block') {\n\t\t\treturn null // Not a block-level element\n\t\t}\n\n\t\t// Check overflow properties\n\t\tconst overflowX = style.overflowX\n\t\tconst overflowY = style.overflowY\n\n\t\t// Check scrollable distances\n\t\tconst scrollableX = overflowX === 'auto' || overflowX === 'scroll'\n\t\tconst scrollableY = overflowY === 'auto' || overflowY === 'scroll'\n\n\t\tif (!scrollableX && !scrollableY) {\n\t\t\treturn null // Not scrollable in any direction\n\t\t}\n\n\t\tconst scrollWidth = element.scrollWidth - element.clientWidth\n\t\tconst scrollHeight = element.scrollHeight - element.clientHeight\n\n\t\t// Consider small distances as not scrollable\n\t\tconst threshold = 4\n\n\t\tif (scrollWidth < threshold && scrollHeight < threshold) {\n\t\t\treturn null // Not scrollable\n\t\t}\n\n\t\tif (!scrollableY && scrollWidth < threshold) {\n\t\t\treturn null // Not scrollable horizontally\n\t\t}\n\n\t\tif (!scrollableX && scrollHeight < threshold) {\n\t\t\treturn null // Not scrollable vertically\n\t\t}\n\n\t\tconst distanceToTop = element.scrollTop\n\t\tconst distanceToLeft = element.scrollLeft\n\t\tconst distanceToRight = element.scrollWidth - element.clientWidth - element.scrollLeft\n\t\tconst distanceToBottom = element.scrollHeight - element.clientHeight - element.scrollTop\n\n\t\tconst scrollData = {\n\t\t\ttop: distanceToTop,\n\t\t\tright: distanceToRight,\n\t\t\tbottom: distanceToBottom,\n\t\t\tleft: distanceToLeft,\n\t\t}\n\n\t\t// Store extra data for the element\n\t\taddExtraData(element, {\n\t\t\tscrollable: true,\n\t\t\tscrollData: scrollData,\n\t\t})\n\n\t\treturn scrollData\n\t}\n\n\t/**\n\t * Checks if a text node is visible.\n\t *\n\t * @param {Text} textNode - The text node to check.\n\t * @returns {boolean} Whether the text node is visible.\n\t */\n\tfunction isTextNodeVisible(textNode) {\n\t\ttry {\n\t\t\t// Special case: when viewportExpansion is -1, consider all text nodes as visible\n\t\t\tif (viewportExpansion === -1) {\n\t\t\t\t// Still check parent visibility for basic filtering\n\t\t\t\tconst parentElement = textNode.parentElement\n\t\t\t\tif (!parentElement) return false\n\n\t\t\t\ttry {\n\t\t\t\t\treturn parentElement.checkVisibility({\n\t\t\t\t\t\tcheckOpacity: true,\n\t\t\t\t\t\tcheckVisibilityCSS: true,\n\t\t\t\t\t})\n\t\t\t\t} catch (e) {\n\t\t\t\t\t// Fallback if checkVisibility is not supported\n\t\t\t\t\tconst style = window.getComputedStyle(parentElement)\n\t\t\t\t\treturn style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst range = document.createRange()\n\t\t\trange.selectNodeContents(textNode)\n\t\t\tconst rects = range.getClientRects() // Use getClientRects for Range\n\n\t\t\tif (!rects || rects.length === 0) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tlet isAnyRectVisible = false\n\t\t\tlet isAnyRectInViewport = false\n\n\t\t\tfor (const rect of rects) {\n\t\t\t\t// Check size\n\t\t\t\tif (rect.width > 0 && rect.height > 0) {\n\t\t\t\t\tisAnyRectVisible = true\n\n\t\t\t\t\t// Viewport check for this rect\n\t\t\t\t\tif (\n\t\t\t\t\t\t!(\n\t\t\t\t\t\t\trect.bottom < -viewportExpansion ||\n\t\t\t\t\t\t\trect.top > window.innerHeight + viewportExpansion ||\n\t\t\t\t\t\t\trect.right < -viewportExpansion ||\n\t\t\t\t\t\t\trect.left > window.innerWidth + viewportExpansion\n\t\t\t\t\t\t)\n\t\t\t\t\t) {\n\t\t\t\t\t\tisAnyRectInViewport = true\n\t\t\t\t\t\tbreak // Found a visible rect in viewport, no need to check others\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!isAnyRectVisible || !isAnyRectInViewport) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\t// Check parent visibility\n\t\t\tconst parentElement = textNode.parentElement\n\t\t\tif (!parentElement) return false\n\n\t\t\ttry {\n\t\t\t\treturn parentElement.checkVisibility({\n\t\t\t\t\tcheckOpacity: true,\n\t\t\t\t\tcheckVisibilityCSS: true,\n\t\t\t\t})\n\t\t\t} catch (e) {\n\t\t\t\t// Fallback if checkVisibility is not supported\n\t\t\t\tconst style = window.getComputedStyle(parentElement)\n\t\t\t\treturn style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tconsole.warn('Error checking text node visibility:', e)\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * Checks if an element is accepted.\n\t *\n\t * @param {HTMLElement} element - The element to check.\n\t * @returns {boolean} Whether the element is accepted.\n\t */\n\tfunction isElementAccepted(element) {\n\t\tif (!element || !element.tagName) return false\n\n\t\t// Always accept body and common container elements\n\t\tconst alwaysAccept = new Set([\n\t\t\t'body',\n\t\t\t'div',\n\t\t\t'main',\n\t\t\t'article',\n\t\t\t'section',\n\t\t\t'nav',\n\t\t\t'header',\n\t\t\t'footer',\n\t\t])\n\t\tconst tagName = element.tagName.toLowerCase()\n\n\t\tif (alwaysAccept.has(tagName)) return true\n\n\t\tconst leafElementDenyList = new Set([\n\t\t\t'svg',\n\t\t\t'script',\n\t\t\t'style',\n\t\t\t'link',\n\t\t\t'meta',\n\t\t\t'noscript',\n\t\t\t'template',\n\t\t])\n\n\t\treturn !leafElementDenyList.has(tagName)\n\t}\n\n\t/**\n\t * Checks if an element is visible.\n\t *\n\t * @param {HTMLElement} element - The element to check.\n\t * @returns {boolean} Whether the element is visible.\n\t */\n\tfunction isElementVisible(element) {\n\t\tconst style = getCachedComputedStyle(element)\n\t\treturn (\n\t\t\telement.offsetWidth > 0 &&\n\t\t\telement.offsetHeight > 0 &&\n\t\t\tstyle?.visibility !== 'hidden' &&\n\t\t\tstyle?.display !== 'none'\n\t\t)\n\t}\n\n\t/**\n\t * Checks if an element is interactive.\n\t *\n\t * lots of comments, and uncommented code - to show the logic of what we already tried\n\t *\n\t * One of the things we tried at the beginning was also to use event listeners, and other fancy class, style stuff -> what actually worked best was just combining most things with computed cursor style :)\n\t *\n\t * @param {HTMLElement} element - The element to check.\n\t */\n\tfunction isInteractiveElement(element) {\n\t\tif (!element || element.nodeType !== Node.ELEMENT_NODE) {\n\t\t\treturn false\n\t\t}\n\n\t\t/**\n\t\t * @edit add interactiveBlacklist interactiveWhitelist\n\t\t */\n\t\tif (interactiveBlacklist.includes(element)) {\n\t\t\treturn false // Skip blacklisted elements\n\t\t}\n\t\tif (interactiveWhitelist.includes(element)) {\n\t\t\treturn true // Skip whitelisted elements\n\t\t}\n\n\t\t// Cache the tagName and style lookups\n\t\tconst tagName = element.tagName.toLowerCase()\n\t\tconst style = getCachedComputedStyle(element)\n\n\t\t// Define interactive cursors\n\t\tconst interactiveCursors = new Set([\n\t\t\t'pointer', // Link/clickable elements\n\t\t\t'move', // Movable elements\n\t\t\t'text', // Text selection\n\t\t\t'grab', // Grabbable elements\n\t\t\t'grabbing', // Currently grabbing\n\t\t\t'cell', // Table cell selection\n\t\t\t'copy', // Copy operation\n\t\t\t'alias', // Alias creation\n\t\t\t'all-scroll', // Scrollable content\n\t\t\t'col-resize', // Column resize\n\t\t\t'context-menu', // Context menu available\n\t\t\t'crosshair', // Precise selection\n\t\t\t'e-resize', // East resize\n\t\t\t'ew-resize', // East-west resize\n\t\t\t'help', // Help available\n\t\t\t'n-resize', // North resize\n\t\t\t'ne-resize', // Northeast resize\n\t\t\t'nesw-resize', // Northeast-southwest resize\n\t\t\t'ns-resize', // North-south resize\n\t\t\t'nw-resize', // Northwest resize\n\t\t\t'nwse-resize', // Northwest-southeast resize\n\t\t\t'row-resize', // Row resize\n\t\t\t's-resize', // South resize\n\t\t\t'se-resize', // Southeast resize\n\t\t\t'sw-resize', // Southwest resize\n\t\t\t'vertical-text', // Vertical text selection\n\t\t\t'w-resize', // West resize\n\t\t\t'zoom-in', // Zoom in\n\t\t\t'zoom-out', // Zoom out\n\t\t])\n\n\t\t// Define non-interactive cursors\n\t\tconst nonInteractiveCursors = new Set([\n\t\t\t'not-allowed', // Action not allowed\n\t\t\t'no-drop', // Drop not allowed\n\t\t\t'wait', // Processing\n\t\t\t'progress', // In progress\n\t\t\t'initial', // Initial value\n\t\t\t'inherit', // Inherited value\n\t\t\t//? Let's just include all potentially clickable elements that are not specifically blocked\n\t\t\t// 'none',        // No cursor\n\t\t\t// 'default',     // Default cursor\n\t\t\t// 'auto',        // Browser default\n\t\t])\n\n\t\t/**\n\t\t * Checks if an element has an interactive pointer.\n\t\t *\n\t\t * @param {HTMLElement} element - The element to check.\n\t\t * @returns {boolean} Whether the element has an interactive pointer.\n\t\t */\n\t\tfunction doesElementHaveInteractivePointer(element) {\n\t\t\tif (element.tagName.toLowerCase() === 'html') return false\n\n\t\t\tif (style?.cursor && interactiveCursors.has(style.cursor)) return true\n\n\t\t\treturn false\n\t\t}\n\n\t\tlet isInteractiveCursor = doesElementHaveInteractivePointer(element)\n\n\t\t// Genius fix for almost all interactive elements\n\t\tif (isInteractiveCursor) {\n\t\t\treturn true\n\t\t}\n\n\t\tconst interactiveElements = new Set([\n\t\t\t'a', // Links\n\t\t\t'button', // Buttons\n\t\t\t'input', // All input types (text, checkbox, radio, etc.)\n\t\t\t'select', // Dropdown menus\n\t\t\t'textarea', // Text areas\n\t\t\t'details', // Expandable details\n\t\t\t'summary', // Summary element (clickable part of details)\n\t\t\t'label', // Form labels (often clickable)\n\t\t\t'option', // Select options\n\t\t\t'optgroup', // Option groups\n\t\t\t'fieldset', // Form fieldsets (can be interactive with legend)\n\t\t\t'legend', // Fieldset legends\n\t\t])\n\n\t\t// Define explicit disable attributes and properties\n\t\tconst explicitDisableTags = new Set([\n\t\t\t'disabled', // Standard disabled attribute\n\t\t\t// 'aria-disabled',      // ARIA disabled state\n\t\t\t'readonly', // Read-only state\n\t\t\t// 'aria-readonly',     // ARIA read-only state\n\t\t\t// 'aria-hidden',       // Hidden from accessibility\n\t\t\t// 'hidden',            // Hidden attribute\n\t\t\t// 'inert',             // Inert attribute\n\t\t\t// 'aria-inert',        // ARIA inert state\n\t\t\t// 'tabindex=\"-1\"',     // Removed from tab order\n\t\t\t// 'aria-hidden=\"true\"' // Hidden from screen readers\n\t\t])\n\n\t\t// handle inputs, select, checkbox, radio, textarea, button and make sure they are not cursor style disabled/not-allowed\n\t\tif (interactiveElements.has(tagName)) {\n\t\t\t// Check for non-interactive cursor\n\t\t\tif (style?.cursor && nonInteractiveCursors.has(style.cursor)) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\t// Check for explicit disable attributes\n\t\t\tfor (const disableTag of explicitDisableTags) {\n\t\t\t\tif (\n\t\t\t\t\telement.hasAttribute(disableTag) ||\n\t\t\t\t\telement.getAttribute(disableTag) === 'true' ||\n\t\t\t\t\telement.getAttribute(disableTag) === ''\n\t\t\t\t) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check for disabled property on form elements\n\t\t\tif (element.disabled) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\t// Check for readonly property on form elements\n\t\t\tif (element.readOnly) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\t// Check for inert property\n\t\t\tif (element.inert) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\treturn true\n\t\t}\n\n\t\tconst role = element.getAttribute('role')\n\t\tconst ariaRole = element.getAttribute('aria-role')\n\n\t\t// Check for contenteditable attribute\n\t\tif (element.getAttribute('contenteditable') === 'true' || element.isContentEditable) {\n\t\t\treturn true\n\t\t}\n\n\t\t// Added enhancement to capture dropdown interactive elements\n\t\tif (\n\t\t\telement.classList &&\n\t\t\t(element.classList.contains('button') ||\n\t\t\t\telement.classList.contains('dropdown-toggle') ||\n\t\t\t\telement.getAttribute('data-index') ||\n\t\t\t\telement.getAttribute('data-toggle') === 'dropdown' ||\n\t\t\t\telement.getAttribute('aria-haspopup') === 'true')\n\t\t) {\n\t\t\treturn true\n\t\t}\n\n\t\tconst interactiveRoles = new Set([\n\t\t\t'button', // Directly clickable element\n\t\t\t// 'link',            // Clickable link\n\t\t\t'menu', // Menu container (ARIA menus)\n\t\t\t'menubar', // Menu bar container\n\t\t\t'menuitem', // Clickable menu item\n\t\t\t'menuitemradio', // Radio-style menu item (selectable)\n\t\t\t'menuitemcheckbox', // Checkbox-style menu item (toggleable)\n\t\t\t'radio', // Radio button (selectable)\n\t\t\t'checkbox', // Checkbox (toggleable)\n\t\t\t'tab', // Tab (clickable to switch content)\n\t\t\t'switch', // Toggle switch (clickable to change state)\n\t\t\t'slider', // Slider control (draggable)\n\t\t\t'spinbutton', // Number input with up/down controls\n\t\t\t'combobox', // Dropdown with text input\n\t\t\t'searchbox', // Search input field\n\t\t\t'textbox', // Text input field\n\t\t\t'listbox', // Selectable list\n\t\t\t'option', // Selectable option in a list\n\t\t\t'scrollbar', // Scrollable control\n\t\t])\n\n\t\t// Basic role/attribute checks\n\t\tconst hasInteractiveRole =\n\t\t\tinteractiveElements.has(tagName) ||\n\t\t\t(role && interactiveRoles.has(role)) ||\n\t\t\t(ariaRole && interactiveRoles.has(ariaRole))\n\n\t\tif (hasInteractiveRole) return true\n\n\t\t// check whether element has event listeners by window.getEventListeners\n\t\ttry {\n\t\t\tif (typeof getEventListeners === 'function') {\n\t\t\t\tconst listeners = getEventListeners(element)\n\t\t\t\tconst mouseEvents = ['click', 'mousedown', 'mouseup', 'dblclick']\n\t\t\t\tfor (const eventType of mouseEvents) {\n\t\t\t\t\tif (listeners[eventType] && listeners[eventType].length > 0) {\n\t\t\t\t\t\treturn true // Found a mouse interaction listener\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst getEventListenersForNode =\n\t\t\t\telement?.ownerDocument?.defaultView?.getEventListenersForNode ||\n\t\t\t\twindow.getEventListenersForNode\n\t\t\tif (typeof getEventListenersForNode === 'function') {\n\t\t\t\tconst listeners = getEventListenersForNode(element)\n\t\t\t\tconst interactionEvents = [\n\t\t\t\t\t'click',\n\t\t\t\t\t'mousedown',\n\t\t\t\t\t'mouseup',\n\t\t\t\t\t'keydown',\n\t\t\t\t\t'keyup',\n\t\t\t\t\t'submit',\n\t\t\t\t\t'change',\n\t\t\t\t\t'input',\n\t\t\t\t\t'focus',\n\t\t\t\t\t'blur',\n\t\t\t\t]\n\t\t\t\tfor (const eventType of interactionEvents) {\n\t\t\t\t\tfor (const listener of listeners) {\n\t\t\t\t\t\tif (listener.type === eventType) {\n\t\t\t\t\t\t\treturn true // Found a common interaction listener\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Fallback: Check common event attributes if getEventListeners is not available (getEventListeners doesn't work in page.evaluate context)\n\t\t\tconst commonMouseAttrs = ['onclick', 'onmousedown', 'onmouseup', 'ondblclick']\n\t\t\tfor (const attr of commonMouseAttrs) {\n\t\t\t\tif (element.hasAttribute(attr) || typeof element[attr] === 'function') {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (e) {\n\t\t\t// console.warn(`Could not check event listeners for ${element.tagName}:`, e);\n\t\t\t// If checking listeners fails, rely on other checks\n\t\t}\n\n\t\t/**\n\t\t * @edit scrollable element detection\n\t\t */\n\t\tif (isScrollableElement(element)) {\n\t\t\treturn true\n\t\t}\n\n\t\treturn false\n\t}\n\n\t/**\n\t * Checks if an element is the topmost element at its position.\n\t *\n\t * @param {HTMLElement} element - The element to check.\n\t * @returns {boolean} Whether the element is the topmost element at its position.\n\t */\n\tfunction isTopElement(element) {\n\t\t// Special case: when viewportExpansion is -1, consider all elements as \"top\" elements\n\t\tif (viewportExpansion === -1) {\n\t\t\treturn true\n\t\t}\n\n\t\tconst rects = getCachedClientRects(element) // Replace element.getClientRects()\n\n\t\tif (!rects || rects.length === 0) {\n\t\t\treturn false // No geometry, cannot be top\n\t\t}\n\n\t\tlet isAnyRectInViewport = false\n\t\tfor (const rect of rects) {\n\t\t\t// Use the same logic as isInExpandedViewport check\n\t\t\tif (\n\t\t\t\trect.width > 0 &&\n\t\t\t\trect.height > 0 &&\n\t\t\t\t!(\n\t\t\t\t\t// Only check non-empty rects\n\t\t\t\t\t(\n\t\t\t\t\t\trect.bottom < -viewportExpansion ||\n\t\t\t\t\t\trect.top > window.innerHeight + viewportExpansion ||\n\t\t\t\t\t\trect.right < -viewportExpansion ||\n\t\t\t\t\t\trect.left > window.innerWidth + viewportExpansion\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tisAnyRectInViewport = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif (!isAnyRectInViewport) {\n\t\t\treturn false // All rects are outside the viewport area\n\t\t}\n\n\t\t// Find the correct document context and root element\n\t\tlet doc = element.ownerDocument\n\n\t\t// If we're in an iframe, elements are considered top by default\n\t\tif (doc !== window.document) {\n\t\t\treturn true\n\t\t}\n\n\t\t/**\n\t\t * @edit improve `sampleRect`, filter out rects with 0 area\n\t\t */\n\t\t// find a rect that has width and height as sample\n\t\tlet rect = Array.from(rects).find((r) => r.width > 0 && r.height > 0)\n\t\tif (!rect) {\n\t\t\treturn false // No valid rect found\n\t\t}\n\n\t\t// For shadow DOM, we need to check within its own root context\n\t\tconst shadowRoot = element.getRootNode()\n\t\tif (shadowRoot instanceof ShadowRoot) {\n\t\t\tconst centerX = rect.left + rect.width / 2\n\t\t\tconst centerY = rect.top + rect.height / 2\n\n\t\t\ttry {\n\t\t\t\tconst topEl = shadowRoot.elementFromPoint(centerX, centerY)\n\t\t\t\tif (!topEl) return false\n\n\t\t\t\tlet current = topEl\n\t\t\t\twhile (current && current !== shadowRoot) {\n\t\t\t\t\tif (current === element) return true\n\t\t\t\t\tcurrent = current.parentElement\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t} catch (e) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\tconst margin = 5\n\n\t\t// For elements in viewport, check if they're topmost. Do the check in the\n\t\t// center of the element and at the corners to ensure we catch more cases.\n\t\tconst checkPoints = [\n\t\t\t// Initially only this was used, but it was not enough\n\t\t\t{ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },\n\t\t\t{ x: rect.left + margin, y: rect.top + margin }, // top left\n\t\t\t// { x: rect.right - margin, y: rect.top + margin },    // top right\n\t\t\t// { x: rect.left + margin, y: rect.bottom - margin },  // bottom left\n\t\t\t{ x: rect.right - margin, y: rect.bottom - margin }, // bottom right\n\t\t]\n\n\t\treturn checkPoints.some(({ x, y }) => {\n\t\t\ttry {\n\t\t\t\tconst topEl = document.elementFromPoint(x, y)\n\t\t\t\tif (!topEl) return false\n\n\t\t\t\tlet current = topEl\n\t\t\t\twhile (current && current !== document.documentElement) {\n\t\t\t\t\tif (current === element) return true\n\t\t\t\t\tcurrent = current.parentElement\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t} catch (e) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Checks if an element is within the expanded viewport.\n\t *\n\t * @param {HTMLElement} element - The element to check.\n\t * @param {number} viewportExpansion - The viewport expansion.\n\t * @returns {boolean} Whether the element is within the expanded viewport.\n\t */\n\tfunction isInExpandedViewport(element, viewportExpansion) {\n\t\tif (viewportExpansion === -1) {\n\t\t\treturn true\n\t\t}\n\n\t\tconst rects = element.getClientRects() // Use getClientRects\n\n\t\tif (!rects || rects.length === 0) {\n\t\t\t// Fallback to getBoundingClientRect if getClientRects is empty,\n\t\t\t// useful for elements like <svg> that might not have client rects but have a bounding box.\n\t\t\tconst boundingRect = getCachedBoundingRect(element)\n\t\t\tif (!boundingRect || boundingRect.width === 0 || boundingRect.height === 0) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn !(\n\t\t\t\tboundingRect.bottom < -viewportExpansion ||\n\t\t\t\tboundingRect.top > window.innerHeight + viewportExpansion ||\n\t\t\t\tboundingRect.right < -viewportExpansion ||\n\t\t\t\tboundingRect.left > window.innerWidth + viewportExpansion\n\t\t\t)\n\t\t}\n\n\t\t// Check if *any* client rect is within the viewport\n\t\tfor (const rect of rects) {\n\t\t\tif (rect.width === 0 || rect.height === 0) continue // Skip empty rects\n\n\t\t\tif (\n\t\t\t\t!(\n\t\t\t\t\trect.bottom < -viewportExpansion ||\n\t\t\t\t\trect.top > window.innerHeight + viewportExpansion ||\n\t\t\t\t\trect.right < -viewportExpansion ||\n\t\t\t\t\trect.left > window.innerWidth + viewportExpansion\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn true // Found at least one rect in the viewport\n\t\t\t}\n\t\t}\n\n\t\treturn false // No rects were found in the viewport\n\t}\n\n\t// /**\n\t//  * Gets the effective scroll of an element.\n\t//  *\n\t//  * @param {HTMLElement} element - The element to get the effective scroll for.\n\t//  * @returns {Object} The effective scroll of the element.\n\t//  */\n\t// function getEffectiveScroll(element) {\n\t//   let currentEl = element;\n\t//   let scrollX = 0;\n\t//   let scrollY = 0;\n\n\t//   while (currentEl && currentEl !== document.documentElement) {\n\t//     if (currentEl.scrollLeft || currentEl.scrollTop) {\n\t//       scrollX += currentEl.scrollLeft;\n\t//       scrollY += currentEl.scrollTop;\n\t//     }\n\t//     currentEl = currentEl.parentElement;\n\t//   }\n\n\t//   scrollX += window.scrollX;\n\t//   scrollY += window.scrollY;\n\n\t//   return { scrollX, scrollY };\n\t// }\n\n\t/**\n\t * Checks if an element is an interactive candidate.\n\t *\n\t * @param {HTMLElement} element - The element to check.\n\t * @returns {boolean} Whether the element is an interactive candidate.\n\t */\n\tfunction isInteractiveCandidate(element) {\n\t\tif (!element || element.nodeType !== Node.ELEMENT_NODE) return false\n\n\t\tconst tagName = element.tagName.toLowerCase()\n\n\t\t// Fast-path for common interactive elements\n\t\tconst interactiveElements = new Set([\n\t\t\t'a',\n\t\t\t'button',\n\t\t\t'input',\n\t\t\t'select',\n\t\t\t'textarea',\n\t\t\t'details',\n\t\t\t'summary',\n\t\t\t'label',\n\t\t])\n\n\t\tif (interactiveElements.has(tagName)) return true\n\n\t\t// Quick attribute checks without getting full lists\n\t\tconst hasQuickInteractiveAttr =\n\t\t\telement.hasAttribute('onclick') ||\n\t\t\telement.hasAttribute('role') ||\n\t\t\telement.hasAttribute('tabindex') ||\n\t\t\telement.hasAttribute('aria-') ||\n\t\t\telement.hasAttribute('data-action') ||\n\t\t\telement.getAttribute('contenteditable') === 'true'\n\n\t\treturn hasQuickInteractiveAttr\n\t}\n\n\t// --- Define constants for distinct interaction check ---\n\tconst DISTINCT_INTERACTIVE_TAGS = new Set([\n\t\t'a',\n\t\t'button',\n\t\t'input',\n\t\t'select',\n\t\t'textarea',\n\t\t'summary',\n\t\t'details',\n\t\t'label',\n\t\t'option',\n\t])\n\tconst INTERACTIVE_ROLES = new Set([\n\t\t'button',\n\t\t'link',\n\t\t'menuitem',\n\t\t'menuitemradio',\n\t\t'menuitemcheckbox',\n\t\t'radio',\n\t\t'checkbox',\n\t\t'tab',\n\t\t'switch',\n\t\t'slider',\n\t\t'spinbutton',\n\t\t'combobox',\n\t\t'searchbox',\n\t\t'textbox',\n\t\t'listbox',\n\t\t'option',\n\t\t'scrollbar',\n\t])\n\n\t/**\n\t * Heuristically determines if an element should be considered as independently interactive,\n\t * even if it's nested inside another interactive container.\n\t *\n\t * This function helps detect deeply nested actionable elements (e.g., menu items within a button)\n\t * that may not be picked up by strict interactivity checks.\n\t *\n\t * @param {HTMLElement} element - The element to check.\n\t * @returns {boolean} Whether the element is heuristically interactive.\n\t */\n\tfunction isHeuristicallyInteractive(element) {\n\t\tif (!element || element.nodeType !== Node.ELEMENT_NODE) return false\n\n\t\t// Skip non-visible elements early for performance\n\t\tif (!isElementVisible(element)) return false\n\n\t\t// Check for common attributes that often indicate interactivity\n\t\tconst hasInteractiveAttributes =\n\t\t\telement.hasAttribute('role') ||\n\t\t\telement.hasAttribute('tabindex') ||\n\t\t\telement.hasAttribute('onclick') ||\n\t\t\ttypeof element.onclick === 'function'\n\n\t\t// Check for semantic class names suggesting interactivity\n\t\tconst hasInteractiveClass = /\\b(btn|clickable|menu|item|entry|link)\\b/i.test(\n\t\t\telement.className || ''\n\t\t)\n\n\t\t// Determine whether the element is inside a known interactive container\n\t\tconst isInKnownContainer = Boolean(\n\t\t\telement.closest('button,a,[role=\"button\"],.menu,.dropdown,.list,.toolbar')\n\t\t)\n\n\t\t// Ensure the element has at least one visible child (to avoid marking empty wrappers)\n\t\tconst hasVisibleChildren = [...element.children].some(isElementVisible)\n\n\t\t// Avoid highlighting elements whose parent is <body> (top-level wrappers)\n\t\tconst isParentBody = element.parentElement && element.parentElement.isSameNode(document.body)\n\n\t\treturn (\n\t\t\t(isInteractiveElement(element) || hasInteractiveAttributes || hasInteractiveClass) &&\n\t\t\thasVisibleChildren &&\n\t\t\tisInKnownContainer &&\n\t\t\t!isParentBody\n\t\t)\n\t}\n\n\t/**\n\t * Checks if an element likely represents a distinct interaction\n\t * separate from its parent (if the parent is also interactive).\n\t *\n\t * @param {HTMLElement} element - The element to check.\n\t * @returns {boolean} Whether the element is a distinct interaction.\n\t */\n\tfunction isElementDistinctInteraction(element) {\n\t\tif (!element || element.nodeType !== Node.ELEMENT_NODE) {\n\t\t\treturn false\n\t\t}\n\n\t\tconst tagName = element.tagName.toLowerCase()\n\t\tconst role = element.getAttribute('role')\n\n\t\t// Check if it's an iframe - always distinct boundary\n\t\tif (tagName === 'iframe') {\n\t\t\treturn true\n\t\t}\n\n\t\t// Check tag name\n\t\tif (DISTINCT_INTERACTIVE_TAGS.has(tagName)) {\n\t\t\treturn true\n\t\t}\n\t\t// Check interactive roles\n\t\tif (role && INTERACTIVE_ROLES.has(role)) {\n\t\t\treturn true\n\t\t}\n\t\t// Check contenteditable\n\t\tif (element.isContentEditable || element.getAttribute('contenteditable') === 'true') {\n\t\t\treturn true\n\t\t}\n\t\t// Check for common testing/automation attributes\n\t\tif (\n\t\t\telement.hasAttribute('data-testid') ||\n\t\t\telement.hasAttribute('data-cy') ||\n\t\t\telement.hasAttribute('data-test')\n\t\t) {\n\t\t\treturn true\n\t\t}\n\t\t// Check for explicit onclick handler (attribute or property)\n\t\tif (element.hasAttribute('onclick') || typeof element.onclick === 'function') {\n\t\t\treturn true\n\t\t}\n\n\t\t// return false\n\n\t\t// Check for other common interaction event listeners\n\t\ttry {\n\t\t\tconst getEventListenersForNode =\n\t\t\t\telement?.ownerDocument?.defaultView?.getEventListenersForNode ||\n\t\t\t\twindow.getEventListenersForNode\n\t\t\tif (typeof getEventListenersForNode === 'function') {\n\t\t\t\tconst listeners = getEventListenersForNode(element)\n\t\t\t\tconst interactionEvents = [\n\t\t\t\t\t'click',\n\t\t\t\t\t'mousedown',\n\t\t\t\t\t'mouseup',\n\t\t\t\t\t'keydown',\n\t\t\t\t\t'keyup',\n\t\t\t\t\t'submit',\n\t\t\t\t\t'change',\n\t\t\t\t\t'input',\n\t\t\t\t\t'focus',\n\t\t\t\t\t'blur',\n\t\t\t\t]\n\t\t\t\tfor (const eventType of interactionEvents) {\n\t\t\t\t\tfor (const listener of listeners) {\n\t\t\t\t\t\tif (listener.type === eventType) {\n\t\t\t\t\t\t\treturn true // Found a common interaction listener\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Fallback: Check common event attributes if getEventListeners is not available (getEventListenersForNode doesn't work in page.evaluate context)\n\t\t\tconst commonEventAttrs = [\n\t\t\t\t'onmousedown',\n\t\t\t\t'onmouseup',\n\t\t\t\t'onkeydown',\n\t\t\t\t'onkeyup',\n\t\t\t\t'onsubmit',\n\t\t\t\t'onchange',\n\t\t\t\t'oninput',\n\t\t\t\t'onfocus',\n\t\t\t\t'onblur',\n\t\t\t]\n\t\t\tif (commonEventAttrs.some((attr) => element.hasAttribute(attr))) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t} catch (e) {\n\t\t\t// console.warn(`Could not check event listeners for ${element.tagName}:`, e);\n\t\t\t// If checking listeners fails, rely on other checks\n\t\t}\n\n\t\t// if the element is not strictly interactive but appears clickable based on heuristic signals\n\t\tif (isHeuristicallyInteractive(element)) {\n\t\t\treturn true\n\t\t}\n\n\t\t// Default to false: if it's interactive but doesn't match above,\n\t\t// assume it triggers the same action as the parent.\n\t\treturn false\n\t}\n\t// --- End distinct interaction check ---\n\n\t/**\n   * Handles the logic for deciding whether to highlight an element and performing the highlight.\n   * @param {\n    {\n        tagName: string;\n        attributes: Record<string, string>;\n        xpath: any;\n        children: never[];\n        isVisible?: boolean;\n        isTopElement?: boolean;\n        isInteractive?: boolean;\n        isInViewport?: boolean;\n        highlightIndex?: number;\n        shadowRoot?: boolean;\n   }} nodeData - The node data object.\n   * @param {HTMLElement} node - The node to highlight.\n   * @param {HTMLElement | null} parentIframe - The parent iframe node.\n   * @param {boolean} isParentHighlighted - Whether the parent node is highlighted.\n   * @returns {boolean} Whether the element was highlighted.\n   */\n\tfunction handleHighlighting(nodeData, node, parentIframe, isParentHighlighted) {\n\t\tif (!nodeData.isInteractive) return false // Not interactive, definitely don't highlight\n\n\t\tlet shouldHighlight = false\n\t\tif (!isParentHighlighted) {\n\t\t\t// Parent wasn't highlighted, this interactive node can be highlighted.\n\t\t\tshouldHighlight = true\n\t\t} else {\n\t\t\t// Parent *was* highlighted. Only highlight this node if it represents a distinct interaction.\n\t\t\tif (isElementDistinctInteraction(node)) {\n\t\t\t\tshouldHighlight = true\n\t\t\t} else {\n\t\t\t\t// console.log(`Skipping highlight for ${nodeData.tagName} (parent highlighted)`);\n\t\t\t\tshouldHighlight = false\n\t\t\t}\n\t\t}\n\n\t\tif (shouldHighlight) {\n\t\t\t// Check viewport status before assigning index and highlighting\n\t\t\tnodeData.isInViewport = isInExpandedViewport(node, viewportExpansion)\n\n\t\t\t// When viewportExpansion is -1, all interactive elements should get a highlight index\n\t\t\t// regardless of viewport status\n\t\t\tif (nodeData.isInViewport || viewportExpansion === -1) {\n\t\t\t\tnodeData.highlightIndex = highlightIndex++\n\n\t\t\t\tif (doHighlightElements) {\n\t\t\t\t\tif (focusHighlightIndex >= 0) {\n\t\t\t\t\t\tif (focusHighlightIndex === nodeData.highlightIndex) {\n\t\t\t\t\t\t\thighlightElement(node, nodeData.highlightIndex, parentIframe)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\thighlightElement(node, nodeData.highlightIndex, parentIframe)\n\t\t\t\t\t}\n\t\t\t\t\treturn true // Successfully highlighted\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// console.log(`Skipping highlight for ${nodeData.tagName} (outside viewport)`);\n\t\t\t}\n\t\t}\n\n\t\treturn false // Did not highlight\n\t}\n\n\t/**\n\t * Creates a node data object for a given node and its descendants.\n\t *\n\t * @param {HTMLElement} node - The node to process.\n\t * @param {HTMLElement | null} parentIframe - The parent iframe node.\n\t * @param {boolean} isParentHighlighted - Whether the parent node is highlighted.\n\t * @returns {string | null} The ID of the node data object, or null if the node is not processed.\n\t */\n\tfunction buildDomTree(node, parentIframe = null, isParentHighlighted = false) {\n\t\t// Fast rejection checks first\n\t\tif (\n\t\t\t!node ||\n\t\t\tnode.id === HIGHLIGHT_CONTAINER_ID ||\n\t\t\t(node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE)\n\t\t) {\n\t\t\treturn null\n\t\t}\n\n\t\tif (!node || node.id === HIGHLIGHT_CONTAINER_ID) {\n\t\t\treturn null\n\t\t}\n\n\t\t/**\n\t\t * @edit add `data-browser-use-ignore` attribute\n\t\t */\n\t\tif (node.dataset?.browserUseIgnore === 'true' || node.dataset?.pageAgentIgnore === 'true') {\n\t\t\treturn null // Skip this node and its children\n\t\t}\n\n\t\t/**\n\t\t * @edit exclude aria-hidden elements\n\t\t */\n\t\tif (node.getAttribute && node.getAttribute('aria-hidden') === 'true') {\n\t\t\treturn null // Skip this node and its children\n\t\t}\n\n\t\t// Special handling for root node (body)\n\t\tif (node === document.body) {\n\t\t\tconst nodeData = {\n\t\t\t\ttagName: 'body',\n\t\t\t\tattributes: {},\n\t\t\t\txpath: '/body',\n\t\t\t\tchildren: [],\n\t\t\t}\n\n\t\t\t// Process children of body\n\t\t\tfor (const child of node.childNodes) {\n\t\t\t\tconst domElement = buildDomTree(child, parentIframe, false) // Body's children have no highlighted parent initially\n\t\t\t\tif (domElement) nodeData.children.push(domElement)\n\t\t\t}\n\n\t\t\tconst id = `${ID.current++}`\n\t\t\tDOM_HASH_MAP[id] = nodeData\n\t\t\treturn id\n\t\t}\n\n\t\t// Early bailout for non-element nodes except text\n\t\tif (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) {\n\t\t\treturn null\n\t\t}\n\n\t\t// Process text nodes\n\t\tif (node.nodeType === Node.TEXT_NODE) {\n\t\t\tconst textContent = node.textContent?.trim()\n\t\t\tif (!textContent) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\t// Only check visibility for text nodes that might be visible\n\t\t\tconst parentElement = node.parentElement\n\t\t\tif (!parentElement || parentElement.tagName.toLowerCase() === 'script') {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\tconst id = `${ID.current++}`\n\t\t\tDOM_HASH_MAP[id] = {\n\t\t\t\ttype: 'TEXT_NODE',\n\t\t\t\ttext: textContent,\n\t\t\t\tisVisible: isTextNodeVisible(node),\n\t\t\t}\n\t\t\treturn id\n\t\t}\n\n\t\t// Quick checks for element nodes\n\t\tif (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) {\n\t\t\treturn null\n\t\t}\n\n\t\t// Early viewport check - only filter out elements clearly outside viewport\n\t\t// The getBoundingClientRect() of the Shadow DOM host element may return width/height = 0\n\t\tif (viewportExpansion !== -1 && !node.shadowRoot) {\n\t\t\tconst rect = getCachedBoundingRect(node) // Keep for initial quick check\n\t\t\tconst style = getCachedComputedStyle(node)\n\n\t\t\t// Skip viewport check for fixed/sticky elements as they may appear anywhere\n\t\t\tconst isFixedOrSticky = style && (style.position === 'fixed' || style.position === 'sticky')\n\n\t\t\t// Check if element has actual dimensions using offsetWidth/Height (quick check)\n\t\t\tconst hasSize = node.offsetWidth > 0 || node.offsetHeight > 0\n\n\t\t\t// Use getBoundingClientRect for the quick OUTSIDE check.\n\t\t\t// isInExpandedViewport will do the more accurate check later if needed.\n\t\t\tif (\n\t\t\t\t!rect ||\n\t\t\t\t(!isFixedOrSticky &&\n\t\t\t\t\t!hasSize &&\n\t\t\t\t\t(rect.bottom < -viewportExpansion ||\n\t\t\t\t\t\trect.top > window.innerHeight + viewportExpansion ||\n\t\t\t\t\t\trect.right < -viewportExpansion ||\n\t\t\t\t\t\trect.left > window.innerWidth + viewportExpansion))\n\t\t\t) {\n\t\t\t\t// console.log(\"Skipping node outside viewport (quick check):\", node.tagName, rect);\n\t\t\t\treturn null\n\t\t\t}\n\t\t}\n\n\t\t/**\n     * @type {\n      {\n          tagName: string;\n          attributes: Record<string, string | null>;\n          xpath: any;\n          children: never[];\n          isVisible?: boolean;\n          isTopElement?: boolean;\n          isInteractive?: boolean;\n          isInViewport?: boolean;\n          highlightIndex?: number;\n          shadowRoot?: boolean;\n      }\n    } nodeData - The node data object.\n     */\n\t\tconst nodeData = {\n\t\t\ttagName: node.tagName.toLowerCase(),\n\t\t\tattributes: {},\n\n\t\t\t/**\n\t\t\t * @edit no need for xpath\n\t\t\t */\n\t\t\t// xpath: getXPathTree(node, true),\n\n\t\t\tchildren: [],\n\t\t}\n\n\t\t// Get attributes for interactive elements or potential text containers\n\t\tif (\n\t\t\tisInteractiveCandidate(node) ||\n\t\t\tnode.tagName.toLowerCase() === 'iframe' ||\n\t\t\tnode.tagName.toLowerCase() === 'body'\n\t\t) {\n\t\t\tconst attributeNames = node.getAttributeNames?.() || []\n\t\t\tfor (const name of attributeNames) {\n\t\t\t\tconst value = node.getAttribute(name)\n\t\t\t\tnodeData.attributes[name] = value\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * @edit @workaround input.checked\n\t\t\t */\n\t\t\tif (\n\t\t\t\tnode.tagName.toLowerCase() === 'input' &&\n\t\t\t\t(node.type === 'checkbox' || node.type === 'radio')\n\t\t\t) {\n\t\t\t\tnodeData.attributes.checked = node.checked ? 'true' : 'false' // Store as string for consistency\n\t\t\t}\n\t\t}\n\n\t\tlet nodeWasHighlighted = false\n\t\t// Perform visibility, interactivity, and highlighting checks\n\t\tif (node.nodeType === Node.ELEMENT_NODE) {\n\t\t\tnodeData.isVisible = isElementVisible(node) // isElementVisible uses offsetWidth/Height, which is fine\n\t\t\tif (nodeData.isVisible) {\n\t\t\t\tnodeData.isTopElement = isTopElement(node)\n\n\t\t\t\t// Special handling for ARIA menu containers - check interactivity even if not top element\n\t\t\t\tconst role = node.getAttribute('role')\n\t\t\t\tconst isMenuContainer = role === 'menu' || role === 'menubar' || role === 'listbox'\n\n\t\t\t\tif (nodeData.isTopElement || isMenuContainer) {\n\t\t\t\t\tnodeData.isInteractive = isInteractiveElement(node)\n\t\t\t\t\t// Call the dedicated highlighting function\n\t\t\t\t\tnodeWasHighlighted = handleHighlighting(nodeData, node, parentIframe, isParentHighlighted)\n\n\t\t\t\t\t/**\n\t\t\t\t\t * @edit direct dom ref\n\t\t\t\t\t */\n\t\t\t\t\tnodeData.ref = node\n\n\t\t\t\t\t/**\n\t\t\t\t\t * @edit make sure attributes exist for interactive candidates.\n\t\t\t\t\t * @note if the element failed the isInteractiveCandidate, attributes would be empty.\n\t\t\t\t\t */\n\t\t\t\t\tif (nodeData.isInteractive && Object.keys(nodeData.attributes).length === 0) {\n\t\t\t\t\t\tconst attributeNames = node.getAttributeNames?.() || []\n\t\t\t\t\t\tfor (const name of attributeNames) {\n\t\t\t\t\t\t\tconst value = node.getAttribute(name)\n\t\t\t\t\t\t\tnodeData.attributes[name] = value\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Process children, with special handling for iframes and rich text editors\n\t\tif (node.tagName) {\n\t\t\tconst tagName = node.tagName.toLowerCase()\n\n\t\t\t// Handle iframes\n\t\t\tif (tagName === 'iframe') {\n\t\t\t\ttry {\n\t\t\t\t\tconst iframeDoc = node.contentDocument || node.contentWindow?.document\n\t\t\t\t\tif (iframeDoc) {\n\t\t\t\t\t\tfor (const child of iframeDoc.childNodes) {\n\t\t\t\t\t\t\tconst domElement = buildDomTree(child, node, false)\n\t\t\t\t\t\t\tif (domElement) nodeData.children.push(domElement)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconsole.warn('Unable to access iframe:', e)\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Handle rich text editors and contenteditable elements\n\t\t\telse if (\n\t\t\t\tnode.isContentEditable ||\n\t\t\t\tnode.getAttribute('contenteditable') === 'true' ||\n\t\t\t\tnode.id === 'tinymce' ||\n\t\t\t\tnode.classList.contains('mce-content-body') ||\n\t\t\t\t(tagName === 'body' && node.getAttribute('data-id')?.startsWith('mce_'))\n\t\t\t) {\n\t\t\t\t// Process all child nodes to capture formatted text\n\t\t\t\tfor (const child of node.childNodes) {\n\t\t\t\t\tconst domElement = buildDomTree(child, parentIframe, nodeWasHighlighted)\n\t\t\t\t\tif (domElement) nodeData.children.push(domElement)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Handle shadow DOM\n\t\t\t\tif (node.shadowRoot) {\n\t\t\t\t\tnodeData.shadowRoot = true\n\t\t\t\t\tfor (const child of node.shadowRoot.childNodes) {\n\t\t\t\t\t\tconst domElement = buildDomTree(child, parentIframe, nodeWasHighlighted)\n\t\t\t\t\t\tif (domElement) nodeData.children.push(domElement)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Handle regular elements\n\t\t\t\tfor (const child of node.childNodes) {\n\t\t\t\t\t// Pass the highlighted status of the *current* node to its children\n\t\t\t\t\tconst passHighlightStatusToChild = nodeWasHighlighted || isParentHighlighted\n\t\t\t\t\tconst domElement = buildDomTree(child, parentIframe, passHighlightStatusToChild)\n\t\t\t\t\tif (domElement) nodeData.children.push(domElement)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Skip empty anchor tags only if they have no dimensions and no children\n\t\tif (nodeData.tagName === 'a' && nodeData.children.length === 0 && !nodeData.attributes.href) {\n\t\t\t// Check if the anchor has actual dimensions\n\t\t\tconst rect = getCachedBoundingRect(node)\n\t\t\tconst hasSize =\n\t\t\t\t(rect && rect.width > 0 && rect.height > 0) || node.offsetWidth > 0 || node.offsetHeight > 0\n\n\t\t\tif (!hasSize) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * @edit add `extra` field for extra data\n\t\t */\n\t\tnodeData.extra = extraData.get(node) || null\n\n\t\tconst id = `${ID.current++}`\n\t\tDOM_HASH_MAP[id] = nodeData\n\t\treturn id\n\t}\n\n\tconst rootId = buildDomTree(document.body)\n\n\t// Clear the cache before starting\n\tDOM_CACHE.clearCache()\n\n\treturn { rootId, map: DOM_HASH_MAP }\n}\n"
  },
  {
    "path": "packages/page-controller/src/dom/dom_tree/type.ts",
    "content": "// FlatDomTree: 扁平化 DOM 树结构，适用于高效存储和遍历页面结构。\n// 每个节点通过 map 索引，支持文本节点和元素节点，字段区分 undefined 和 false。\n\nexport interface FlatDomTree {\n\trootId: string\n\tmap: Record<string, DomNode>\n}\n\nexport type DomNode = TextDomNode | ElementDomNode | InteractiveElementDomNode\n\nexport interface TextDomNode {\n\ttype: 'TEXT_NODE'\n\ttext: string\n\tisVisible: boolean\n\t// 其他可选字段\n\t[key: string]: unknown\n}\n\nexport interface ElementDomNode {\n\ttagName: string\n\tattributes?: Record<string, string>\n\txpath?: string\n\tchildren?: string[]\n\tisVisible?: boolean\n\tisTopElement?: boolean\n\tisInViewport?: boolean\n\tisNew?: boolean\n\tisInteractive?: false\n\thighlightIndex?: number\n\textra?: Record<string, any>\n\t// 其他可选字段\n\t[key: string]: unknown\n}\n\nexport interface InteractiveElementDomNode {\n\ttagName: string\n\tattributes?: Record<string, string>\n\txpath?: string\n\tchildren?: string[]\n\tisVisible?: boolean\n\tisTopElement?: boolean\n\tisInViewport?: boolean\n\tisInteractive: true\n\thighlightIndex: number\n\t/**\n\t * 可交互元素的 dom 引用\n\t */\n\tref: HTMLElement\n\t// 其他可选字段\n\t[key: string]: unknown\n}\n"
  },
  {
    "path": "packages/page-controller/src/dom/getPageInfo.ts",
    "content": "export function getPageInfo() {\n\tconst viewport_width = window.innerWidth\n\tconst viewport_height = window.innerHeight\n\n\tconst page_width = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth || 0)\n\tconst page_height = Math.max(\n\t\tdocument.documentElement.scrollHeight,\n\t\tdocument.body.scrollHeight || 0\n\t)\n\n\tconst scroll_x = window.scrollX || window.pageXOffset || document.documentElement.scrollLeft || 0\n\tconst scroll_y = window.scrollY || window.pageYOffset || document.documentElement.scrollTop || 0\n\n\tconst pixels_below = Math.max(0, page_height - (window.innerHeight + scroll_y))\n\tconst pixels_right = Math.max(0, page_width - (window.innerWidth + scroll_x))\n\n\treturn {\n\t\t// Current viewport dimensions\n\t\tviewport_width,\n\t\tviewport_height,\n\n\t\t// Total page dimensions\n\t\tpage_width,\n\t\tpage_height,\n\n\t\t// Current scroll position\n\t\tscroll_x,\n\t\tscroll_y,\n\n\t\tpixels_above: scroll_y,\n\t\tpixels_below,\n\n\t\tpages_above: viewport_height > 0 ? scroll_y / viewport_height : 0,\n\t\tpages_below: viewport_height > 0 ? pixels_below / viewport_height : 0,\n\t\ttotal_pages: viewport_height > 0 ? page_height / viewport_height : 0,\n\n\t\tcurrent_page_position: scroll_y / Math.max(1, page_height - viewport_height),\n\n\t\tpixels_left: scroll_x,\n\t\tpixels_right,\n\t}\n}\n"
  },
  {
    "path": "packages/page-controller/src/dom/index.ts",
    "content": "import domTree from './dom_tree/index.js'\nimport {\n\tElementDomNode,\n\tFlatDomTree,\n\tInteractiveElementDomNode,\n\tTextDomNode,\n} from './dom_tree/type'\n\n/**\n * Viewport expansion for DOM tree extraction.\n * -1 means full page (no viewport restriction)\n * 0 means viewport only\n * positive values expand the viewport by that many pixels\n *\n * @note Since isTopElement depends on elementFromPoint,\n * it returns null when out of viewport, this feature has no practical use, only differ between -1 and 0\n */\nconst DEFAULT_VIEWPORT_EXPANSION = -1\n\nexport function resolveViewportExpansion(viewportExpansion?: number): number {\n\treturn viewportExpansion ?? DEFAULT_VIEWPORT_EXPANSION\n}\n\nexport interface DomConfig {\n\tviewportExpansion?: number\n\tinteractiveBlacklist?: (Element | (() => Element))[]\n\tinteractiveWhitelist?: (Element | (() => Element))[]\n\tincludeAttributes?: string[]\n\thighlightOpacity?: number\n\thighlightLabelOpacity?: number\n}\n\n/**\n * 用于检测可交互元素是否是新出现的。\n */\nconst newElementsCache = new WeakMap<HTMLElement, string>()\n\nexport function getFlatTree(config: DomConfig): FlatDomTree {\n\tconst viewportExpansion = resolveViewportExpansion(config.viewportExpansion)\n\n\tconst interactiveBlacklist = [] as Element[]\n\tfor (const item of config.interactiveBlacklist || []) {\n\t\tif (typeof item === 'function') {\n\t\t\tinteractiveBlacklist.push(item())\n\t\t} else {\n\t\t\tinteractiveBlacklist.push(item)\n\t\t}\n\t}\n\n\tconst interactiveWhitelist = [] as Element[]\n\tfor (const item of config.interactiveWhitelist || []) {\n\t\tif (typeof item === 'function') {\n\t\t\tinteractiveWhitelist.push(item())\n\t\t} else {\n\t\t\tinteractiveWhitelist.push(item)\n\t\t}\n\t}\n\n\tconst elements = domTree({\n\t\tdoHighlightElements: true,\n\t\tdebugMode: true,\n\t\tfocusHighlightIndex: -1,\n\t\tviewportExpansion,\n\t\tinteractiveBlacklist,\n\t\tinteractiveWhitelist,\n\t\thighlightOpacity: config.highlightOpacity ?? 0.0,\n\t\thighlightLabelOpacity: config.highlightLabelOpacity ?? 0.1,\n\t}) as FlatDomTree\n\n\tconst currentUrl = window.location.href\n\n\t/**\n\t * 标记新出现的元素\n\t * @todo browser-use 使用 hash(位置，属性等信息) 来判断是否同一个元素，\n\t *       能够解决 1. 元素被删除后重新添加 2. 页面卸载 等问题。\n\t *       这里先简单做.\n\t */\n\tfor (const nodeId in elements.map) {\n\t\tconst node = elements.map[nodeId]\n\t\tif (node.isInteractive && node.ref) {\n\t\t\tconst ref = node.ref as HTMLElement\n\t\t\t// @note 这样太严格，元素是可以跨页面存在的\n\t\t\t// if (newElementsCache.get(ref) !== currentUrl) {\n\t\t\tif (!newElementsCache.has(ref)) {\n\t\t\t\tnewElementsCache.set(ref, currentUrl)\n\t\t\t\tnode.isNew = true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn elements\n}\n\nconst globRegexCache = new Map<string, RegExp>()\n\nfunction globToRegex(pattern: string): RegExp {\n\tlet regex = globRegexCache.get(pattern)\n\tif (!regex) {\n\t\tconst escaped = pattern.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n\t\tregex = new RegExp(`^${escaped.replace(/\\*/g, '.*')}$`)\n\t\tglobRegexCache.set(pattern, regex)\n\t}\n\treturn regex\n}\n\nfunction matchAttributes(\n\tattrs: Record<string, string>,\n\tpatterns: string[]\n): Record<string, string> {\n\tconst result: Record<string, string> = {}\n\n\tfor (const pattern of patterns) {\n\t\tif (pattern.includes('*')) {\n\t\t\tconst regex = globToRegex(pattern)\n\t\t\tfor (const key of Object.keys(attrs)) {\n\t\t\t\tif (regex.test(key) && attrs[key].trim()) {\n\t\t\t\t\tresult[key] = attrs[key].trim()\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tconst value = attrs[pattern]\n\t\t\tif (value && value.trim()) {\n\t\t\t\tresult[pattern] = value.trim()\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\n/**\n * elementsToString 内部使用的类型\n */\ninterface TreeNode {\n\ttype: 'text' | 'element'\n\tparent: TreeNode | null\n\tchildren: TreeNode[]\n\tisVisible: boolean\n\t// Text node properties\n\ttext?: string\n\t// Element node properties\n\ttagName?: string\n\tattributes?: Record<string, string>\n\tisInteractive?: boolean\n\tisTopElement?: boolean\n\tisNew?: boolean\n\thighlightIndex?: number\n\textra?: Record<string, any>\n}\n\n/**\n * 对应 python 中的 views::clickable_elements_to_string,\n * 将 dom 信息处理成适合 llm 阅读的文本格式\n * @形如\n * ``` text\n * [0]<a aria-label=page-agent.js 首页 />\n * [1]<div >P />\n * [2]<div >page-agent.js\n * UI Agent in your webpage />\n * [3]<a >文档 />\n * [4]<a aria-label=查看源码（在新窗口打开）>源码 />\n * UI Agent in your webpage\n * 用户输入需求，AI 理解页面并自动操作。\n * [5]<a role=button>快速开始 />\n * [6]<a role=button>查看文档 />\n * 无需后端\n * ```\n * 其中可交互元素用序号标出，提示llm可以用序号操作。\n * 缩进代表父子关系。\n * 普通文本则直接列出来。\n *\n * @todo 数据脱敏过滤器\n */\nexport function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: string[]): string {\n\tconst DEFAULT_INCLUDE_ATTRIBUTES = [\n\t\t'title',\n\t\t'type',\n\t\t'checked',\n\t\t'name',\n\t\t'role',\n\t\t'value',\n\t\t'placeholder',\n\t\t'data-date-format',\n\t\t'alt',\n\t\t'aria-label',\n\t\t'aria-expanded',\n\t\t'data-state',\n\t\t'aria-checked',\n\n\t\t// @edit added for better form handling\n\t\t'id',\n\t\t'for',\n\n\t\t// for jump check\n\t\t'target',\n\n\t\t// absolute position dropdown menu\n\t\t'aria-haspopup',\n\t\t'aria-controls',\n\t\t'aria-owns',\n\n\t\t// content editable\n\t\t'contenteditable',\n\t]\n\n\tconst includeAttrs = [...(includeAttributes || []), ...DEFAULT_INCLUDE_ATTRIBUTES]\n\n\t// Helper function to cap text length\n\tconst capTextLength = (text: string, maxLength: number): string => {\n\t\tif (text.length > maxLength) {\n\t\t\treturn text.substring(0, maxLength) + '...'\n\t\t}\n\t\treturn text\n\t}\n\n\t// Build tree structure from flat map\n\tconst buildTreeNode = (nodeId: string): TreeNode | null => {\n\t\tconst node = flatTree.map[nodeId]\n\t\tif (!node) return null\n\n\t\tif (node.type === 'TEXT_NODE') {\n\t\t\tconst textNode = node as TextDomNode\n\t\t\treturn {\n\t\t\t\ttype: 'text',\n\t\t\t\ttext: textNode.text,\n\t\t\t\tisVisible: textNode.isVisible,\n\t\t\t\tparent: null,\n\t\t\t\tchildren: [],\n\t\t\t}\n\t\t} else {\n\t\t\tconst elementNode = node as ElementDomNode\n\t\t\tconst children: TreeNode[] = []\n\n\t\t\tif (elementNode.children) {\n\t\t\t\tfor (const childId of elementNode.children) {\n\t\t\t\t\tconst child = buildTreeNode(childId)\n\t\t\t\t\tif (child) {\n\t\t\t\t\t\tchild.parent = null // Will be set later\n\t\t\t\t\t\tchildren.push(child)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: 'element',\n\t\t\t\ttagName: elementNode.tagName,\n\t\t\t\tattributes: elementNode.attributes ?? {},\n\t\t\t\tisVisible: elementNode.isVisible ?? false,\n\t\t\t\tisInteractive: elementNode.isInteractive ?? false,\n\t\t\t\tisTopElement: elementNode.isTopElement ?? false,\n\t\t\t\tisNew: elementNode.isNew ?? false,\n\t\t\t\thighlightIndex: elementNode.highlightIndex,\n\t\t\t\tparent: null,\n\t\t\t\tchildren,\n\t\t\t\textra: elementNode.extra ?? {},\n\t\t\t}\n\t\t}\n\t}\n\n\t// Set parent references\n\tconst setParentReferences = (node: TreeNode, parent: TreeNode | null = null) => {\n\t\tnode.parent = parent\n\t\tfor (const child of node.children) {\n\t\t\tsetParentReferences(child, node)\n\t\t}\n\t}\n\n\t// Build root node\n\tconst rootNode = buildTreeNode(flatTree.rootId)\n\tif (!rootNode) return ''\n\n\tsetParentReferences(rootNode)\n\n\t// Helper to check if text node has parent with highlight index\n\tconst hasParentWithHighlightIndex = (node: TreeNode): boolean => {\n\t\tlet current = node.parent\n\t\twhile (current) {\n\t\t\tif (current.type === 'element' && current.highlightIndex !== undefined) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tcurrent = current.parent\n\t\t}\n\t\treturn false\n\t}\n\n\t// Helper to check if parent is top element\n\t// const isParentTopElement = (node: TreeNode): boolean => {\n\t// \treturn node.parent?.type === 'element' && node.parent.isTopElement === true\n\t// }\n\n\t// Main processing function\n\tconst processNode = (node: TreeNode, depth: number, result: string[]): void => {\n\t\tlet nextDepth = depth\n\t\tconst depthStr = '\\t'.repeat(depth)\n\n\t\tif (node.type === 'element') {\n\t\t\t// Add element with highlight_index\n\t\t\tif (node.highlightIndex !== undefined) {\n\t\t\t\tnextDepth += 1\n\n\t\t\t\tconst text = getAllTextTillNextClickableElement(node)\n\t\t\t\tlet attributesHtmlStr = ''\n\n\t\t\t\tif (includeAttrs.length > 0 && node.attributes) {\n\t\t\t\t\tconst attributesToInclude = matchAttributes(node.attributes, includeAttrs)\n\n\t\t\t\t\t// Remove duplicate values (for attributes longer than 5 chars)\n\t\t\t\t\tconst keys = Object.keys(attributesToInclude)\n\t\t\t\t\tif (keys.length > 1) {\n\t\t\t\t\t\tconst keysToRemove = new Set<string>()\n\t\t\t\t\t\tconst seenValues: Record<string, string> = {}\n\n\t\t\t\t\t\tfor (const key of keys) {\n\t\t\t\t\t\t\tconst value = attributesToInclude[key]\n\t\t\t\t\t\t\tif (value.length > 5) {\n\t\t\t\t\t\t\t\tif (value in seenValues) {\n\t\t\t\t\t\t\t\t\tkeysToRemove.add(key)\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tseenValues[value] = key\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor (const key of keysToRemove) {\n\t\t\t\t\t\t\tdelete attributesToInclude[key]\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Remove role if it matches tagName\n\t\t\t\t\tif (attributesToInclude.role === node.tagName) {\n\t\t\t\t\t\tdelete attributesToInclude.role\n\t\t\t\t\t}\n\n\t\t\t\t\t// Remove attributes that duplicate text content\n\t\t\t\t\tconst attrsToRemoveIfTextMatches = ['aria-label', 'placeholder', 'title']\n\t\t\t\t\tfor (const attr of attrsToRemoveIfTextMatches) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tattributesToInclude[attr] &&\n\t\t\t\t\t\t\tattributesToInclude[attr].toLowerCase().trim() === text.toLowerCase().trim()\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tdelete attributesToInclude[attr]\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (Object.keys(attributesToInclude).length > 0) {\n\t\t\t\t\t\tattributesHtmlStr = Object.entries(attributesToInclude)\n\t\t\t\t\t\t\t.map(([key, value]) => `${key}=${capTextLength(value, 20)}`)\n\t\t\t\t\t\t\t.join(' ')\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Build the line\n\t\t\t\tconst highlightIndicator = node.isNew\n\t\t\t\t\t? `*[${node.highlightIndex}]`\n\t\t\t\t\t: `[${node.highlightIndex}]`\n\t\t\t\tlet line = `${depthStr}${highlightIndicator}<${node.tagName ?? ''}`\n\n\t\t\t\tif (attributesHtmlStr) {\n\t\t\t\t\tline += ` ${attributesHtmlStr}`\n\t\t\t\t}\n\n\t\t\t\t/**\n\t\t\t\t * @edit scrollable 数据\n\t\t\t\t */\n\t\t\t\tif (node.extra) {\n\t\t\t\t\tif (node.extra.scrollable) {\n\t\t\t\t\t\tlet scrollDataText = ''\n\t\t\t\t\t\tif (node.extra.scrollData?.left)\n\t\t\t\t\t\t\tscrollDataText += `left=${node.extra.scrollData.left}, `\n\t\t\t\t\t\tif (node.extra.scrollData?.top) scrollDataText += `top=${node.extra.scrollData.top}, `\n\t\t\t\t\t\tif (node.extra.scrollData?.right)\n\t\t\t\t\t\t\tscrollDataText += `right=${node.extra.scrollData.right}, `\n\t\t\t\t\t\tif (node.extra.scrollData?.bottom)\n\t\t\t\t\t\t\tscrollDataText += `bottom=${node.extra.scrollData.bottom}`\n\n\t\t\t\t\t\tline += ` data-scrollable=\"${scrollDataText}\"`\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (text) {\n\t\t\t\t\tconst trimmedText = text.trim()\n\t\t\t\t\tif (!attributesHtmlStr) {\n\t\t\t\t\t\tline += ' '\n\t\t\t\t\t}\n\t\t\t\t\tline += `>${trimmedText}`\n\t\t\t\t} else if (!attributesHtmlStr) {\n\t\t\t\t\tline += ' '\n\t\t\t\t}\n\n\t\t\t\tline += ' />'\n\t\t\t\tresult.push(line)\n\t\t\t}\n\n\t\t\t// Process children regardless\n\t\t\tfor (const child of node.children) {\n\t\t\t\tprocessNode(child, nextDepth, result)\n\t\t\t}\n\t\t} else if (node.type === 'text') {\n\t\t\t// Add text only if it doesn't have a highlighted parent\n\t\t\tif (hasParentWithHighlightIndex(node)) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tnode.parent &&\n\t\t\t\tnode.parent.type === 'element' &&\n\t\t\t\tnode.parent.isVisible &&\n\t\t\t\tnode.parent.isTopElement\n\t\t\t) {\n\t\t\t\tresult.push(`${depthStr}${node.text ?? ''}`)\n\t\t\t}\n\t\t}\n\t}\n\n\tconst result: string[] = []\n\tprocessNode(rootNode, 0, result)\n\treturn result.join('\\n')\n}\n\n// Get all text until next clickable element\nexport const getAllTextTillNextClickableElement = (node: TreeNode, maxDepth = -1): string => {\n\tconst textParts: string[] = []\n\n\tconst collectText = (currentNode: TreeNode, currentDepth: number) => {\n\t\tif (maxDepth !== -1 && currentDepth > maxDepth) {\n\t\t\treturn\n\t\t}\n\n\t\t// Skip this branch if we hit a highlighted element (except for the current node)\n\t\tif (\n\t\t\tcurrentNode.type === 'element' &&\n\t\t\tcurrentNode !== node &&\n\t\t\tcurrentNode.highlightIndex !== undefined\n\t\t) {\n\t\t\treturn\n\t\t}\n\n\t\tif (currentNode.type === 'text' && currentNode.text) {\n\t\t\ttextParts.push(currentNode.text)\n\t\t} else if (currentNode.type === 'element') {\n\t\t\tfor (const child of currentNode.children) {\n\t\t\t\tcollectText(child, currentDepth + 1)\n\t\t\t}\n\t\t}\n\t}\n\n\tcollectText(node, 0)\n\treturn textParts.join('\\n').trim()\n}\n\nexport function getSelectorMap(flatTree: FlatDomTree): Map<number, InteractiveElementDomNode> {\n\tconst selectorMap = new Map<number, InteractiveElementDomNode>()\n\n\tconst keys = Object.keys(flatTree.map)\n\tfor (const key of keys) {\n\t\tconst node = flatTree.map[key]\n\t\tif (node.isInteractive && typeof node.highlightIndex === 'number') {\n\t\t\tselectorMap.set(node.highlightIndex, node as InteractiveElementDomNode)\n\t\t}\n\t}\n\n\treturn selectorMap\n}\n\nexport function getElementTextMap(simplifiedHTML: string) {\n\tconst lines = simplifiedHTML\n\t\t.split('\\n')\n\t\t.map((line) => line.trim())\n\t\t.filter((line) => line.length > 0)\n\tconst elementTextMap = new Map<number, string>()\n\tfor (const line of lines) {\n\t\tconst regex = /^\\[(\\d+)\\]<[^>]+>([^<]*)/\n\t\tconst match = regex.exec(line)\n\t\tif (match) {\n\t\t\tconst index = parseInt(match[1], 10)\n\t\t\telementTextMap.set(index, line)\n\t\t}\n\t}\n\n\treturn elementTextMap\n}\n\nexport function cleanUpHighlights() {\n\tconst cleanupFunctions = (window as any)._highlightCleanupFunctions || []\n\tfor (const cleanup of cleanupFunctions) {\n\t\tif (typeof cleanup === 'function') {\n\t\t\tcleanup()\n\t\t}\n\t}\n\n\t;(window as any)._highlightCleanupFunctions = []\n}\n\n// 监听 URL 的任何变化，立刻清空 highLights\nwindow.addEventListener('popstate', () => {\n\t// console.log('URL changed (popstate), highlights cleaned up.')\n\tcleanUpHighlights()\n})\nwindow.addEventListener('hashchange', () => {\n\t// console.log('URL changed (hashchange), highlights cleaned up.')\n\tcleanUpHighlights()\n})\nwindow.addEventListener('beforeunload', () => {\n\t// console.log('Page is unloading, highlights cleaned up.')\n\tcleanUpHighlights()\n})\n\nconst navigation = (window as any).navigation\nif (navigation && typeof navigation.addEventListener === 'function') {\n\tnavigation.addEventListener('navigate', () => {\n\t\t// console.log('Navigation event detected, highlights cleaned up.')\n\t\tcleanUpHighlights()\n\t})\n} else {\n\t// 定时器\n\tlet currentUrl = window.location.href\n\tsetInterval(() => {\n\t\tif (window.location.href !== currentUrl) {\n\t\t\tcurrentUrl = window.location.href\n\t\t\t// console.log('URL changed (interval), highlights cleaned up.')\n\t\t\tcleanUpHighlights()\n\t\t}\n\t}, 500)\n}\n"
  },
  {
    "path": "packages/page-controller/src/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\ndeclare module '*.module.css' {\n\tconst classes: Record<string, string>\n\texport default classes\n}\n"
  },
  {
    "path": "packages/page-controller/src/mask/SimulatorMask.module.css",
    "content": ".wrapper {\n\tposition: fixed;\n\tinset: 0;\n\tz-index: 2147483641; /* 确保在所有元素之上，除了 panel */\n\tcursor: wait;\n\toverflow: hidden;\n\n\tdisplay: none;\n}\n\n.wrapper.visible {\n\tdisplay: block;\n}\n"
  },
  {
    "path": "packages/page-controller/src/mask/SimulatorMask.ts",
    "content": "import { Motion } from 'ai-motion'\n\nimport { isPageDark } from './checkDarkMode'\n\nimport styles from './SimulatorMask.module.css'\nimport cursorStyles from './cursor.module.css'\n\nexport class SimulatorMask {\n\tshown: boolean = false\n\twrapper = document.createElement('div')\n\tmotion: Motion | null = null\n\n\t#cursor = document.createElement('div')\n\n\t#currentCursorX = 0\n\t#currentCursorY = 0\n\n\t#targetCursorX = 0\n\t#targetCursorY = 0\n\n\tconstructor() {\n\t\tthis.wrapper.id = 'page-agent-runtime_simulator-mask'\n\t\tthis.wrapper.className = styles.wrapper\n\t\tthis.wrapper.setAttribute('data-browser-use-ignore', 'true')\n\t\tthis.wrapper.setAttribute('data-page-agent-ignore', 'true')\n\n\t\ttry {\n\t\t\tconst motion = new Motion({\n\t\t\t\tmode: isPageDark() ? 'dark' : 'light',\n\t\t\t\tstyles: { position: 'absolute', inset: '0' },\n\t\t\t})\n\t\t\tthis.motion = motion\n\t\t\tthis.wrapper.appendChild(motion.element)\n\t\t\tmotion.autoResize(this.wrapper)\n\t\t} catch (e) {\n\t\t\tconsole.warn('[SimulatorMask] Motion overlay unavailable:', e)\n\t\t}\n\n\t\t// Capture all mouse, keyboard, and wheel events\n\t\tthis.wrapper.addEventListener('click', (e) => {\n\t\t\te.stopPropagation()\n\t\t\te.preventDefault()\n\t\t})\n\t\tthis.wrapper.addEventListener('mousedown', (e) => {\n\t\t\te.stopPropagation()\n\t\t\te.preventDefault()\n\t\t})\n\t\tthis.wrapper.addEventListener('mouseup', (e) => {\n\t\t\te.stopPropagation()\n\t\t\te.preventDefault()\n\t\t})\n\t\tthis.wrapper.addEventListener('mousemove', (e) => {\n\t\t\te.stopPropagation()\n\t\t\te.preventDefault()\n\t\t})\n\t\tthis.wrapper.addEventListener('wheel', (e) => {\n\t\t\te.stopPropagation()\n\t\t\te.preventDefault()\n\t\t})\n\t\tthis.wrapper.addEventListener('keydown', (e) => {\n\t\t\te.stopPropagation()\n\t\t\te.preventDefault()\n\t\t})\n\t\tthis.wrapper.addEventListener('keyup', (e) => {\n\t\t\te.stopPropagation()\n\t\t\te.preventDefault()\n\t\t})\n\n\t\t// Create AI cursor\n\t\tthis.#createCursor()\n\t\t// this.show()\n\n\t\tdocument.body.appendChild(this.wrapper)\n\n\t\tthis.#moveCursorToTarget()\n\n\t\twindow.addEventListener('PageAgent::MovePointerTo', (event: Event) => {\n\t\t\tconst { x, y } = (event as CustomEvent).detail\n\t\t\tthis.setCursorPosition(x, y)\n\t\t})\n\n\t\twindow.addEventListener('PageAgent::ClickPointer', (event: Event) => {\n\t\t\tthis.triggerClickAnimation()\n\t\t})\n\t}\n\n\t#createCursor() {\n\t\tthis.#cursor.className = cursorStyles.cursor\n\n\t\t// Create ripple effect container\n\t\tconst rippleContainer = document.createElement('div')\n\t\trippleContainer.className = cursorStyles.cursorRipple\n\t\tthis.#cursor.appendChild(rippleContainer)\n\n\t\t// Create filling layer\n\t\tconst fillingLayer = document.createElement('div')\n\t\tfillingLayer.className = cursorStyles.cursorFilling\n\t\tthis.#cursor.appendChild(fillingLayer)\n\n\t\t// Create border layer\n\t\tconst borderLayer = document.createElement('div')\n\t\tborderLayer.className = cursorStyles.cursorBorder\n\t\tthis.#cursor.appendChild(borderLayer)\n\n\t\tthis.wrapper.appendChild(this.#cursor)\n\t}\n\n\t#moveCursorToTarget() {\n\t\tconst newX = this.#currentCursorX + (this.#targetCursorX - this.#currentCursorX) * 0.2\n\t\tconst newY = this.#currentCursorY + (this.#targetCursorY - this.#currentCursorY) * 0.2\n\n\t\tconst xDistance = Math.abs(newX - this.#targetCursorX)\n\t\tif (xDistance > 0) {\n\t\t\tif (xDistance < 2) {\n\t\t\t\tthis.#currentCursorX = this.#targetCursorX\n\t\t\t} else {\n\t\t\t\tthis.#currentCursorX = newX\n\t\t\t}\n\t\t\tthis.#cursor.style.left = `${this.#currentCursorX}px`\n\t\t}\n\n\t\tconst yDistance = Math.abs(newY - this.#targetCursorY)\n\t\tif (yDistance > 0) {\n\t\t\tif (yDistance < 2) {\n\t\t\t\tthis.#currentCursorY = this.#targetCursorY\n\t\t\t} else {\n\t\t\t\tthis.#currentCursorY = newY\n\t\t\t}\n\t\t\tthis.#cursor.style.top = `${this.#currentCursorY}px`\n\t\t}\n\n\t\trequestAnimationFrame(() => this.#moveCursorToTarget())\n\t}\n\n\tsetCursorPosition(x: number, y: number) {\n\t\tthis.#targetCursorX = x\n\t\tthis.#targetCursorY = y\n\t}\n\n\ttriggerClickAnimation() {\n\t\tthis.#cursor.classList.remove(cursorStyles.clicking)\n\t\t// Force reflow to restart animation\n\t\tvoid this.#cursor.offsetHeight\n\t\tthis.#cursor.classList.add(cursorStyles.clicking)\n\t}\n\n\tshow() {\n\t\tif (this.shown) return\n\n\t\tthis.shown = true\n\t\tthis.motion?.start()\n\t\tthis.motion?.fadeIn()\n\n\t\tthis.wrapper.classList.add(styles.visible)\n\n\t\t// Initialize cursor position\n\t\tthis.#currentCursorX = window.innerWidth / 2\n\t\tthis.#currentCursorY = window.innerHeight / 2\n\t\tthis.#targetCursorX = this.#currentCursorX\n\t\tthis.#targetCursorY = this.#currentCursorY\n\t\tthis.#cursor.style.left = `${this.#currentCursorX}px`\n\t\tthis.#cursor.style.top = `${this.#currentCursorY}px`\n\t}\n\n\thide() {\n\t\tif (!this.shown) return\n\n\t\tthis.shown = false\n\t\tthis.motion?.fadeOut()\n\t\tthis.motion?.pause()\n\n\t\tthis.#cursor.classList.remove(cursorStyles.clicking)\n\n\t\tsetTimeout(() => {\n\t\t\tthis.wrapper.classList.remove(styles.visible)\n\t\t}, 800) // Match the animation duration\n\t}\n\n\tdispose() {\n\t\tthis.motion?.dispose()\n\t\tthis.wrapper.remove()\n\t}\n}\n"
  },
  {
    "path": "packages/page-controller/src/mask/checkDarkMode.ts",
    "content": "/**\n * Checks for common dark mode CSS classes on the html or body elements.\n * @returns {boolean} - True if a common dark mode class is found.\n */\nfunction hasDarkModeClass() {\n\tconst DEFAULT_DARK_MODE_CLASSES = ['dark', 'dark-mode', 'theme-dark', 'night', 'night-mode']\n\n\tconst htmlElement = document.documentElement\n\tconst bodyElement = document.body || document.documentElement // can be null in some cases\n\n\t// Check class names on <html> and <body>\n\tfor (const className of DEFAULT_DARK_MODE_CLASSES) {\n\t\tif (htmlElement.classList.contains(className) || bodyElement?.classList.contains(className)) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// Some sites use data attributes\n\tconst darkThemeAttribute = htmlElement.getAttribute('data-theme')\n\tif (darkThemeAttribute?.toLowerCase().includes('dark')) {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n/**\n * Parses an RGB or RGBA color string and returns an object with r, g, b properties.\n * @param {string} colorString - e.g., \"rgb(34, 34, 34)\" or \"rgba(0, 0, 0, 0.5)\"\n * @returns {{r: number, g: number, b: number}|null}\n */\nfunction parseRgbColor(colorString: string) {\n\tconst rgbMatch = /rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/.exec(colorString)\n\tif (!rgbMatch) {\n\t\treturn null // Not a valid rgb/rgba string\n\t}\n\treturn {\n\t\tr: parseInt(rgbMatch[1]),\n\t\tg: parseInt(rgbMatch[2]),\n\t\tb: parseInt(rgbMatch[3]),\n\t}\n}\n\n/**\n * Determines if a color is \"dark\" based on its calculated luminance.\n * @param {string} colorString - The CSS color string (e.g., \"rgb(50, 50, 50)\").\n * @param {number} threshold - A value between 0 and 255. Colors with luminance below this will be considered dark. Default is 128.\n * @returns {boolean} - True if the color is considered dark.\n */\nfunction isColorDark(colorString: string, threshold = 128) {\n\tif (!colorString || colorString === 'transparent' || colorString.startsWith('rgba(0, 0, 0, 0)')) {\n\t\treturn false // Transparent is not dark\n\t}\n\n\tconst rgb = parseRgbColor(colorString)\n\tif (!rgb) {\n\t\treturn false // Could not parse color\n\t}\n\n\t// Calculate perceived luminance using the standard formula\n\tconst luminance = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b\n\n\treturn luminance < threshold\n}\n\n/**\n * Checks the background color of the body element to determine if the page is dark.\n * @returns {boolean}\n */\nfunction isBackgroundDark() {\n\t// We check both <html> and <body> because some pages set the color on <html>\n\tconst htmlStyle = window.getComputedStyle(document.documentElement)\n\tconst bodyStyle = window.getComputedStyle(document.body || document.documentElement)\n\n\t// Get background colors\n\tconst htmlBgColor = htmlStyle.backgroundColor\n\tconst bodyBgColor = bodyStyle.backgroundColor\n\n\t// The body's background might be transparent, in which case we should\n\t// fall back to the html element's background.\n\tif (isColorDark(bodyBgColor)) {\n\t\treturn true\n\t} else if (bodyBgColor === 'transparent' || bodyBgColor.startsWith('rgba(0, 0, 0, 0)')) {\n\t\treturn isColorDark(htmlBgColor)\n\t}\n\n\treturn false\n}\n\n/**\n * A comprehensive function to determine if the page is currently in a dark theme.\n * It combines class checking and background color analysis.\n * @returns {boolean} - True if the page is likely dark.\n */\nexport function isPageDark() {\n\ttry {\n\t\t// Strategy 1: Check for common dark mode classes\n\t\tif (hasDarkModeClass()) {\n\t\t\treturn true\n\t\t}\n\n\t\t// Strategy 2: Analyze the computed background color\n\t\tif (isBackgroundDark()) {\n\t\t\treturn true\n\t\t}\n\n\t\t// @TODO add more checks here, e.g., analyzing text color,\n\t\t// or checking the background of major layout elements like <main> or #app.\n\n\t\treturn false\n\t} catch (error) {\n\t\tconsole.warn('Error determining if page is dark:', error)\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "packages/page-controller/src/mask/cursor.module.css",
    "content": "/* AI 光标样式 */\n.cursor {\n\tposition: absolute;\n\twidth: var(--cursor-size, 75px);\n\theight: var(--cursor-size, 75px);\n\tpointer-events: none;\n\tz-index: 10000;\n}\n\n.cursorBorder {\n\tposition: absolute;\n\twidth: 100%;\n\theight: 100%;\n\tbackground: linear-gradient(45deg, rgb(57, 182, 255), rgb(189, 69, 251));\n\tmask-image: url(./cursor-border.svg);\n\tmask-size: 100% 100%;\n\tmask-repeat: no-repeat;\n\n\ttransform-origin: center;\n\ttransform: rotate(-135deg) scale(1.2);\n\tmargin-left: -10px;\n\tmargin-top: -18px;\n}\n\n.cursorFilling {\n\tposition: absolute;\n\twidth: 100%;\n\theight: 100%;\n\tbackground: url(./cursor-fill.svg);\n\tbackground-size: 100% 100%;\n\tbackground-repeat: no-repeat;\n\n\ttransform-origin: center;\n\ttransform: rotate(-135deg) scale(1.2);\n\tmargin-left: -10px;\n\tmargin-top: -18px;\n}\n\n.cursorRipple {\n\tposition: absolute;\n\twidth: 100%;\n\theight: 100%;\n\tpointer-events: none;\n\tmargin-left: -50%;\n\tmargin-top: -50%;\n\n\t&::after {\n\t\tcontent: '';\n\t\topacity: 0;\n\t\tposition: absolute;\n\t\tinset: 0;\n\t\tborder: 4px solid rgba(57, 182, 255, 1);\n\t\tborder-radius: 50%;\n\t}\n}\n\n.cursor.clicking .cursorRipple::after {\n\tanimation: cursor-ripple 300ms ease-out forwards;\n}\n\n@keyframes cursor-ripple {\n\t0% {\n\t\ttransform: scale(0);\n\t\topacity: 1;\n\t}\n\t100% {\n\t\ttransform: scale(2);\n\t\topacity: 0;\n\t}\n}\n"
  },
  {
    "path": "packages/page-controller/src/patches/antd.ts",
    "content": "import type { PageController } from '../PageController'\n\nconst clearFunctions = [] as (() => void)[]\n\n/**\n * antd 的 select 是 div 包 input 的结构，所有信息都在 input 标签上，\n * 但是 input 不可见，也不会出现在清洗后的树里，因此这里把他提上来\n */\nfunction fixAntdSelect() {\n\tconst selects = [...document.querySelectorAll('input[role=\"combobox\"]')]\n\t// for (const select of selects) {}\n}\n\nexport function patchAntd(pageController: PageController) {\n\tpageController.addEventListener('beforeUpdate', fixAntdSelect)\n\tpageController.addEventListener('afterUpdate', () => {\n\t\tfor (const fn of clearFunctions) fn()\n\t\tclearFunctions.length = 0\n\t})\n}\n"
  },
  {
    "path": "packages/page-controller/src/patches/react.ts",
    "content": "import type { PageController } from '../PageController'\n\n// Find common React root elements and add data-page-agent-not-interactive attribute\nexport function patchReact(pageController: PageController) {\n\tconst reactRootElements = document.querySelectorAll(\n\t\t'[data-reactroot], [data-reactid], [data-react-checksum], #root, #app, [id^=\"root-\"], [id^=\"app-\"], #adex-wrapper, #adex-root'\n\t)\n\n\tfor (const element of reactRootElements) {\n\t\telement.setAttribute('data-page-agent-not-interactive', 'true')\n\t}\n}\n\n/**\n * @todo (Heavy, might have false negatives) Interaction detection, if element width/height equals body offsetWidth/Height, consider it root element and non-interactive (React often attaches many events to root elements, causing false positives)\n */\n"
  },
  {
    "path": "packages/page-controller/src/utils/index.ts",
    "content": "// ======= type guards =======\n// @note instanceof fails for elements inside iframes\n\nexport function isHTMLElement(el: unknown): el is HTMLElement {\n\t// @todo either specify to HTMLElement or allow Element here.\n\treturn !!el && (el as Node).nodeType === 1\n}\n\nexport function isInputElement(el: Element): el is HTMLInputElement {\n\treturn el?.nodeType === 1 && el.tagName === 'INPUT'\n}\n\nexport function isTextAreaElement(el: Element): el is HTMLTextAreaElement {\n\treturn el?.nodeType === 1 && el.tagName === 'TEXTAREA'\n}\n\nexport function isSelectElement(el: Element): el is HTMLSelectElement {\n\treturn el?.nodeType === 1 && el.tagName === 'SELECT'\n}\n\nexport function isAnchorElement(el: Element): el is HTMLAnchorElement {\n\treturn el?.nodeType === 1 && el.tagName === 'A'\n}\n\n// ======= iframe helpers =======\n\n/** Iframe offset for translating element coordinates to top-frame viewport. */\nexport function getIframeOffset(element: HTMLElement): { x: number; y: number } {\n\tconst frame = element.ownerDocument.defaultView?.frameElement as HTMLElement | null\n\tif (!frame) return { x: 0, y: 0 }\n\tconst rect = frame.getBoundingClientRect()\n\treturn { x: rect.left, y: rect.top }\n}\n\n/**\n * Get native value setter from the element's own prototype (iframe-safe).\n * @note for React\n */\nexport function getNativeValueSetter(element: HTMLInputElement | HTMLTextAreaElement) {\n\t// eslint-disable-next-line @typescript-eslint/unbound-method\n\treturn Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element) as object, 'value')!\n\t\t.set as (v: string) => void\n}\n\n// ======= general utils =======\n\nexport async function waitFor(seconds: number): Promise<void> {\n\tawait new Promise((resolve) => setTimeout(resolve, seconds * 1000))\n}\n\n// ======= dom utils =======\n\nexport async function movePointerToElement(element: HTMLElement) {\n\tconst rect = element.getBoundingClientRect()\n\tconst offset = getIframeOffset(element)\n\tconst x = rect.left + rect.width / 2 + offset.x\n\tconst y = rect.top + rect.height / 2 + offset.y\n\n\twindow.dispatchEvent(new CustomEvent('PageAgent::MovePointerTo', { detail: { x, y } }))\n\n\tawait waitFor(0.3)\n}\n"
  },
  {
    "path": "packages/page-controller/tsconfig.dts.json",
    "content": "{\n    \"extends\": \"./tsconfig.json\",\n    \"compilerOptions\": {\n        // @workaround DTS bug\n        // dts do not work with monorepo path mapping\n        // disable path mapping for it\n        \"paths\": {}\n    }\n}\n"
  },
  {
    "path": "packages/page-controller/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.base.json\",\n    \"compilerOptions\": {\n        \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.tsbuildinfo\",\n        \"noEmit\": false,\n        \"allowImportingTsExtensions\": false,\n        \"baseUrl\": \".\",\n        \"outDir\": \"dist\"\n    },\n    \"include\": [\"**/*.ts\", \"**/*.js\"],\n    \"exclude\": [\"dist\", \"node_modules\"]\n}\n"
  },
  {
    "path": "packages/page-controller/vite.config.js",
    "content": "// @ts-check\nimport chalk from 'chalk'\nimport { dirname, resolve } from 'path'\nimport dts from 'unplugin-dts/vite'\nimport { fileURLToPath } from 'url'\nimport { defineConfig } from 'vite'\nimport cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nconsole.log(chalk.cyan(`📦 Building @page-agent/page-controller`))\n\nexport default defineConfig({\n\tclearScreen: false,\n\tplugins: [\n\t\tdts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),\n\t\tcssInjectedByJsPlugin({ relativeCSSInjection: true }),\n\t],\n\tpublicDir: false,\n\tesbuild: {\n\t\tkeepNames: true,\n\t},\n\tbuild: {\n\t\tlib: {\n\t\t\tentry: resolve(__dirname, 'src/PageController.ts'),\n\t\t\tname: 'PageController',\n\t\t\tfileName: 'page-controller',\n\t\t\tformats: ['es'],\n\t\t},\n\t\toutDir: resolve(__dirname, 'dist', 'lib'),\n\t\trollupOptions: {\n\t\t\texternal: ['@page-agent/*', 'ai-motion'],\n\t\t\tonwarn: function (message, handler) {\n\t\t\t\tif (message.code === 'EVAL') return\n\t\t\t\thandler(message)\n\t\t\t},\n\t\t},\n\t\tminify: false,\n\t\tsourcemap: true,\n\t\tcssCodeSplit: true,\n\t},\n\tdefine: {\n\t\t'process.env.NODE_ENV': '\"production\"',\n\t},\n})\n"
  },
  {
    "path": "packages/ui/package.json",
    "content": "{\n    \"name\": \"@page-agent/ui\",\n    \"version\": \"1.6.0\",\n    \"type\": \"module\",\n    \"main\": \"./dist/lib/page-agent-ui.js\",\n    \"module\": \"./dist/lib/page-agent-ui.js\",\n    \"types\": \"./dist/lib/index.d.ts\",\n    \"exports\": {\n        \".\": {\n            \"types\": \"./dist/lib/index.d.ts\",\n            \"import\": \"./dist/lib/page-agent-ui.js\",\n            \"default\": \"./dist/lib/page-agent-ui.js\"\n        }\n    },\n    \"files\": [\n        \"dist/\"\n    ],\n    \"description\": \"UI components for page-agent - Panel and i18n\",\n    \"keywords\": [\n        \"page-agent\",\n        \"ui\",\n        \"panel\",\n        \"i18n\"\n    ],\n    \"author\": \"Simon<gaomeng1900>\",\n    \"license\": \"MIT\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/alibaba/page-agent.git\",\n        \"directory\": \"packages/ui\"\n    },\n    \"homepage\": \"https://alibaba.github.io/page-agent/\",\n    \"scripts\": {\n        \"build\": \"vite build\",\n        \"prepublishOnly\": \"node -e \\\"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\\\"\",\n        \"postpublish\": \"node -e \\\"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\\\"\"\n    }\n}\n"
  },
  {
    "path": "packages/ui/src/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\ndeclare module '*.module.css' {\n\tconst classes: Record<string, string>\n\texport default classes\n}\n"
  },
  {
    "path": "packages/ui/src/i18n/index.ts",
    "content": "import {\n\ttype SupportedLanguage,\n\ttype TranslationKey,\n\ttype TranslationParams,\n\ttype TranslationSchema,\n\tlocales,\n} from './locales'\n\nexport class I18n {\n\tprivate language: SupportedLanguage\n\tprivate translations: TranslationSchema\n\n\tconstructor(language: SupportedLanguage = 'en-US') {\n\t\tthis.language = language in locales ? language : 'en-US'\n\t\tthis.translations = locales[this.language]\n\t}\n\n\t// 类型安全的翻译方法\n\tt(key: TranslationKey, params?: TranslationParams): string {\n\t\tconst value = this.getNestedValue(this.translations, key)\n\t\tif (!value) {\n\t\t\tconsole.warn(`Translation key \"${key}\" not found for language \"${this.language}\"`)\n\t\t\treturn key\n\t\t}\n\n\t\tif (params) {\n\t\t\treturn this.interpolate(value, params)\n\t\t}\n\t\treturn value\n\t}\n\n\tprivate getNestedValue(obj: any, path: string): string | undefined {\n\t\treturn path.split('.').reduce((current, key) => current?.[key], obj)\n\t}\n\n\tprivate interpolate(template: string, params: TranslationParams): string {\n\t\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key) => {\n\t\t\t// Use != null to check for both null and undefined, allow empty strings\n\t\t\treturn params[key] != null ? params[key].toString() : match\n\t\t})\n\t}\n\n\tgetLanguage(): SupportedLanguage {\n\t\treturn this.language\n\t}\n}\n\n// 导出类型和实例创建函数\nexport type { TranslationKey, SupportedLanguage, TranslationParams }\nexport { locales }\n"
  },
  {
    "path": "packages/ui/src/i18n/locales.ts",
    "content": "// English translations (base/reference language)\nconst enUS = {\n\tui: {\n\t\tpanel: {\n\t\t\tready: 'Ready',\n\t\t\tthinking: 'Thinking...',\n\t\t\ttaskInput: 'Enter new task, describe steps in detail, press Enter to submit',\n\t\t\tuserAnswerPrompt: 'Please answer the question above, press Enter to submit',\n\t\t\ttaskTerminated: 'Task terminated',\n\t\t\ttaskCompleted: 'Task completed',\n\t\t\tuserAnswer: 'User answer: {{input}}',\n\t\t\tquestion: 'Question: {{question}}',\n\t\t\twaitingPlaceholder: 'Waiting for task to start...',\n\t\t\tstop: 'Stop',\n\t\t\tclose: 'Close',\n\t\t\texpand: 'Expand history',\n\t\t\tcollapse: 'Collapse history',\n\t\t\tstep: 'Step {{number}}',\n\t\t},\n\t\ttools: {\n\t\t\tclicking: 'Clicking element [{{index}}]...',\n\t\t\tinputting: 'Inputting text to element [{{index}}]...',\n\t\t\tselecting: 'Selecting option \"{{text}}\"...',\n\t\t\tscrolling: 'Scrolling page...',\n\t\t\twaiting: 'Waiting {{seconds}} seconds...',\n\t\t\taskingUser: 'Asking user...',\n\t\t\tdone: 'Task done',\n\t\t\tclicked: '🖱️ Clicked element [{{index}}]',\n\t\t\tinputted: '⌨️ Inputted text \"{{text}}\"',\n\t\t\tselected: '☑️ Selected option \"{{text}}\"',\n\t\t\tscrolled: '🛞 Page scrolled',\n\t\t\twaited: '⌛️ Wait completed',\n\t\t\texecuting: 'Executing {{toolName}}...',\n\t\t\tresultSuccess: 'success',\n\t\t\tresultFailure: 'failed',\n\t\t\tresultError: 'error',\n\t\t},\n\t\terrors: {\n\t\t\telementNotFound: 'No interactive element found at index {{index}}',\n\t\t\ttaskRequired: 'Task description is required',\n\t\t\texecutionFailed: 'Task execution failed',\n\t\t\tnotInputElement: 'Element is not an input or textarea',\n\t\t\tnotSelectElement: 'Element is not a select element',\n\t\t\toptionNotFound: 'Option \"{{text}}\" not found',\n\t\t},\n\t},\n} as const\n\n// Chinese translations (must match the structure of enUS)\nconst zhCN = {\n\tui: {\n\t\tpanel: {\n\t\t\tready: '准备就绪',\n\t\t\tthinking: '正在思考...',\n\t\t\ttaskInput: '输入新任务，详细描述步骤，回车提交',\n\t\t\tuserAnswerPrompt: '请回答上面问题，回车提交',\n\t\t\ttaskTerminated: '任务已终止',\n\t\t\ttaskCompleted: '任务结束',\n\t\t\tuserAnswer: '用户回答: {{input}}',\n\t\t\tquestion: '询问: {{question}}',\n\t\t\twaitingPlaceholder: '等待任务开始...',\n\t\t\tstop: '终止',\n\t\t\tclose: '关闭',\n\t\t\texpand: '展开历史',\n\t\t\tcollapse: '收起历史',\n\t\t\tstep: '步骤 {{number}}',\n\t\t},\n\t\ttools: {\n\t\t\tclicking: '正在点击元素 [{{index}}]...',\n\t\t\tinputting: '正在输入文本到元素 [{{index}}]...',\n\t\t\tselecting: '正在选择选项 \"{{text}}\"...',\n\t\t\tscrolling: '正在滚动页面...',\n\t\t\twaiting: '等待 {{seconds}} 秒...',\n\t\t\taskingUser: '正在询问用户...',\n\t\t\tdone: '结束任务',\n\t\t\tclicked: '🖱️ 已点击元素 [{{index}}]',\n\t\t\tinputted: '⌨️ 已输入文本 \"{{text}}\"',\n\t\t\tselected: '☑️ 已选择选项 \"{{text}}\"',\n\t\t\tscrolled: '🛞 页面滚动完成',\n\t\t\twaited: '⌛️ 等待完成',\n\t\t\texecuting: '正在执行 {{toolName}}...',\n\t\t\tresultSuccess: '成功',\n\t\t\tresultFailure: '失败',\n\t\t\tresultError: '错误',\n\t\t},\n\t\terrors: {\n\t\t\telementNotFound: '未找到索引为 {{index}} 的交互元素',\n\t\t\ttaskRequired: '任务描述不能为空',\n\t\t\texecutionFailed: '任务执行失败',\n\t\t\tnotInputElement: '元素不是输入框或文本域',\n\t\t\tnotSelectElement: '元素不是选择框',\n\t\t\toptionNotFound: '未找到选项 \"{{text}}\"',\n\t\t},\n\t},\n} as const\n\n// Type definitions generated from English base structure (but with string values)\ntype DeepStringify<T> = {\n\t[K in keyof T]: T[K] extends string ? string : T[K] extends object ? DeepStringify<T[K]> : T[K]\n}\n\nexport type TranslationSchema = DeepStringify<typeof enUS>\n\n// Utility type: Extract all nested paths from translation object\ntype NestedKeyOf<ObjectType extends object> = {\n\t[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object\n\t\t? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`\n\t\t: `${Key}`\n}[keyof ObjectType & (string | number)]\n\n// Extract all possible key paths from translation structure\nexport type TranslationKey = NestedKeyOf<TranslationSchema>\n\n// Parameterized translation types\nexport type TranslationParams = Record<string, string | number>\n\nexport const locales = {\n\t'en-US': enUS,\n\t'zh-CN': zhCN,\n} as const\n\nexport type SupportedLanguage = keyof typeof locales\n"
  },
  {
    "path": "packages/ui/src/index.ts",
    "content": "export { Panel, type PanelConfig } from './panel/Panel'\nexport { I18n, type SupportedLanguage, type TranslationKey } from './i18n'\n"
  },
  {
    "path": "packages/ui/src/motion-css/createMotion.ts",
    "content": "import styles from './motion.module.css'\n\nexport function createMotion() {\n\tconst wrapper = document.createElement('div')\n\twrapper.className = styles.wrapper\n\n\t{\n\t\tconst colorWrapper = document.createElement('div')\n\t\tcolorWrapper.className = styles.colorWrapper\n\t\twrapper.appendChild(colorWrapper)\n\n\t\tconst layerA = document.createElement('div')\n\t\tlayerA.className = styles.colorLayer + ' ' + styles.layerA\n\t\tcolorWrapper.appendChild(layerA)\n\n\t\tconst layerB = document.createElement('div')\n\t\tlayerB.className = styles.colorLayer + ' ' + styles.layerB\n\t\tcolorWrapper.appendChild(layerB)\n\n\t\tconst layerC = document.createElement('div')\n\t\tlayerC.className = styles.colorLayer + ' ' + styles.layerC\n\t\tcolorWrapper.appendChild(layerC)\n\t}\n\n\t{\n\t\tconst borderWrapper = document.createElement('div')\n\t\tborderWrapper.className = styles.borderWrapper\n\t\twrapper.appendChild(borderWrapper)\n\n\t\tconst layerA = document.createElement('div')\n\t\tlayerA.className = styles.borderLayer + ' ' + styles.layerA\n\t\tborderWrapper.appendChild(layerA)\n\n\t\tconst layerB = document.createElement('div')\n\t\tlayerB.className = styles.borderLayer + ' ' + styles.layerB\n\t\tborderWrapper.appendChild(layerB)\n\n\t\tconst layerC = document.createElement('div')\n\t\tlayerC.className = styles.borderLayer + ' ' + styles.layerC\n\t\tborderWrapper.appendChild(layerC)\n\t}\n\n\tfunction show() {\n\t\twrapper.classList.remove(styles.exit)\n\t\twrapper.classList.remove(styles.entry)\n\t\t// Force reflow to restart animation\n\t\tvoid wrapper.offsetHeight\n\t\twrapper.classList.add(styles.entry)\n\t}\n\n\tfunction hide() {\n\t\twrapper.classList.remove(styles.entry)\n\t\twrapper.classList.remove(styles.exit)\n\t\t// Force reflow to restart animation\n\t\tvoid wrapper.offsetHeight\n\t\twrapper.classList.add(styles.exit)\n\t}\n\n\treturn {\n\t\telement: wrapper,\n\t\tshow,\n\t\thide,\n\t}\n}\n"
  },
  {
    "path": "packages/ui/src/motion-css/motion.module.css",
    "content": ".wrapper {\n\tposition: absolute;\n\tinset: 0;\n\tpointer-events: none;\n\n\ttransform-origin: center;\n\n\t--color-1: rgb(57, 182, 255);\n\t--color-2: rgb(189, 69, 251);\n\t--color-3: rgb(255, 87, 51);\n\t--color-4: rgb(255, 214, 0);\n\n\t--blend-mode: screen;\n}\n\n.colorLayer {\n\tposition: absolute;\n\tinset: 0;\n\n\t/* 变亮混合模式 */\n\t/* mix-blend-mode: screen; */\n\t/* mix-blend-mode: overlay; */\n\t/* mix-blend-mode: multiply; */\n\tmix-blend-mode: add;\n\n\t/* 边框遮罩 - 中间透明，边缘不透明 */\n\tmask-image: url(https://img.alicdn.com/imgextra/i2/O1CN01iW1wfX1C0ICvoPbTq_!!6000000000018-2-tps-512-512.png);\n\tmask-repeat: no-repeat;\n\tmask-size: calc(100% + 10px) calc(100% + 10px);\n}\n\n.borderWrapper {\n\tposition: absolute;\n\tinset: 0;\n\n\t/* filter: blur(10px); */\n}\n\n.borderLayer {\n\tposition: absolute;\n\tinset: 0;\n\n\t/* 变亮混合模式 */\n\t/* mix-blend-mode: overlay; */\n\tmix-blend-mode: add;\n\n\tmask-image:\n\t\tlinear-gradient(\n\t\t\tto right,\n\t\t\tblack 0px,\n\t\t\tblack 2px,\n\t\t\ttransparent 2px,\n\t\t\ttransparent calc(100% - 2px),\n\t\t\tblack calc(100% - 2px),\n\t\t\tblack 100%\n\t\t),\n\t\tlinear-gradient(\n\t\t\tto top,\n\t\t\tblack 0px,\n\t\t\tblack 2px,\n\t\t\ttransparent 2px,\n\t\t\ttransparent calc(100% - 2px),\n\t\t\tblack calc(100% - 2px),\n\t\t\tblack 100%\n\t\t);\n\n\tmask-composite: add;\n\tmask-repeat: no-repeat;\n\tmask-size: 100% 100%;\n\n\t/* filter: blur(100px); */\n}\n\n.blueLayer {\n\t&.colorLayer {\n\t\tmask-position: left -5px top -5px;\n\t}\n\n\t&::after {\n\t\tcontent: '';\n\t\tposition: absolute;\n\t\t/* inset: 0; */\n\t\twidth: calc(max(100vw, 100vh) * 1.5);\n\t\theight: 600px;\n\t\ttop: calc(50% - 300px);\n\t\tleft: 50%;\n\t\tfilter: blur(100px);\n\t\tbackground: rgb(57, 182, 255);\n\t\tanimation: rotate-clockwise 4s linear infinite;\n\t\tanimation-delay: -3s;\n\t}\n}\n\n.purpleLayer {\n\t&.colorLayer {\n\t\tmask-position: left -3px top -7px;\n\t}\n\n\t&::after {\n\t\tcontent: '';\n\t\tposition: absolute;\n\t\t/* inset: 0; */\n\t\twidth: calc(max(100vw, 100vh) * 1.5);\n\t\theight: 600px;\n\t\ttop: calc(50% - 300px);\n\t\tleft: 50%;\n\t\tfilter: blur(100px);\n\t\tbackground: rgb(189, 69, 251);\n\t\tanimation: rotate-clockwise 4s linear infinite;\n\t\tanimation-delay: -2s;\n\t}\n}\n\n.orangeLayer {\n\t/* opacity: 0.5; */\n\n\t&.colorLayer {\n\t\tmask-position: left -7px top -2px;\n\t}\n\n\t&::after {\n\t\tcontent: '';\n\t\tposition: absolute;\n\t\t/* inset: 0; */\n\t\twidth: calc(max(100vw, 100vh) * 1.5);\n\t\theight: 600px;\n\t\ttop: calc(50% - 300px);\n\t\tleft: 50%;\n\t\tfilter: blur(100px);\n\t\tbackground: rgb(255, 87, 51);\n\t\tanimation: rotate-counter-clockwise 3s linear infinite;\n\t\tanimation-delay: -2s;\n\t}\n}\n\n.yellowLayer {\n\t/* opacity: 0.5; */\n\n\t&.colorLayer {\n\t\tmask-position: left -6px top -4px;\n\t}\n\n\t&::after {\n\t\tcontent: '';\n\t\tposition: absolute;\n\t\t/* inset: 0; */\n\t\twidth: calc(max(100vw, 100vh) * 1.5);\n\t\theight: 600px;\n\t\ttop: calc(50% - 300px);\n\t\tleft: 50%;\n\t\tfilter: blur(100px);\n\t\tbackground: rgb(255, 214, 0);\n\t\tanimation: rotate-counter-clockwise 4s linear infinite;\n\t\tanimation-delay: -1s;\n\t}\n}\n\n/* 旋转动画 */\n@keyframes rotate-clockwise {\n\t0% {\n\t\ttransform: translateX(-50%) rotate(0deg);\n\t}\n\t100% {\n\t\ttransform: translateX(-50%) rotate(360deg);\n\t}\n}\n\n@keyframes rotate-counter-clockwise {\n\t0% {\n\t\ttransform: translateX(-50%) rotate(0deg);\n\t}\n\t100% {\n\t\ttransform: translateX(-50%) rotate(-360deg);\n\t}\n}\n\n@keyframes wrapper-entry {\n\tfrom {\n\t\ttransform: scale(1.1);\n\t}\n\tto {\n\t\ttransform: scale(1);\n\t}\n}\n\n/* \nrgb(57, 182, 255)\nrgb(189, 69, 251)\nrgb(255, 87, 51)\nrgb(255, 214, 0)\n*/\n\n@keyframes mask-running {\n\tfrom {\n\t\ttransform: translateX(0%);\n\t}\n\tto {\n\t\ttransform: translateX(100%);\n\t}\n}\n\n@keyframes mask-running-reverse {\n\tfrom {\n\t\ttransform: translateX(100%);\n\t}\n\tto {\n\t\ttransform: translateX(0%);\n\t}\n}\n\n.colorWrapper {\n\tposition: absolute;\n\tinset: 0;\n\n\t.colorLayer {\n\t\tposition: absolute;\n\t\tinset: 0;\n\n\t\tmix-blend-mode: var(--blend-mode);\n\n\t\t/* 边框遮罩 - 中间透明，边缘不透明 */\n\t\tmask-image: url(https://img.alicdn.com/imgextra/i2/O1CN01iW1wfX1C0ICvoPbTq_!!6000000000018-2-tps-512-512.png);\n\t\tmask-repeat: no-repeat;\n\t\tmask-size: 100% 100%;\n\t}\n}\n\n.borderWrapper {\n\tposition: absolute;\n\tinset: 0;\n\n\t--blend-mode: lighten;\n\n\t.borderLayer {\n\t\tposition: absolute;\n\t\tinset: 0;\n\n\t\tmix-blend-mode: var(--blend-mode);\n\n\t\tmask-border: url(https://img.alicdn.com/imgextra/i3/O1CN01bFjRug1yssyWEUbKL_!!6000000006635-2-tps-256-256.png)\n\t\t\t25;\n\t\t-webkit-mask-box-image: url(https://img.alicdn.com/imgextra/i3/O1CN01bFjRug1yssyWEUbKL_!!6000000006635-2-tps-256-256.png)\n\t\t\t25;\n\n\t\tmask-repeat: no-repeat;\n\t\tmask-size: 100% 100%;\n\n\t\tbackground-color: var(--color-2);\n\t}\n}\n\n.entry .colorWrapper,\n.entry .borderWrapper {\n\tanimation: wrapper-entry 0.8s ease-in-out forwards;\n}\n\n.exit .colorWrapper,\n.exit .borderWrapper {\n\tanimation: wrapper-entry 0.8s ease-in-out reverse forwards;\n}\n\n.layerA {\n\tposition: absolute;\n\tinset: 0;\n\n\t&::before {\n\t\tmix-blend-mode: var(--blend-mode);\n\t\tcontent: '';\n\t\tdisplay: block;\n\t\tposition: absolute;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tleft: -100%;\n\t\ttop: 0;\n\t\tbackground-image: linear-gradient(\n\t\t\tto right bottom,\n\t\t\ttransparent,\n\t\t\tvar(--color-1),\n\t\t\ttransparent,\n\t\t\tvar(--color-1),\n\t\t\ttransparent\n\t\t);\n\t\tanimation: mask-running 2s linear infinite;\n\t}\n\n\t&::after {\n\t\tmix-blend-mode: var(--blend-mode);\n\t\tcontent: '';\n\t\tdisplay: block;\n\t\tposition: absolute;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tleft: 0;\n\t\ttop: 0;\n\t\tbackground-image: linear-gradient(\n\t\t\tto right bottom,\n\t\t\ttransparent,\n\t\t\tvar(--color-1),\n\t\t\ttransparent,\n\t\t\tvar(--color-1),\n\t\t\ttransparent\n\t\t);\n\t\tanimation: mask-running 2s linear infinite;\n\t}\n}\n\n.layerB {\n\tposition: absolute;\n\tinset: 0;\n\n\t&::before {\n\t\tmix-blend-mode: var(--blend-mode);\n\t\tcontent: '';\n\t\tdisplay: block;\n\t\tposition: absolute;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tleft: -100%;\n\t\ttop: 0;\n\t\tbackground: linear-gradient(\n\t\t\tto right top,\n\t\t\ttransparent,\n\t\t\tvar(--color-2),\n\t\t\ttransparent,\n\t\t\tvar(--color-2),\n\t\t\ttransparent\n\t\t);\n\t\tanimation: mask-running-reverse 3s linear infinite;\n\t}\n\n\t&::after {\n\t\tmix-blend-mode: var(--blend-mode);\n\t\tcontent: '';\n\t\tdisplay: block;\n\t\tposition: absolute;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tleft: 0;\n\t\ttop: 0;\n\t\tbackground: linear-gradient(\n\t\t\tto right top,\n\t\t\ttransparent,\n\t\t\tvar(--color-2),\n\t\t\ttransparent,\n\t\t\tvar(--color-2),\n\t\t\ttransparent\n\t\t);\n\t\tanimation: mask-running-reverse 3s linear infinite;\n\t}\n}\n\n.layerC {\n\tposition: absolute;\n\tinset: 0;\n\n\topacity: 0.5;\n\n\t&::before {\n\t\tmix-blend-mode: var(--blend-mode);\n\t\tcontent: '';\n\t\tdisplay: block;\n\t\tposition: absolute;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tleft: -100%;\n\t\ttop: 0;\n\t\tbackground: linear-gradient(\n\t\t\tto right top,\n\t\t\ttransparent,\n\t\t\tvar(--color-3),\n\t\t\ttransparent,\n\t\t\tvar(--color-3),\n\t\t\ttransparent\n\t\t);\n\t\tanimation: mask-running 1s linear infinite;\n\t}\n\n\t&::after {\n\t\tmix-blend-mode: var(--blend-mode);\n\t\tcontent: '';\n\t\tdisplay: block;\n\t\tposition: absolute;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tleft: 0;\n\t\ttop: 0;\n\t\tbackground: linear-gradient(\n\t\t\tto right top,\n\t\t\ttransparent,\n\t\t\tvar(--color-3),\n\t\t\ttransparent,\n\t\t\tvar(--color-3),\n\t\t\ttransparent\n\t\t);\n\t\tanimation: mask-running 1s linear infinite;\n\t}\n}\n"
  },
  {
    "path": "packages/ui/src/motion-css/readme",
    "content": "This is the CSS implementation of ai-motion.\n\nEasy to use but Terrible performance. Causing full screen glitching in some browsers.\n\nUse it only in a small area.\n"
  },
  {
    "path": "packages/ui/src/panel/Panel.module.css",
    "content": ".wrapper {\n\tposition: fixed;\n\tbottom: 100px;\n\tleft: 50%;\n\ttransform: translateX(-50%) translateY(20px);\n\topacity: 0;\n\tz-index: 2147483642; /* 比 SimulatorMask 高一层 */\n\tbox-sizing: border-box;\n\n\toverflow: visible;\n\n\t* {\n\t\tbox-sizing: border-box;\n\t}\n\n\t--width: 360px;\n\t--height: 40px;\n\t--border-radius: 12px;\n\n\t--side-space: 12px; /* 控制栏两侧的间距 */\n\t--history-width: calc(var(--width) - var(--side-space) * 2);\n\n\t--color-1: rgb(57, 182, 255);\n\t--color-2: rgb(189, 69, 251);\n\t--color-3: rgb(255, 87, 51);\n\t--color-4: rgb(255, 214, 0);\n\n\twidth: var(--width);\n\theight: var(--height);\n\n\ttransition: all 0.3s ease-in-out;\n\n\t/* 响应式设计 */\n\t@media (max-width: 480px) {\n\t\twidth: calc(100vw - 40px);\n\t\t--width: calc(100vw - 40px);\n\t}\n\n\t.background {\n\t\tposition: absolute;\n\t\tinset: -2px -8px;\n\t\tborder-radius: calc(var(--border-radius) + 4px);\n\t\tfilter: blur(16px);\n\t\toverflow: hidden;\n\t\t/* mix-blend-mode: lighten; */\n\t\t/* display: none; */\n\n\t\t&::before {\n\t\t\tcontent: '';\n\t\t\tz-index: -1;\n\t\t\tpointer-events: none;\n\t\t\tposition: absolute;\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\t/* left: -100%; */\n\t\t\tleft: 0;\n\t\t\ttop: 0;\n\n\t\t\tbackground-image: linear-gradient(\n\t\t\t\tto bottom left,\n\t\t\t\tvar(--color-1),\n\t\t\t\tvar(--color-2),\n\t\t\t\tvar(--color-1)\n\t\t\t);\n\t\t\tanimation: mask-running 2s linear infinite;\n\t\t}\n\t\t&::after {\n\t\t\tcontent: '';\n\t\t\tz-index: -1;\n\t\t\tpointer-events: none;\n\t\t\tposition: absolute;\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\tleft: 0;\n\t\t\ttop: 0;\n\n\t\t\tbackground-image: linear-gradient(\n\t\t\t\tto bottom left,\n\t\t\t\tvar(--color-2),\n\t\t\t\tvar(--color-1),\n\t\t\t\tvar(--color-2)\n\t\t\t);\n\t\t\tanimation: mask-running 2s linear infinite;\n\t\t\tanimation-delay: 1s;\n\t\t}\n\t}\n}\n\n@keyframes mask-running {\n\tfrom {\n\t\ttransform: translateX(-100%);\n\t}\n\tto {\n\t\ttransform: translateX(100%);\n\t}\n}\n\n/* 控制栏 */\n.header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tpadding: 8px 12px;\n\tuser-select: none;\n\n\tposition: absolute;\n\tinset: 0;\n\n\tcursor: pointer;\n\tflex-shrink: 0; /* 防止 header 被压缩 */\n\n\tbackground: rgba(0, 0, 0, 0.5);\n\tbackdrop-filter: blur(10px);\n\tborder-radius: var(--border-radius);\n\tbackground-clip: padding-box;\n\n\tbox-shadow:\n\t\t0 0 0px 2px rgba(255, 255, 255, 0.4),\n\t\t0 0 5px 1px rgba(255, 255, 255, 0.3);\n\n\t.statusSection {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tgap: 8px;\n\t\tflex: 1;\n\t\tmin-height: 24px; /* 确保垂直居中 */\n\n\t\t.indicator {\n\t\t\twidth: 6px;\n\t\t\theight: 6px;\n\t\t\tborder-radius: 50%;\n\t\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\t\tflex-shrink: 0;\n\t\t\tanimation: none; /* 默认无动画 */\n\n\t\t\t/* 运行状态 - 有动画 */\n\t\t\t&.thinking {\n\t\t\t\tbackground: rgb(57, 182, 255);\n\t\t\t\tanimation: pulse 0.8s ease-in-out infinite;\n\t\t\t}\n\n\t\t\t&.tool_executing {\n\t\t\t\tbackground: rgb(189, 69, 251);\n\t\t\t\tanimation: pulse 0.6s ease-in-out infinite;\n\t\t\t}\n\n\t\t\t&.retry {\n\t\t\t\tbackground: rgb(255, 214, 0);\n\t\t\t\tanimation: retryPulse 1s ease-in-out infinite;\n\t\t\t}\n\n\t\t\t/* 静止状态 - 无动画 */\n\t\t\t&.completed,\n\t\t\t&.input,\n\t\t\t&.output {\n\t\t\t\tbackground: rgb(34, 197, 94);\n\t\t\t\tanimation: none;\n\t\t\t}\n\n\t\t\t&.error {\n\t\t\t\tbackground: rgb(239, 68, 68);\n\t\t\t\tanimation: none;\n\t\t\t}\n\t\t}\n\n\t\t.statusText {\n\t\t\tcolor: white;\n\t\t\tfont-size: 12px;\n\t\t\tline-height: 1;\n\t\t\tfont-weight: 500;\n\t\t\ttransition: all 0.3s ease-in-out;\n\t\t\tposition: relative;\n\t\t\toverflow: hidden;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tmin-height: 24px; /* 确保垂直居中 */\n\n\t\t\t&.fadeOut {\n\t\t\t\tanimation: statusTextFadeOut 0.3s ease forwards;\n\t\t\t}\n\n\t\t\t&.fadeIn {\n\t\t\t\tanimation: statusTextFadeIn 0.3s ease forwards;\n\t\t\t}\n\t\t}\n\t}\n\n\t.controls {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tgap: 4px;\n\n\t\t.controlButton {\n\t\t\twidth: 24px;\n\t\t\theight: 24px;\n\t\t\tborder: none;\n\t\t\tborder-radius: 4px;\n\t\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\t\tcolor: white;\n\t\t\tcursor: pointer;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tfont-size: 12px;\n\t\t\tline-height: 1;\n\n\t\t\t&:hover {\n\t\t\t\tbackground: rgba(255, 255, 255, 0.2);\n\t\t\t}\n\t\t}\n\n\t\t.stopButton {\n\t\t\tbackground: rgba(239, 68, 68, 0.2);\n\t\t\tcolor: rgb(255, 41, 41);\n\t\t\tfont-weight: 600;\n\n\t\t\t&:hover {\n\t\t\t\tbackground: rgba(239, 68, 68, 0.3);\n\t\t\t}\n\t\t}\n\t}\n}\n\n@keyframes statusTextFadeIn {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateY(5px);\n\t}\n\t100% {\n\t\topacity: 1;\n\t\ttransform: translateY(0);\n\t}\n}\n\n@keyframes statusTextFadeOut {\n\t0% {\n\t\topacity: 1;\n\t\ttransform: translateY(0);\n\t}\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translateY(-5px);\n\t}\n}\n\n.historySectionWrapper {\n\tposition: absolute;\n\twidth: var(--history-width);\n\tbottom: var(--height);\n\tleft: var(--side-space);\n\tz-index: -2;\n\n\tpadding-top: 0px;\n\tvisibility: collapse;\n\toverflow: hidden;\n\n\ttransition: all 0.2s;\n\n\tbackground: rgba(2, 0, 20, 0.5);\n\t/* background: rgba(186, 186, 186, 0.2); */\n\tbackdrop-filter: blur(10px);\n\n\ttext-shadow: 0 0 1px rgba(0, 0, 0, 0.2);\n\n\tborder-top-left-radius: calc(var(--border-radius) + 4px);\n\tborder-top-right-radius: calc(var(--border-radius) + 4px);\n\n\t/* border: 2px solid rgba(255, 255, 255, 0.8); */\n\tborder: 2px solid rgba(255, 255, 255, 0.4);\n\tbox-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);\n\n\t/* @media (prefers-color-scheme: dark) {\n\t\tbox-shadow:\n\t\t\t0 8px 32px 0 rgba(0, 0, 0, 0.85),\n\t\t\t0 2px 12px 0 rgba(57, 182, 255, 0.1);\n\t} */\n\n\t.expanded & {\n\t\tpadding-top: 8px;\n\t\tvisibility: visible;\n\t}\n\n\t.historySection {\n\t\tposition: relative;\n\t\toverflow-y: auto;\n\t\toverscroll-behavior: contain;\n\t\tscrollbar-width: none;\n\t\tmax-height: 0;\n\t\tpadding-inline: 8px;\n\n\t\ttransition: max-height 0.2s;\n\n\t\t.expanded & {\n\t\t\tmax-height: 400px;\n\t\t}\n\n\t\t.historyItem {\n\t\t\t/* backdrop-filter: blur(10px); */\n\t\t\tpadding: 8px 10px;\n\t\t\tmargin-bottom: 6px;\n\t\t\tbackground: linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03));\n\t\t\tborder-radius: 8px;\n\t\t\tborder-left: 2px solid rgba(57, 182, 255, 0.5);\n\t\t\tfont-size: 12px;\n\t\t\tcolor: white;\n\t\t\t/* color: black; */\n\t\t\tline-height: 1.3;\n\t\t\tposition: relative;\n\t\t\toverflow: hidden;\n\n\t\t\t/* 微妙的内阴影 */\n\t\t\tbox-shadow:\n\t\t\t\tinset 0 1px 0 rgba(255, 255, 255, 0.1),\n\t\t\t\t0 1px 3px rgba(0, 0, 0, 0.1);\n\n\t\t\t&::before {\n\t\t\t\tcontent: '';\n\t\t\t\tposition: absolute;\n\t\t\t\ttop: 0;\n\t\t\t\tleft: 0;\n\t\t\t\tright: 0;\n\t\t\t\theight: 1px;\n\t\t\t\tbackground: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);\n\t\t\t}\n\n\t\t\t&:hover {\n\t\t\t\tbackground: linear-gradient(135deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.06));\n\t\t\t\t/* transform: translateY(-1px); */\n\t\t\t\tbox-shadow:\n\t\t\t\t\tinset 0 1px 0 rgba(255, 255, 255, 0.15),\n\t\t\t\t\t0 2px 4px rgba(0, 0, 0, 0.15);\n\t\t\t}\n\n\t\t\t&:last-child {\n\t\t\t\tmargin-bottom: 10px;\n\t\t\t}\n\n\t\t\t&.completed,\n\t\t\t&.input,\n\t\t\t&.output {\n\t\t\t\tborder-left-color: rgb(34, 197, 94);\n\t\t\t\tbackground: linear-gradient(135deg, rgba(34, 197, 94, 0.1), rgba(34, 197, 94, 0.05));\n\t\t\t}\n\n\t\t\t&.error {\n\t\t\t\tborder-left-color: rgb(239, 68, 68);\n\t\t\t\tbackground: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(239, 68, 68, 0.05));\n\t\t\t}\n\n\t\t\t&.retry {\n\t\t\t\tborder-left-color: rgb(255, 214, 0);\n\t\t\t\tbackground: linear-gradient(135deg, rgba(255, 214, 0, 0.1), rgba(255, 214, 0, 0.05));\n\t\t\t}\n\n\t\t\t&.observation {\n\t\t\t\tborder-left-color: rgb(147, 51, 234);\n\t\t\t\tbackground: linear-gradient(135deg, rgba(147, 51, 234, 0.1), rgba(147, 51, 234, 0.05));\n\t\t\t}\n\n\t\t\t&.question {\n\t\t\t\tborder-left-color: rgb(255, 159, 67);\n\t\t\t\tbackground: linear-gradient(135deg, rgba(255, 159, 67, 0.15), rgba(255, 159, 67, 0.08));\n\t\t\t}\n\n\t\t\t/* 突出显示 done 成功结果 */\n\t\t\t&.doneSuccess {\n\t\t\t\tbackground: linear-gradient(\n\t\t\t\t\t135deg,\n\t\t\t\t\trgba(34, 197, 94, 0.25),\n\t\t\t\t\trgba(34, 197, 94, 0.15),\n\t\t\t\t\trgba(34, 197, 94, 0.08)\n\t\t\t\t);\n\t\t\t\tborder: none;\n\t\t\t\tborder-left: 4px solid rgb(34, 197, 94);\n\t\t\t\tbox-shadow:\n\t\t\t\t\t0 4px 12px rgba(34, 197, 94, 0.3),\n\t\t\t\t\tinset 0 1px 0 rgba(255, 255, 255, 0.2),\n\t\t\t\t\t0 0 20px rgba(34, 197, 94, 0.1);\n\t\t\t\tfont-weight: 600;\n\t\t\t\tcolor: rgb(220, 252, 231);\n\t\t\t\tpadding: 10px 12px;\n\t\t\t\tmargin-bottom: 8px;\n\t\t\t\tborder-radius: 8px;\n\t\t\t\tposition: relative;\n\t\t\t\toverflow: hidden;\n\n\t\t\t\t&::before {\n\t\t\t\t\tbackground: linear-gradient(90deg, transparent, rgba(34, 197, 94, 0.4), transparent);\n\t\t\t\t}\n\n\t\t\t\t&::after {\n\t\t\t\t\tcontent: '';\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0;\n\t\t\t\t\tleft: -100%;\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\theight: 100%;\n\t\t\t\t\tbackground: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);\n\t\t\t\t\tanimation: shimmer 2s ease-in-out infinite;\n\t\t\t\t}\n\n\t\t\t\t.historyContent {\n\t\t\t\t\t.statusIcon {\n\t\t\t\t\t\tfont-size: 16px;\n\t\t\t\t\t\tanimation: celebrate 0.8s ease-in-out;\n\t\t\t\t\t\tfilter: drop-shadow(0 2px 4px rgba(34, 197, 94, 0.5));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t/* 突出显示 done 失败结果 */\n\t\t\t&.doneError {\n\t\t\t\tbackground: linear-gradient(\n\t\t\t\t\t135deg,\n\t\t\t\t\trgba(239, 68, 68, 0.25),\n\t\t\t\t\trgba(239, 68, 68, 0.15),\n\t\t\t\t\trgba(239, 68, 68, 0.08)\n\t\t\t\t);\n\t\t\t\tborder: none;\n\t\t\t\tborder-left: 4px solid rgb(239, 68, 68);\n\t\t\t\tbox-shadow:\n\t\t\t\t\t0 4px 12px rgba(239, 68, 68, 0.3),\n\t\t\t\t\tinset 0 1px 0 rgba(255, 255, 255, 0.2),\n\t\t\t\t\t0 0 20px rgba(239, 68, 68, 0.1);\n\t\t\t\tfont-weight: 600;\n\t\t\t\tcolor: rgb(254, 226, 226);\n\t\t\t\tpadding: 10px 12px;\n\t\t\t\tmargin-bottom: 8px;\n\t\t\t\tborder-radius: 8px;\n\t\t\t\tposition: relative;\n\t\t\t\toverflow: hidden;\n\n\t\t\t\t&::before {\n\t\t\t\t\tbackground: linear-gradient(90deg, transparent, rgba(239, 68, 68, 0.4), transparent);\n\t\t\t\t}\n\n\t\t\t\t.historyContent {\n\t\t\t\t\t.statusIcon {\n\t\t\t\t\t\tfont-size: 16px;\n\t\t\t\t\t\tfilter: drop-shadow(0 2px 4px rgba(239, 68, 68, 0.5));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.historyContent {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: flex-start;\n\t\t\t\tgap: 8px;\n\n\t\t\t\tword-break: break-all;\n\t\t\t\twhite-space: pre-wrap;\n\n\t\t\t\t/* overflow-x: auto; */\n\n\t\t\t\t.statusIcon {\n\t\t\t\t\tfont-size: 12px;\n\t\t\t\t\tflex-shrink: 0;\n\t\t\t\t\tline-height: 1;\n\t\t\t\t\ttransition: all 0.3s ease;\n\t\t\t\t}\n\n\t\t\t\t.reflectionLines {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-direction: column;\n\t\t\t\t\tgap: 4px;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.historyMeta {\n\t\t\t\tfont-size: 10px;\n\t\t\t\tcolor: rgba(255, 255, 255, 0.6);\n\t\t\t\t/* color: rgb(61, 61, 61); */\n\t\t\t\tmargin-top: 8px;\n\t\t\t\tline-height: 1;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/* 动画关键帧 - 更快的闪烁 */\n@keyframes pulse {\n\t0%,\n\t100% {\n\t\topacity: 1;\n\t\ttransform: scale(1);\n\t}\n\t50% {\n\t\topacity: 0.4;\n\t\ttransform: scale(1.3);\n\t}\n}\n\n/* 重试动画 - 旋转脉冲 */\n@keyframes retryPulse {\n\t0%,\n\t100% {\n\t\topacity: 1;\n\t\ttransform: scale(1) rotate(0deg);\n\t}\n\t25% {\n\t\topacity: 0.6;\n\t\ttransform: scale(1.2) rotate(90deg);\n\t}\n\t50% {\n\t\topacity: 0.8;\n\t\ttransform: scale(1.1) rotate(180deg);\n\t}\n\t75% {\n\t\topacity: 0.6;\n\t\ttransform: scale(1.2) rotate(270deg);\n\t}\n}\n\n/* 庆祝动画 */\n@keyframes celebrate {\n\t0%,\n\t100% {\n\t\ttransform: scale(1);\n\t}\n\t25% {\n\t\ttransform: scale(1.2) rotate(-5deg);\n\t}\n\t75% {\n\t\ttransform: scale(1.2) rotate(5deg);\n\t}\n}\n\n/* done 卡片的光泽效果 */\n@keyframes shimmer {\n\t0% {\n\t\tleft: -100%;\n\t}\n\t100% {\n\t\tleft: 100%;\n\t}\n}\n\n/* 输入区域样式 */\n.inputSectionWrapper {\n\tposition: absolute;\n\twidth: var(--history-width);\n\ttop: var(--height);\n\tleft: var(--side-space);\n\tz-index: -1;\n\n\tvisibility: visible;\n\toverflow: hidden;\n\n\theight: 48px;\n\n\ttransition: all 0.2s;\n\n\tbackground: rgba(186, 186, 186, 0.2);\n\tbackdrop-filter: blur(10px);\n\n\tborder-bottom-left-radius: calc(var(--border-radius) + 4px);\n\tborder-bottom-right-radius: calc(var(--border-radius) + 4px);\n\n\tborder: 2px solid rgba(255, 255, 255, 0.3);\n\tbox-shadow: 0 1px 16px rgba(0, 0, 0, 0.4);\n\n\t&.hidden {\n\t\tvisibility: collapse;\n\t\theight: 0;\n\t}\n\n\t.inputSection {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tgap: 4px;\n\t\tpadding: 8px 8px;\n\n\t\t.taskInput {\n\t\t\tflex: 1;\n\t\t\tbackground: rgba(255, 255, 255, 0.4);\n\t\t\tborder: 1px solid rgba(255, 255, 255, 0.3);\n\t\t\tborder-radius: 10px;\n\t\t\tpadding-inline: 10px;\n\t\t\tcolor: rgb(20, 20, 20);\n\t\t\tfont-size: 12px;\n\t\t\theight: 28px;\n\t\t\tline-height: 1;\n\t\t\toutline: none;\n\t\t\ttransition: all 0.2s ease;\n\n\t\t\t/* text-shadow: 0 0 2px rgba(255, 255, 255, 0.8); */\n\n\t\t\t/* border-color: rgba(57, 182, 255, 0.3); */\n\n\t\t\t&::placeholder {\n\t\t\t\tcolor: rgb(53, 53, 53);\n\t\t\t}\n\n\t\t\t&:focus {\n\t\t\t\tbackground: rgba(255, 255, 255, 0.8);\n\t\t\t\tborder-color: rgba(57, 182, 255, 0.6);\n\t\t\t\tbox-shadow: 0 0 0 2px rgba(57, 182, 255, 0.2);\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "packages/ui/src/panel/Panel.ts",
    "content": "import { I18n, type SupportedLanguage } from '../i18n'\nimport { truncate } from '../utils'\nimport { createCard, createReflectionLines } from './cards'\nimport type { AgentActivity, PanelAgentAdapter } from './types'\n\nimport styles from './Panel.module.css'\n\n/**\n * Panel configuration\n */\nexport interface PanelConfig {\n\tlanguage?: SupportedLanguage\n\t/**\n\t * Whether to prompt for next task after task completion\n\t * @default true\n\t */\n\tpromptForNextTask?: boolean\n}\n\n/**\n * Agent control panel\n *\n * Architecture:\n * - History list: renders directly from agent.history (historical events)\n * - Header bar: shows activity events (transient state) and agent status\n *\n * This separation ensures data consistency - history is the single source of truth\n * for what has been done, while activity shows what is happening now.\n */\nexport class Panel {\n\t#wrapper: HTMLElement\n\t#indicator: HTMLElement\n\t#statusText: HTMLElement\n\t#historySection: HTMLElement\n\t#expandButton: HTMLElement\n\t#actionButton: HTMLElement\n\t#inputSection: HTMLElement\n\t#taskInput: HTMLInputElement\n\n\t#agent: PanelAgentAdapter\n\t#config: PanelConfig\n\t#isExpanded = false\n\t#i18n: I18n\n\t#userAnswerResolver: ((input: string) => void) | null = null\n\t#isWaitingForUserAnswer: boolean = false\n\t#headerUpdateTimer: ReturnType<typeof setInterval> | null = null\n\t#pendingHeaderText: string | null = null\n\t#isAnimating = false\n\n\t// Event handlers (bound for removal)\n\t#onStatusChange = () => this.#handleStatusChange()\n\t#onHistoryChange = () => this.#handleHistoryChange()\n\t#onActivity = (e: Event) => this.#handleActivity((e as CustomEvent<AgentActivity>).detail)\n\t#onAgentDispose = () => this.dispose()\n\n\tget wrapper(): HTMLElement {\n\t\treturn this.#wrapper\n\t}\n\n\t/**\n\t * Create a Panel bound to an agent\n\t * @param agent - Agent instance that implements PanelAgentAdapter\n\t * @param config - Optional panel configuration\n\t */\n\tconstructor(agent: PanelAgentAdapter, config: PanelConfig = {}) {\n\t\tthis.#agent = agent\n\t\tthis.#config = config\n\t\tthis.#i18n = new I18n(config.language ?? 'en-US')\n\n\t\t// Set up askUser callback on agent\n\t\tthis.#agent.onAskUser = (question) => this.#askUser(question)\n\n\t\t// Create UI elements\n\t\tthis.#wrapper = this.#createWrapper()\n\t\tthis.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`)!\n\t\tthis.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!\n\t\tthis.#historySection = this.#wrapper.querySelector(`.${styles.historySection}`)!\n\t\tthis.#expandButton = this.#wrapper.querySelector(`.${styles.expandButton}`)!\n\t\tthis.#actionButton = this.#wrapper.querySelector(`.${styles.stopButton}`)!\n\t\tthis.#inputSection = this.#wrapper.querySelector(`.${styles.inputSectionWrapper}`)!\n\t\tthis.#taskInput = this.#wrapper.querySelector(`.${styles.taskInput}`)!\n\n\t\t// Listen to agent events\n\t\tthis.#agent.addEventListener('statuschange', this.#onStatusChange)\n\t\tthis.#agent.addEventListener('historychange', this.#onHistoryChange)\n\t\tthis.#agent.addEventListener('activity', this.#onActivity)\n\t\tthis.#agent.addEventListener('dispose', this.#onAgentDispose)\n\n\t\tthis.#setupEventListeners()\n\t\tthis.#startHeaderUpdateLoop()\n\n\t\tthis.#showInputArea()\n\n\t\tthis.hide() // Start hidden\n\t}\n\n\t// ========== Agent event handlers ==========\n\n\t/** Handle agent status change */\n\t#handleStatusChange(): void {\n\t\tconst status = this.#agent.status\n\n\t\t// Map agent status to UI indicator type\n\t\tconst indicatorType =\n\t\t\tstatus === 'running' ? 'thinking' : status === 'idle' ? 'thinking' : status\n\t\tthis.#updateStatusIndicator(indicatorType)\n\n\t\t// Morph action button: running = stop (■), not running = close (X)\n\t\tif (status === 'running') {\n\t\t\tthis.#actionButton.textContent = '■'\n\t\t\tthis.#actionButton.title = this.#i18n.t('ui.panel.stop')\n\t\t} else {\n\t\t\tthis.#actionButton.textContent = 'X'\n\t\t\tthis.#actionButton.title = this.#i18n.t('ui.panel.close')\n\t\t}\n\n\t\t// Show/hide based on status\n\t\tif (status === 'running') {\n\t\t\tthis.show()\n\t\t\tthis.#hideInputArea() // Hide input while running\n\t\t}\n\n\t\t// Handle completion\n\t\tif (status === 'completed' || status === 'error') {\n\t\t\tif (!this.#isExpanded) {\n\t\t\t\tthis.#expand()\n\t\t\t}\n\t\t\tif (this.#shouldShowInputArea()) {\n\t\t\t\tthis.#showInputArea()\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Handle agent history change - re-render history list from agent.history */\n\t#handleHistoryChange(): void {\n\t\tthis.#renderHistory()\n\t}\n\n\t/**\n\t * Handle agent activity - transient state for immediate UI feedback\n\t * Activity events are NOT persisted in history, only used for header bar updates\n\t */\n\t#handleActivity(activity: AgentActivity): void {\n\t\tswitch (activity.type) {\n\t\t\tcase 'thinking':\n\t\t\t\tthis.#pendingHeaderText = this.#i18n.t('ui.panel.thinking')\n\t\t\t\tthis.#updateStatusIndicator('thinking')\n\t\t\t\tbreak\n\n\t\t\tcase 'executing':\n\t\t\t\tthis.#pendingHeaderText = this.#getToolExecutingText(activity.tool, activity.input)\n\t\t\t\tthis.#updateStatusIndicator('executing')\n\t\t\t\tbreak\n\n\t\t\tcase 'executed':\n\t\t\t\tthis.#pendingHeaderText = truncate(activity.output, 50)\n\t\t\t\tbreak\n\n\t\t\tcase 'retrying':\n\t\t\t\tthis.#pendingHeaderText = `Retrying (${activity.attempt}/${activity.maxAttempts})`\n\t\t\t\tthis.#updateStatusIndicator('retrying')\n\t\t\t\tbreak\n\n\t\t\tcase 'error':\n\t\t\t\tthis.#pendingHeaderText = truncate(activity.message, 50)\n\t\t\t\tthis.#updateStatusIndicator('error')\n\t\t\t\tbreak\n\t\t}\n\t}\n\n\t/**\n\t * Ask for user input (internal, called by agent via onAskUser)\n\t */\n\t#askUser(question: string): Promise<string> {\n\t\treturn new Promise((resolve) => {\n\t\t\t// Set `waiting for user answer` state\n\t\t\tthis.#isWaitingForUserAnswer = true\n\t\t\tthis.#userAnswerResolver = resolve\n\n\t\t\t// Expand history panel\n\t\t\tif (!this.#isExpanded) {\n\t\t\t\tthis.#expand()\n\t\t\t}\n\n\t\t\t// Add temporary question card so user can see the full question\n\t\t\tconst tempCard = document.createElement('div')\n\t\t\ttempCard.innerHTML = createCard({\n\t\t\t\ticon: '❓',\n\t\t\t\tcontent: `Question: ${question}`,\n\t\t\t\ttype: 'question',\n\t\t\t})\n\t\t\tconst cardElement = tempCard.firstElementChild as HTMLElement\n\t\t\tcardElement.setAttribute('data-temp-card', 'true')\n\t\t\tthis.#historySection.appendChild(cardElement)\n\t\t\tthis.#scrollToBottom()\n\n\t\t\tthis.#showInputArea(this.#i18n.t('ui.panel.userAnswerPrompt'))\n\t\t})\n\t}\n\n\t// ========== Public control methods ==========\n\n\tshow(): void {\n\t\tthis.wrapper.style.display = 'block'\n\t\tvoid this.wrapper.offsetHeight\n\t\tthis.wrapper.style.opacity = '1'\n\t\tthis.wrapper.style.transform = 'translateX(-50%) translateY(0)'\n\t}\n\n\thide(): void {\n\t\tthis.wrapper.style.opacity = '0'\n\t\tthis.wrapper.style.transform = 'translateX(-50%) translateY(20px)'\n\t\tthis.wrapper.style.display = 'none'\n\t}\n\n\treset(): void {\n\t\tthis.#statusText.textContent = this.#i18n.t('ui.panel.ready')\n\t\tthis.#updateStatusIndicator('thinking')\n\t\tthis.#renderHistory()\n\t\tthis.#collapse()\n\t\t// Reset user input state\n\t\tthis.#isWaitingForUserAnswer = false\n\t\tthis.#userAnswerResolver = null\n\t\t// Show input area\n\t\tthis.#showInputArea()\n\t}\n\n\texpand(): void {\n\t\tthis.#expand()\n\t}\n\n\tcollapse(): void {\n\t\tthis.#collapse()\n\t}\n\n\t/**\n\t * Dispose panel and clean up event listeners\n\t */\n\tdispose(): void {\n\t\t// Remove agent event listeners\n\t\tthis.#agent.removeEventListener('statuschange', this.#onStatusChange)\n\t\tthis.#agent.removeEventListener('historychange', this.#onHistoryChange)\n\t\tthis.#agent.removeEventListener('activity', this.#onActivity)\n\t\tthis.#agent.removeEventListener('dispose', this.#onAgentDispose)\n\n\t\t// Clean up UI\n\t\tthis.#isWaitingForUserAnswer = false\n\t\tthis.#stopHeaderUpdateLoop()\n\t\tthis.wrapper.remove()\n\t}\n\n\t// ========== Private methods ==========\n\n\t#getToolExecutingText(toolName: string, args: unknown): string {\n\t\tconst a = args as Record<string, string | number>\n\t\tswitch (toolName) {\n\t\t\tcase 'click_element_by_index':\n\t\t\t\treturn this.#i18n.t('ui.tools.clicking', { index: a.index })\n\t\t\tcase 'input_text':\n\t\t\t\treturn this.#i18n.t('ui.tools.inputting', { index: a.index })\n\t\t\tcase 'select_dropdown_option':\n\t\t\t\treturn this.#i18n.t('ui.tools.selecting', { text: a.text })\n\t\t\tcase 'scroll':\n\t\t\t\treturn this.#i18n.t('ui.tools.scrolling')\n\t\t\tcase 'wait':\n\t\t\t\treturn this.#i18n.t('ui.tools.waiting', { seconds: a.seconds })\n\t\t\tcase 'ask_user':\n\t\t\t\treturn this.#i18n.t('ui.tools.askingUser')\n\t\t\tcase 'done':\n\t\t\t\treturn this.#i18n.t('ui.tools.done')\n\t\t\tdefault:\n\t\t\t\treturn this.#i18n.t('ui.tools.executing', { toolName })\n\t\t}\n\t}\n\n\t/**\n\t * Action button handler: stop when running, close (dispose) when idle\n\t */\n\t#handleActionButton(): void {\n\t\tif (this.#agent.status === 'running') {\n\t\t\tthis.#agent.stop()\n\t\t} else {\n\t\t\tthis.#agent.dispose()\n\t\t}\n\t}\n\n\t/**\n\t * Submit task\n\t */\n\t#submitTask() {\n\t\tconst input = this.#taskInput.value.trim()\n\t\tif (!input) return\n\n\t\t// Hide input area\n\t\tthis.#hideInputArea()\n\n\t\tif (this.#isWaitingForUserAnswer) {\n\t\t\t// Handle user input mode\n\t\t\tthis.#handleUserAnswer(input)\n\t\t} else {\n\t\t\t// Execute task via agent\n\t\t\tthis.#agent.execute(input)\n\t\t}\n\t}\n\n\t/**\n\t * Handle user answer\n\t */\n\t#handleUserAnswer(input: string): void {\n\t\t// Remove temporary question cards (only direct children for safety)\n\t\tArray.from(this.#historySection.children).forEach((child) => {\n\t\t\tif (child.getAttribute('data-temp-card') === 'true') {\n\t\t\t\tchild.remove()\n\t\t\t}\n\t\t})\n\n\t\t// Reset state\n\t\tthis.#isWaitingForUserAnswer = false\n\n\t\t// Call resolver to return user input\n\t\tif (this.#userAnswerResolver) {\n\t\t\tthis.#userAnswerResolver(input)\n\t\t\tthis.#userAnswerResolver = null\n\t\t}\n\t}\n\n\t/**\n\t * Show input area\n\t */\n\t#showInputArea(placeholder?: string): void {\n\t\t// Clear input field\n\t\tthis.#taskInput.value = ''\n\t\tthis.#taskInput.placeholder = placeholder || this.#i18n.t('ui.panel.taskInput')\n\t\tthis.#inputSection.classList.remove(styles.hidden)\n\t\t// Focus on input field\n\t\tsetTimeout(() => {\n\t\t\tthis.#taskInput.focus()\n\t\t}, 100)\n\t}\n\n\t/**\n\t * Hide input area\n\t */\n\t#hideInputArea(): void {\n\t\tthis.#inputSection.classList.add(styles.hidden)\n\t}\n\n\t/**\n\t * Check if input area should be shown\n\t */\n\t#shouldShowInputArea(): boolean {\n\t\t// Always show input area if waiting for user input\n\t\tif (this.#isWaitingForUserAnswer) return true\n\n\t\tconst history = this.#agent.history\n\t\tif (history.length === 0) {\n\t\t\treturn true // Initial state\n\t\t}\n\n\t\tconst status = this.#agent.status\n\t\tconst isTaskEnded = status === 'completed' || status === 'error'\n\n\t\t// Only show input area after task completion if configured to do so\n\t\tif (isTaskEnded) {\n\t\t\treturn this.#config.promptForNextTask ?? true\n\t\t}\n\n\t\treturn false\n\t}\n\n\t#createWrapper(): HTMLElement {\n\t\tconst wrapper = document.createElement('div')\n\t\twrapper.id = 'page-agent-runtime_agent-panel'\n\t\twrapper.className = styles.wrapper\n\t\twrapper.setAttribute('data-browser-use-ignore', 'true')\n\t\twrapper.setAttribute('data-page-agent-ignore', 'true')\n\n\t\twrapper.innerHTML = `\n\t\t\t<div class=\"${styles.background}\"></div>\n\t\t\t<div class=\"${styles.historySectionWrapper}\">\n\t\t\t\t<div class=\"${styles.historySection}\">\n\t\t\t\t\t<div class=\"${styles.historyItem}\">\n\t\t\t\t\t\t<div class=\"${styles.historyContent}\">\n\t\t\t\t\t\t\t<span class=\"${styles.statusIcon}\">🧠</span>\n\t\t\t\t\t\t\t<span>${this.#i18n.t('ui.panel.waitingPlaceholder')}</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"${styles.header}\">\n\t\t\t\t<div class=\"${styles.statusSection}\">\n\t\t\t\t\t<div class=\"${styles.indicator} ${styles.thinking}\"></div>\n\t\t\t\t\t<div class=\"${styles.statusText}\">${this.#i18n.t('ui.panel.ready')}</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"${styles.controls}\">\n\t\t\t\t\t<button class=\"${styles.controlButton} ${styles.expandButton}\" title=\"${this.#i18n.t('ui.panel.expand')}\">\n\t\t\t\t\t\t▼\n\t\t\t\t\t</button>\n\t\t\t\t\t<button class=\"${styles.controlButton} ${styles.stopButton}\" title=\"${this.#i18n.t('ui.panel.close')}\">\n\t\t\t\t\t\tX\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"${styles.inputSectionWrapper} ${styles.hidden}\">\n\t\t\t\t<div class=\"${styles.inputSection}\">\n\t\t\t\t\t<input \n\t\t\t\t\t\ttype=\"text\" \n\t\t\t\t\t\tclass=\"${styles.taskInput}\" \n\t\t\t\t\t\tmaxlength=\"200\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t`\n\n\t\tdocument.body.appendChild(wrapper)\n\t\treturn wrapper\n\t}\n\n\t#setupEventListeners(): void {\n\t\t// Click header area to expand/collapse\n\t\tconst header = this.wrapper.querySelector(`.${styles.header}`)!\n\t\theader.addEventListener('click', (e) => {\n\t\t\t// Don't trigger expand/collapse if clicking on buttons\n\t\t\tif ((e.target as HTMLElement).closest(`.${styles.controlButton}`)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.#toggle()\n\t\t})\n\n\t\t// Expand button\n\t\tthis.#expandButton.addEventListener('click', (e) => {\n\t\t\te.stopPropagation()\n\t\t\tthis.#toggle()\n\t\t})\n\n\t\t// Action button (stop / close)\n\t\tthis.#actionButton.addEventListener('click', (e) => {\n\t\t\te.stopPropagation()\n\t\t\tthis.#handleActionButton()\n\t\t})\n\n\t\t// Submit on Enter key in input field\n\t\tthis.#taskInput.addEventListener('keydown', (e) => {\n\t\t\tif (e.isComposing) return // Ignore IME composition keys\n\t\t\tif (e.key === 'Enter') {\n\t\t\t\te.preventDefault()\n\t\t\t\tthis.#submitTask()\n\t\t\t}\n\t\t})\n\n\t\t// Prevent input area click event bubbling\n\t\tthis.#inputSection.addEventListener('click', (e) => {\n\t\t\te.stopPropagation()\n\t\t})\n\t}\n\n\t#toggle(): void {\n\t\tif (this.#isExpanded) {\n\t\t\tthis.#collapse()\n\t\t} else {\n\t\t\tthis.#expand()\n\t\t}\n\t}\n\n\t#expand(): void {\n\t\tthis.#isExpanded = true\n\t\tthis.wrapper.classList.add(styles.expanded)\n\t\tthis.#expandButton.textContent = '▲'\n\t}\n\n\t#collapse(): void {\n\t\tthis.#isExpanded = false\n\t\tthis.wrapper.classList.remove(styles.expanded)\n\t\tthis.#expandButton.textContent = '▼'\n\t}\n\n\t/**\n\t * Start periodic header update loop\n\t */\n\t#startHeaderUpdateLoop(): void {\n\t\t// Check every 450ms (same as total animation duration)\n\t\tthis.#headerUpdateTimer = setInterval(() => {\n\t\t\tthis.#checkAndUpdateHeader()\n\t\t}, 450)\n\t}\n\n\t/**\n\t * Stop periodic header update loop\n\t */\n\t#stopHeaderUpdateLoop(): void {\n\t\tif (this.#headerUpdateTimer) {\n\t\t\tclearInterval(this.#headerUpdateTimer)\n\t\t\tthis.#headerUpdateTimer = null\n\t\t}\n\t}\n\n\t/**\n\t * Check if header needs update and trigger animation if not currently animating\n\t */\n\t#checkAndUpdateHeader(): void {\n\t\t// If no pending text or currently animating, skip\n\t\tif (!this.#pendingHeaderText || this.#isAnimating) {\n\t\t\treturn\n\t\t}\n\n\t\t// If text is already displayed, clear pending and skip\n\t\tif (this.#statusText.textContent === this.#pendingHeaderText) {\n\t\t\tthis.#pendingHeaderText = null\n\t\t\treturn\n\t\t}\n\n\t\t// Start animation\n\t\tconst textToShow = this.#pendingHeaderText\n\t\tthis.#pendingHeaderText = null\n\t\tthis.#animateTextChange(textToShow)\n\t}\n\n\t/**\n\t * Animate text change with fade out/in effect\n\t */\n\t#animateTextChange(newText: string): void {\n\t\tthis.#isAnimating = true\n\n\t\t// Fade out current text\n\t\tthis.#statusText.classList.add(styles.fadeOut)\n\n\t\tsetTimeout(() => {\n\t\t\t// Update text content\n\t\t\tthis.#statusText.textContent = newText\n\n\t\t\t// Fade in new text\n\t\t\tthis.#statusText.classList.remove(styles.fadeOut)\n\t\t\tthis.#statusText.classList.add(styles.fadeIn)\n\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.#statusText.classList.remove(styles.fadeIn)\n\t\t\t\tthis.#isAnimating = false\n\t\t\t}, 300)\n\t\t}, 150) // Half the duration of fade out animation\n\t}\n\n\t#updateStatusIndicator(\n\t\ttype: 'thinking' | 'executing' | 'executed' | 'retrying' | 'completed' | 'error'\n\t): void {\n\t\t// Clear all status classes\n\t\tthis.#indicator.className = styles.indicator\n\n\t\t// Add corresponding status class\n\t\tthis.#indicator.classList.add(styles[type])\n\t}\n\n\t#scrollToBottom(): void {\n\t\t// Execute in next event loop to ensure DOM update completion\n\t\tsetTimeout(() => {\n\t\t\tthis.#historySection.scrollTop = this.#historySection.scrollHeight\n\t\t}, 0)\n\t}\n\n\t/**\n\t * Render history directly from agent.history\n\t *\n\t * Renders:\n\t * 1. Task (first item, from agent.task)\n\t * 2. Reflection cards (evaluation, memory, next_goal)\n\t * 3. Tool execution with output\n\t * 4. Observations\n\t */\n\t#renderHistory(): void {\n\t\tconst items: string[] = []\n\n\t\t// 1. Task card (always first)\n\t\tconst task = this.#agent.task\n\t\tif (task) {\n\t\t\titems.push(this.#createTaskCard(task))\n\t\t}\n\n\t\t// 2. Render each history event\n\t\tconst history = this.#agent.history\n\t\tfor (const event of history) {\n\t\t\titems.push(...this.#createHistoryCards(event))\n\t\t}\n\n\t\tthis.#historySection.innerHTML = items.join('')\n\t\tthis.#scrollToBottom()\n\t}\n\n\t#createTaskCard(task: string): string {\n\t\treturn createCard({ icon: '🎯', content: task, type: 'input' })\n\t}\n\n\t/** Create cards for a history event */\n\t#createHistoryCards(event: PanelAgentAdapter['history'][number]): string[] {\n\t\tconst cards: string[] = []\n\t\tconst meta =\n\t\t\tevent.type === 'step' && event.stepIndex !== undefined\n\t\t\t\t? this.#i18n.t('ui.panel.step', {\n\t\t\t\t\t\tnumber: (event.stepIndex + 1).toString(),\n\t\t\t\t\t})\n\t\t\t\t: undefined\n\n\t\tif (event.type === 'step') {\n\t\t\t// Reflection card\n\t\t\tif (event.reflection) {\n\t\t\t\tconst lines = createReflectionLines(event.reflection)\n\t\t\t\tif (lines.length > 0) {\n\t\t\t\t\tcards.push(createCard({ icon: '🧠', content: lines, meta }))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Action card\n\t\t\tconst action = event.action\n\t\t\tif (action) {\n\t\t\t\tcards.push(...this.#createActionCards(action, meta))\n\t\t\t}\n\t\t} else if (event.type === 'observation') {\n\t\t\tcards.push(\n\t\t\t\tcreateCard({ icon: '👁️', content: event.content || '', meta, type: 'observation' })\n\t\t\t)\n\t\t} else if (event.type === 'user_takeover') {\n\t\t\tcards.push(createCard({ icon: '👤', content: 'User takeover', meta, type: 'input' }))\n\t\t} else if (event.type === 'retry') {\n\t\t\tconst retryInfo = `${event.message || 'Retrying'} (${event.attempt}/${event.maxAttempts})`\n\t\t\tcards.push(createCard({ icon: '🔄', content: retryInfo, meta, type: 'observation' }))\n\t\t} else if (event.type === 'error') {\n\t\t\tcards.push(\n\t\t\t\tcreateCard({ icon: '❌', content: event.message || 'Error', meta, type: 'observation' })\n\t\t\t)\n\t\t}\n\n\t\treturn cards\n\t}\n\n\t/** Create cards for an action */\n\t#createActionCards(\n\t\taction: { name: string; input: unknown; output: string },\n\t\tmeta?: string\n\t): string[] {\n\t\tconst cards: string[] = []\n\n\t\tif (action.name === 'done') {\n\t\t\tconst input = action.input as { text?: string }\n\t\t\tconst text = input.text || action.output || ''\n\t\t\tif (text) {\n\t\t\t\tcards.push(createCard({ icon: '🤖', content: text, meta, type: 'output' }))\n\t\t\t}\n\t\t} else if (action.name === 'ask_user') {\n\t\t\tconst input = action.input as { question?: string }\n\t\t\tconst answer = action.output.replace(/^User answered:\\s*/i, '')\n\t\t\tcards.push(\n\t\t\t\tcreateCard({\n\t\t\t\t\ticon: '❓',\n\t\t\t\t\tcontent: `Question: ${input.question || ''}`,\n\t\t\t\t\tmeta,\n\t\t\t\t\ttype: 'question',\n\t\t\t\t})\n\t\t\t)\n\t\t\tcards.push(createCard({ icon: '💬', content: `Answer: ${answer}`, meta, type: 'input' }))\n\t\t} else {\n\t\t\tconst toolText = this.#getToolExecutingText(action.name, action.input)\n\t\t\tcards.push(createCard({ icon: '🔨', content: toolText, meta }))\n\t\t\tif (action.output?.length > 0) {\n\t\t\t\tcards.push(createCard({ icon: '🔨', content: action.output, meta, type: 'output' }))\n\t\t\t}\n\t\t}\n\n\t\treturn cards\n\t}\n}\n"
  },
  {
    "path": "packages/ui/src/panel/cards.ts",
    "content": "/**\n * Card HTML generation utilities for Panel\n */\nimport { escapeHtml } from '../utils'\n\nimport styles from './Panel.module.css'\n\ntype CardType = 'default' | 'input' | 'output' | 'question' | 'observation'\n\ninterface CardOptions {\n\ticon: string\n\tcontent: string | string[]\n\tmeta?: string\n\ttype?: CardType\n}\n\n/** Create a single history card */\nexport function createCard({ icon, content, meta, type }: CardOptions): string {\n\tconst typeClass = type ? styles[type] : ''\n\tconst contentHtml = Array.isArray(content)\n\t\t? `<div class=\"${styles.reflectionLines}\">${content.map((line) => `<span>${escapeHtml(line)}</span>`).join('')}</div>`\n\t\t: `<span>${escapeHtml(content)}</span>`\n\n\treturn `\n\t\t<div class=\"${styles.historyItem} ${typeClass}\">\n\t\t\t<div class=\"${styles.historyContent}\">\n\t\t\t\t<span class=\"${styles.statusIcon}\">${icon}</span>\n\t\t\t\t${contentHtml}\n\t\t\t</div>\n\t\t\t${meta ? `<div class=\"${styles.historyMeta}\">${meta}</div>` : ''}\n\t\t</div>\n\t`\n}\n\n/** Create reflection lines from reflection object */\nexport function createReflectionLines(reflection: {\n\tevaluation_previous_goal?: string\n\tmemory?: string\n\tnext_goal?: string\n}): string[] {\n\tconst lines: string[] = []\n\tif (reflection.evaluation_previous_goal) {\n\t\tlines.push(`🔍 ${reflection.evaluation_previous_goal}`)\n\t}\n\tif (reflection.memory) {\n\t\tlines.push(`💾 ${reflection.memory}`)\n\t}\n\tif (reflection.next_goal) {\n\t\tlines.push(`🎯 ${reflection.next_goal}`)\n\t}\n\treturn lines\n}\n"
  },
  {
    "path": "packages/ui/src/panel/types.ts",
    "content": "/**\n * Agent activity - transient state for immediate UI feedback.\n *\n * Unlike historical events (which are persisted), activities are ephemeral\n * and represent \"what the agent is doing right now\". UI components should\n * listen to 'activity' events to show real-time feedback.\n *\n * Note: There is no 'idle' activity - absence of activity events means idle.\n *\n * Events dispatched: CustomEvent<AgentActivity>\n */\nexport type AgentActivity =\n\t| { type: 'thinking' }\n\t| { type: 'executing'; tool: string; input: unknown }\n\t| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }\n\t| { type: 'retrying'; attempt: number; maxAttempts: number }\n\t| { type: 'error'; message: string }\n\n/**\n * Minimal interface that Panel expects from an agent.\n * Panel does not depend on PageAgent directly - it only requires this interface.\n * This enables decoupling and allows any agent implementation to work with Panel.\n *\n * Events:\n * - 'statuschange': Agent status changed (idle/running/completed/error)\n * - 'historychange': Historical events updated (persisted)\n * - 'activity': Transient activity for immediate UI feedback (thinking/executing/etc)\n * - 'dispose': Agent is being disposed\n */\nexport interface PanelAgentAdapter extends EventTarget {\n\t/** Current agent status */\n\treadonly status: 'idle' | 'running' | 'completed' | 'error'\n\n\t/** History of agent events */\n\treadonly history: readonly {\n\t\ttype: 'step' | 'observation' | 'user_takeover' | 'retry' | 'error'\n\t\tstepIndex?: number\n\t\t/** For 'step' type */\n\t\treflection?: {\n\t\t\tevaluation_previous_goal?: string\n\t\t\tmemory?: string\n\t\t\tnext_goal?: string\n\t\t}\n\t\t/** For 'step' type */\n\t\taction?: {\n\t\t\tname: string\n\t\t\tinput: unknown\n\t\t\toutput: string\n\t\t}\n\t\t/** For 'observation' type */\n\t\tcontent?: string\n\t\t/** For 'retry' type */\n\t\tattempt?: number\n\t\tmaxAttempts?: number\n\t\t/** For 'retry' and 'error' types */\n\t\tmessage?: string\n\t}[]\n\n\t/** Current task being executed */\n\treadonly task: string\n\n\t/**\n\t * Callback for when agent needs user input.\n\t * Panel will set this to handle user questions via its UI.\n\t */\n\tonAskUser?: (question: string) => Promise<string>\n\n\t/** Execute a task */\n\texecute(task: string): Promise<unknown>\n\n\t/** Stop the current task (agent remains reusable) */\n\tstop(): void\n\n\t/** Dispose the agent (terminal, cannot be reused) */\n\tdispose(): void\n}\n"
  },
  {
    "path": "packages/ui/src/utils.ts",
    "content": "export function truncate(text: string, maxLength: number): string {\n\tif (text.length > maxLength) {\n\t\treturn text.substring(0, maxLength) + '...'\n\t}\n\treturn text\n}\n\n/**\n * Escape HTML special characters to prevent XSS and rendering issues\n */\nexport function escapeHtml(text: string): string {\n\treturn text\n\t\t.replace(/&/g, '&amp;')\n\t\t.replace(/</g, '&lt;')\n\t\t.replace(/>/g, '&gt;')\n\t\t.replace(/\"/g, '&quot;')\n\t\t.replace(/'/g, '&#039;')\n}\n"
  },
  {
    "path": "packages/ui/tsconfig.dts.json",
    "content": "{\n    \"extends\": \"./tsconfig.json\",\n    \"compilerOptions\": {\n        // @workaround DTS bug\n        // dts do not work with monorepo path mapping\n        // disable path mapping for it\n        \"paths\": {}\n    }\n}\n"
  },
  {
    "path": "packages/ui/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.base.json\",\n    \"compilerOptions\": {\n        \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.tsbuildinfo\",\n        \"noEmit\": false,\n        \"allowImportingTsExtensions\": false,\n        \"baseUrl\": \".\",\n        \"outDir\": \"dist\"\n    },\n    \"include\": [\"**/*.ts\", \"**/*.js\"],\n    \"exclude\": [\"dist\", \"node_modules\"]\n}\n"
  },
  {
    "path": "packages/ui/vite.config.js",
    "content": "// @ts-check\nimport chalk from 'chalk'\nimport { dirname, resolve } from 'path'\nimport dts from 'unplugin-dts/vite'\nimport { fileURLToPath } from 'url'\nimport { defineConfig } from 'vite'\nimport cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\nconsole.log(chalk.cyan(`📦 Building @page-agent/ui`))\n\nexport default defineConfig({\n\tclearScreen: false,\n\tplugins: [\n\t\tdts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),\n\t\tcssInjectedByJsPlugin({ relativeCSSInjection: true }),\n\t],\n\tpublicDir: false,\n\tesbuild: {\n\t\tkeepNames: true,\n\t},\n\tbuild: {\n\t\tlib: {\n\t\t\tentry: resolve(__dirname, 'src/index.ts'),\n\t\t\tname: 'PageAgentUI',\n\t\t\tfileName: 'page-agent-ui',\n\t\t\tformats: ['es'],\n\t\t},\n\t\toutDir: resolve(__dirname, 'dist', 'lib'),\n\t\trollupOptions: {\n\t\t\texternal: [],\n\t\t},\n\t\tminify: false,\n\t\tsourcemap: true,\n\t\tcssCodeSplit: true,\n\t},\n\tdefine: {\n\t\t'process.env.NODE_ENV': '\"production\"',\n\t},\n})\n"
  },
  {
    "path": "packages/website/AGENTS.md",
    "content": "# Website Package - Instructions for Coding Assistants\n\n## Tech Stack\n\n- **React** with TypeScript\n- **Vite** for dev server and build\n- **Tailwind CSS** for styling\n- **shadcn/ui** (new-york style) for UI components — **do NOT hand-edit `src/components/ui/` files**\n- **Magic UI** for animations and effects\n- **wouter** with browser routing (`base: \"/page-agent\"`)\n- **lucide-react** for icons\n\n## Component Guidelines\n\n### Use shadcn/ui Components First\n\n**ALWAYS prefer shadcn/ui components over custom implementations.**\n\nBefore creating any UI component, check if shadcn already provides it:\n\n```bash\n# IMPORTANT: Run from packages/website/, NOT from repo root\ncd packages/website\n\n# Add a new shadcn component\nnpx shadcn@latest add <component-name>\n\n# Add a Magic UI component\nnpx shadcn@latest add \"@magicui/<component-name>\"\n```\n\nAvailable shadcn components: https://ui.shadcn.com/docs/components\nAvailable Magic UI components: https://magicui.design/docs/components\n\n### Current UI Components\n\nLocated in `src/components/ui/`:\n\n**From shadcn/ui:**\n\n- `alert`, `badge`, `button`, `separator`, `sonner`, `switch`, `tooltip`\n\n**From Magic UI:**\n\n- `animated-gradient-text`, `animated-shiny-text`, `aurora-text`\n- `hyper-text`, `magic-card`, `neon-gradient-card`, `particles`\n- `sparkles-text`, `text-animate`, `typing-animation`\n\n**Custom:**\n\n- `highlighter`, `kbd`, `spinner`\n\n### Styling Rules\n\n1. **Prefer Tailwind classes** over custom CSS\n2. Support dark mode via `dark:` classes\n3. Use CSS variables from `src/index.css` for theme colors\n\n## Project Structure\n\n```\nsrc/\n├── pages/\n│   ├── home/\n│   │   ├── index.tsx  # Homepage\n│   │   └── ...Section.tsx\n│   └── docs/\n│       ├── index.tsx    # Docs route switch\n│       ├── Layout.tsx   # Sidebar navigation\n│       └── [section]/[topic]/page.tsx\n├── components/\n│   ├── ui/              # shadcn/ui + Magic UI (DO NOT hand-edit)\n│   ├── Heading.tsx      # Anchor heading for doc pages\n│   ├── Header.tsx       # Site header\n│   └── Footer.tsx       # Site footer\n├── i18n/                # Internationalization\n├── router.tsx           # Root layout + routing\n└── main.tsx             # App entry\n```\n\n## Routing\n\nUses wouter browser routing with base path for GitHub Pages deployment at `https://alibaba.github.io/page-agent/`.\n\n```tsx\n// main.tsx\n<Router base=\"/page-agent\">\n  <PagesRouter />\n</Router>\n```\n\n**Key rules:**\n\n- Header and Footer live in `router.tsx` **outside** `<Switch>`, so they always see the root router context (`base=\"/page-agent\"`)\n- Docs pages are nested via `<Route path=\"/docs\" nest>`, which creates a child context (`base=\"/page-agent/docs\"`)\n- Inside the docs nest, Link hrefs are relative to `/docs` (e.g. `href=\"/features/models\"`, NOT `href=\"/docs/features/models\"`)\n- **Never use `~` prefix** in Link hrefs — it bypasses the base path entirely\n- Doc page headings use `<Heading id=\"slug\" level={2}>` for anchor links\n\n### SPA on GitHub Pages\n\nInstead of `404.html` redirects, the build copies `index.html` into every route directory. Add new routes to the `SPA_ROUTES` array in `vite.config.js`.\n\n## Adding New Pages\n\n### Documentation Page\n\n1. Create `src/pages/docs/<section>/<slug>/page.tsx`\n2. Add route in `src/pages/docs/index.tsx`\n3. Add navigation item in `src/pages/docs/Layout.tsx`\n4. Add path to `SPA_ROUTES` in `vite.config.js`\n\n## Configuration Files\n\n| File              | Purpose                 |\n| ----------------- | ----------------------- |\n| `components.json` | shadcn/ui configuration |\n| `vite.config.js`  | Vite build + SPA routes |\n| `tsconfig.json`   | TypeScript config       |\n\n## Commands\n\n```bash\nnpm start            # Dev server (from root)\nnpm run build:website    # Build website (from root)\n```\n"
  },
  {
    "path": "packages/website/components.json",
    "content": "{\n    \"$schema\": \"https://ui.shadcn.com/schema.json\",\n    \"style\": \"new-york\",\n    \"rsc\": false,\n    \"tsx\": true,\n    \"tailwind\": {\n        \"config\": \"\",\n        \"css\": \"src/index.css\",\n        \"baseColor\": \"neutral\",\n        \"cssVariables\": true,\n        \"prefix\": \"\"\n    },\n    \"iconLibrary\": \"lucide\",\n    \"aliases\": {\n        \"components\": \"@/components\",\n        \"utils\": \"@/lib/utils\",\n        \"ui\": \"@/components/ui\",\n        \"lib\": \"@/lib\",\n        \"hooks\": \"@/hooks\"\n    },\n    \"registries\": {\n        \"@magicui\": \"https://magicui.design/r/{name}.json\"\n    }\n}\n"
  },
  {
    "path": "packages/website/index.html",
    "content": "<!doctype html>\n<html lang=\"zh-CN\">\n\t<head>\n\t\t<meta charset=\"UTF-8\" />\n\t\t<link\n\t\t\trel=\"icon\"\n\t\t\ttype=\"image/svg+xml\"\n\t\t\thref=\"https://img.alicdn.com/imgextra/i2/O1CN012eGDRI1X6nnMt9clU_!!6000000002875-49-tps-64-64.webp\"\n\t\t/>\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\t\t<title>PageAgent - The GUI Agent Living in Your Webpage</title>\n\t\t<meta\n\t\t\tname=\"description\"\n\t\t\tcontent=\"PageAgent.js: Intelligent GUI Agent for any website. Modern web AI automation with minimal integration.\"\n\t\t/>\n\t\t<meta\n\t\t\tname=\"keywords\"\n\t\t\tcontent=\"PageAgent, AI Agent, GUI Agent, Web Automation, GUI Automation, Frontend, CDN, JavaScript, React, Vite, LLM\"\n\t\t/>\n\t\t<meta\n\t\t\tproperty=\"og:image\"\n\t\t\tcontent=\"https://img.alicdn.com/imgextra/i3/O1CN01JPT4Fj1FJTfmHfNxO_!!6000000000466-49-tps-512-512.webp\"\n\t\t/>\n\t\t<meta property=\"og:url\" content=\"https://alibaba.github.io/page-agent\" />\n\t\t<meta property=\"og:type\" content=\"website\" />\n\t\t<meta name=\"theme-color\" content=\"#58c0fc\" />\n\t\t<meta name=\"color-scheme\" content=\"light dark\" />\n\t\t<meta name=\"author\" content=\"PageAgent.js Team\" />\n\t\t<meta property=\"og:title\" content=\"PageAgent.js - AI-powered GUI Agent\" />\n\t\t<meta property=\"og:description\" content=\"The GUI Agent living in your website.\" />\n\t\t<meta property=\"og:type\" content=\"website\" />\n\t\t<meta property=\"og:locale\" content=\"en_US\" />\n\t\t<meta property=\"og:locale:alternate\" content=\"zh_CN\" />\n\n\t\t<!-- Google tag (gtag.js) -->\n\t\t<script async src=\"https://www.googletagmanager.com/gtag/js?id=G-HCGRJTN3HM\"></script>\n\t\t<script>\n\t\t\twindow.dataLayer = window.dataLayer || []\n\t\t\tfunction gtag() {\n\t\t\t\tdataLayer.push(arguments)\n\t\t\t}\n\t\t\tgtag('js', new Date())\n\n\t\t\tgtag('config', 'G-HCGRJTN3HM')\n\t\t</script>\n\t</head>\n\t<body>\n\t\t<div id=\"root\">\n\t\t\t<div id=\"sk\">\n\t\t\t\t<p class=\"sk-text\" id=\"sk-text\">Loading...</p>\n\t\t\t\t<style>\n\t\t\t\t\t#sk{display:flex;align-items:center;justify-content:center;min-height:100vh;background:linear-gradient(135deg,#eff6ff,#f5f3ff)}\n\t\t\t\t\t@media(prefers-color-scheme:dark){#sk{background:linear-gradient(135deg,#111827,#1f2937)}}\n\t\t\t\t\t.sk-text{font:400 14px/1 system-ui,sans-serif;color:#94a3b8;animation:skf 2s ease-in-out infinite}\n\t\t\t\t\t@keyframes skf{0%,100%{opacity:.6}50%{opacity:.3}}\n\t\t\t\t</style>\n\t\t\t</div>\n\t\t</div>\n\t\t<script type=\"module\" src=\"./src/main.tsx\"></script>\n\t\t<script>\n\t\t\tconst updateHtmlLang = () => {\n\t\t\t\tconst lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh-CN'\n\t\t\t\tdocument.documentElement.lang = lang\n\t\t\t\tconst el = document.getElementById('sk-text')\n\t\t\t\tif (el) el.textContent = lang.startsWith('zh') ? '加载中...' : 'Loading...'\n\t\t\t}\n\t\t\tupdateHtmlLang()\n\t\t\twindow.addEventListener('storage', updateHtmlLang)\n\t\t</script>\n\t</body>\n</html>\n"
  },
  {
    "path": "packages/website/package.json",
    "content": "{\n    \"name\": \"@page-agent/website\",\n    \"private\": true,\n    \"version\": \"1.6.0\",\n    \"type\": \"module\",\n    \"scripts\": {\n        \"dev\": \"vite --host 0.0.0.0\",\n        \"build:website\": \"vite build\",\n        \"preview\": \"vite preview\",\n        \"typecheck\": \"tsc --noEmit\"\n    },\n    \"devDependencies\": {\n        \"@radix-ui/react-icons\": \"^1.3.2\",\n        \"@radix-ui/react-separator\": \"^1.1.8\",\n        \"@radix-ui/react-slot\": \"^1.2.4\",\n        \"@radix-ui/react-switch\": \"^1.2.6\",\n        \"@radix-ui/react-tooltip\": \"^1.2.8\",\n        \"@types/react\": \"^19.2.14\",\n        \"@types/react-dom\": \"^19.2.1\",\n        \"class-variance-authority\": \"^0.7.1\",\n        \"clsx\": \"^2.1.1\",\n        \"lucide-react\": \"^0.577.0\",\n        \"motion\": \"^12.37.0\",\n        \"next-themes\": \"^0.4.6\",\n        \"react\": \"^19.2.4\",\n        \"react-dom\": \"^19.2.4\",\n        \"rough-notation\": \"^0.5.1\",\n        \"simple-icons\": \"^16.12.0\",\n        \"sonner\": \"^2.0.7\",\n        \"tailwind-merge\": \"^3.5.0\",\n        \"tailwindcss\": \"^4.1.14\",\n        \"tw-animate-css\": \"^1.4.0\",\n        \"wouter\": \"^3.9.0\"\n    }\n}\n"
  },
  {
    "path": "packages/website/public/robots.txt",
    "content": "User-agent: *\nAllow: /\n\nSitemap: https://alibaba.github.io/page-agent/sitemap.xml\n"
  },
  {
    "path": "packages/website/src/components/APIReference.tsx",
    "content": "/**\n * API Reference component for displaying TypeScript interface definitions\n *\n * Provides a beautiful, readable table for documenting API interfaces\n */\nimport * as React from 'react'\n\nimport { Badge } from '@/components/ui/badge'\nimport { cn } from '@/lib/utils'\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface PropDefinition {\n\t/** Property name */\n\tname: string\n\t/** TypeScript type (can include generics, unions, etc.) */\n\ttype: string\n\t/** Whether the property is required */\n\trequired?: boolean\n\t/** Default value if any */\n\tdefaultValue?: string\n\t/** Description of the property */\n\tdescription: React.ReactNode\n\t/** Mark as experimental/deprecated */\n\tstatus?: 'experimental' | 'deprecated'\n}\n\nexport interface APIReferenceProps {\n\t/** Title for the API section */\n\ttitle?: string\n\t/** Optional description */\n\tdescription?: React.ReactNode\n\t/** Property definitions */\n\tproperties: PropDefinition[]\n\t/** Display variant: 'properties' for fields, 'methods' for methods */\n\tvariant?: 'properties' | 'methods'\n\t/** Additional CSS classes */\n\tclassName?: string\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function APIReference({\n\ttitle,\n\tdescription,\n\tproperties,\n\tvariant = 'properties',\n\tclassName,\n}: APIReferenceProps) {\n\tconst isMethodsVariant = variant === 'methods'\n\treturn (\n\t\t<div className={cn('my-6', className)}>\n\t\t\t{title && (\n\t\t\t\t<h3 className=\"text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2\">{title}</h3>\n\t\t\t)}\n\t\t\t{description && (\n\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-400 mb-4\">{description}</p>\n\t\t\t)}\n\n\t\t\t<div className=\"overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700\">\n\t\t\t\t<table className=\"w-full text-sm\">\n\t\t\t\t\t<thead>\n\t\t\t\t\t\t<tr className=\"bg-gray-50 dark:bg-gray-800/50\">\n\t\t\t\t\t\t\t<th className=\"px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t\t\t{isMethodsVariant ? 'Method' : 'Property'}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th className=\"px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t\t\t{isMethodsVariant ? 'Return Type' : 'Type'}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th className=\"px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300 hidden md:table-cell\">\n\t\t\t\t\t\t\t\tDefault\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th className=\"px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t\t\tDescription\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</thead>\n\t\t\t\t\t<tbody className=\"divide-y divide-gray-100 dark:divide-gray-800\">\n\t\t\t\t\t\t{properties.map((prop) => (\n\t\t\t\t\t\t\t<PropRow key={prop.name} {...prop} />\n\t\t\t\t\t\t))}\n\t\t\t\t\t</tbody>\n\t\t\t\t</table>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nfunction PropRow({ name, type, required, defaultValue, description, status }: PropDefinition) {\n\treturn (\n\t\t<tr className=\"bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors\">\n\t\t\t{/* Property name */}\n\t\t\t<td className=\"px-4 py-3 align-top\">\n\t\t\t\t<div className=\"flex items-center gap-2 flex-wrap\">\n\t\t\t\t\t<code className=\"font-mono text-sm font-medium text-indigo-600 dark:text-indigo-400\">\n\t\t\t\t\t\t{name}\n\t\t\t\t\t</code>\n\t\t\t\t\t{required && (\n\t\t\t\t\t\t<Badge\n\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\tclassName=\"text-[10px] px-1.5 py-0 border-red-300 text-red-600 dark:border-red-800 dark:text-red-400\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t</Badge>\n\t\t\t\t\t)}\n\t\t\t\t\t{status === 'experimental' && (\n\t\t\t\t\t\t<Badge\n\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\tclassName=\"text-[10px] px-1.5 py-0 border-amber-300 text-amber-600 dark:border-amber-800 dark:text-amber-400\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\texperimental\n\t\t\t\t\t\t</Badge>\n\t\t\t\t\t)}\n\t\t\t\t\t{status === 'deprecated' && (\n\t\t\t\t\t\t<Badge\n\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\tclassName=\"text-[10px] px-1.5 py-0 border-gray-300 text-gray-500 dark:border-gray-700 dark:text-gray-500 line-through\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tdeprecated\n\t\t\t\t\t\t</Badge>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</td>\n\n\t\t\t{/* Type */}\n\t\t\t<td className=\"px-4 py-3 align-top\">\n\t\t\t\t<code className=\"font-mono text-xs text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded wrap-break-word\">\n\t\t\t\t\t{type}\n\t\t\t\t</code>\n\t\t\t</td>\n\n\t\t\t{/* Default value */}\n\t\t\t<td className=\"px-4 py-3 align-top hidden md:table-cell\">\n\t\t\t\t{defaultValue ? (\n\t\t\t\t\t<code className=\"font-mono text-xs text-gray-600 dark:text-gray-400\">{defaultValue}</code>\n\t\t\t\t) : (\n\t\t\t\t\t<span className=\"text-gray-400 dark:text-gray-600\">-</span>\n\t\t\t\t)}\n\t\t\t</td>\n\n\t\t\t{/* Description */}\n\t\t\t<td className=\"px-4 py-3 align-top text-gray-600 dark:text-gray-400\">{description}</td>\n\t\t</tr>\n\t)\n}\n\n// ============================================================================\n// Utility Components\n// ============================================================================\n\n/** Code inline span for type references in descriptions */\nexport function TypeRef({ children }: { children: React.ReactNode }) {\n\treturn (\n\t\t<code className=\"font-mono text-xs text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-950/50 px-1 py-0.5 rounded\">\n\t\t\t{children}\n\t\t</code>\n\t)\n}\n\n/** Section divider for grouping related APIs */\nexport function APIDivider({ title }: { title: string }) {\n\treturn (\n\t\t<div className=\"flex items-center gap-4 my-8\">\n\t\t\t<div className=\"h-px flex-1 bg-gradient-to-r from-transparent via-gray-200 dark:via-gray-700 to-transparent\" />\n\t\t\t<span className=\"text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400\">\n\t\t\t\t{title}\n\t\t\t</span>\n\t\t\t<div className=\"h-px flex-1 bg-gradient-to-r from-transparent via-gray-200 dark:via-gray-700 to-transparent\" />\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/BetaNotice.tsx",
    "content": "import { useLanguage } from '@/i18n/context'\n\nexport default function BetaNotice() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<div className=\"bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-8\">\n\t\t\t<div className=\"flex items-start\">\n\t\t\t\t<div className=\"shrink-0\">\n\t\t\t\t\t<span className=\"text-xl\">🚧</span>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"ml-3\">\n\t\t\t\t\t<h3 className=\"text-sm font-medium text-orange-800 dark:text-orange-200 mb-1\">\n\t\t\t\t\t\t{isZh ? 'Beta 阶段' : 'Beta Stage'}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<p className=\"text-sm text-orange-700 dark:text-orange-300\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '当前功能未完成，接口可能随时变更。正式版本发布前请勿用于生产环境。'\n\t\t\t\t\t\t\t: 'Current features are incomplete and the API may change at any time. Please do not use in production environments before the official release.'}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/CodeEditor.tsx",
    "content": "/**\n * 代码编辑器组件，模拟现代代码编辑器的外观\n */\nimport React from 'react'\n\nimport HighlightSyntax from './HighlightSyntax'\n\ninterface CodeEditorProps {\n\tcode: string\n\tlanguage?: string\n\ttitle?: string\n\tshowLineNumbers?: boolean\n\tshowHeader?: boolean\n\tshowFooter?: boolean\n\tclassName?: string\n}\n\nconst CodeEditor: React.FC<CodeEditorProps> = ({\n\tcode,\n\tlanguage = 'javascript',\n\ttitle,\n\tshowLineNumbers = false,\n\tshowHeader = false,\n\tshowFooter = false,\n\tclassName = '',\n}) => {\n\tconst lines = code.split('\\n')\n\n\t// 使用 Tailwind 的 dark: 前缀实现自动主题切换\n\tconst containerClasses =\n\t\t'bg-linear-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 border-gray-300 dark:border-gray-700'\n\tconst headerClasses = 'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-700'\n\tconst headerTextClasses = 'text-gray-700 dark:text-gray-300'\n\tconst languageTextClasses = 'text-gray-600 dark:text-gray-400'\n\tconst lineNumbersClasses =\n\t\t'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-500'\n\tconst codeAreaClasses = 'bg-white dark:bg-gray-900'\n\tconst footerClasses =\n\t\t'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-400'\n\tconst copyButtonClasses =\n\t\t'bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-white'\n\n\treturn (\n\t\t<div\n\t\t\tclassName={`group relative ${containerClasses} rounded-xl border shadow-2xl my-4 overflow-hidden ${className}`}\n\t\t>\n\t\t\t{/* 编辑器顶部栏 */}\n\t\t\t{showHeader && (\n\t\t\t\t<div className={`flex items-center justify-between px-4 py-3 ${headerClasses} border-b`}>\n\t\t\t\t\t<div className=\"flex items-center space-x-3\">\n\t\t\t\t\t\t{/* 窗口控制按钮 */}\n\t\t\t\t\t\t<div className=\"flex space-x-2\">\n\t\t\t\t\t\t\t<div className=\"w-3 h-3 bg-red-500 rounded-full\"></div>\n\t\t\t\t\t\t\t<div className=\"w-3 h-3 bg-yellow-500 rounded-full\"></div>\n\t\t\t\t\t\t\t<div className=\"w-3 h-3 bg-green-500 rounded-full\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t{title && (\n\t\t\t\t\t\t\t<span className={`text-sm ${headerTextClasses} font-medium ml-2`}>{title}</span>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"flex items-center space-x-2\">\n\t\t\t\t\t\t<span className={`text-xs ${languageTextClasses} uppercase tracking-wide`}>\n\t\t\t\t\t\t\t{language}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<div className=\"w-2 h-2 bg-green-400 rounded-full animate-pulse\"></div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t{/* 代码内容区域 */}\n\t\t\t<div className=\"relative\">\n\t\t\t\t<div className=\"flex\">\n\t\t\t\t\t{/* 行号 */}\n\t\t\t\t\t{showLineNumbers && (\n\t\t\t\t\t\t<div className={`shrink-0 px-4 py-4 ${lineNumbersClasses} border-r select-none`}>\n\t\t\t\t\t\t\t<div className=\"text-xs font-mono leading-6\">\n\t\t\t\t\t\t\t\t{lines.map((line, lineIdx) => {\n\t\t\t\t\t\t\t\t\tconst lineNum = lineIdx + 1\n\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t<div key={`${lineNum}-${line.substring(0, 20)}`} className=\"text-right\">\n\t\t\t\t\t\t\t\t\t\t\t{lineNum}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\n\t\t\t\t\t{/* 代码内容 */}\n\t\t\t\t\t<div className={`flex-1 px-4 py-4 ${codeAreaClasses} overflow-x-auto`}>\n\t\t\t\t\t\t<div className=\"text-sm font-mono leading-6\">\n\t\t\t\t\t\t\t<HighlightSyntax code={code} />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t{/* 复制按钮 */}\n\t\t\t\t<button\n\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\tnavigator.clipboard.writeText(code).catch(console.error)\n\t\t\t\t\t}}\n\t\t\t\t\tclassName={`absolute top-3 right-3 p-2 ${copyButtonClasses} rounded-lg transition-all duration-200 opacity-0 group-hover:opacity-100`}\n\t\t\t\t\ttitle=\"复制代码\"\n\t\t\t\t>\n\t\t\t\t\t<svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\td=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</svg>\n\t\t\t\t</button>\n\t\t\t</div>\n\n\t\t\t{/* 底部状态栏 */}\n\t\t\t{showFooter && (\n\t\t\t\t<div className={`px-4 py-2 ${footerClasses} border-t`}>\n\t\t\t\t\t<div className=\"flex items-center justify-between text-xs\">\n\t\t\t\t\t\t<span>{lines.length} lines</span>\n\t\t\t\t\t\t<span>UTF-8</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n\nexport default CodeEditor\n"
  },
  {
    "path": "packages/website/src/components/Footer.tsx",
    "content": "import { siGithub, siX } from 'simple-icons'\n\nimport { useLanguage } from '@/i18n/context'\n\nexport default function Footer() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<footer\n\t\t\tclassName=\"bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700\"\n\t\t\trole=\"contentinfo\"\n\t\t>\n\t\t\t<div className=\"max-w-7xl mx-auto px-6 py-6\">\n\t\t\t\t<div className=\"flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0\">\n\t\t\t\t\t<div className=\"text-gray-600 dark:text-gray-300 text-sm text-center md:text-left\">\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"https://x.com/simonluvramen\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\tclassName=\"inline-block bg-[linear-gradient(60deg,#39b6ff_0%,#bd45fb_33%,#ff5733_66%,#ffd600_100%)] bg-clip-text text-xs leading-none text-transparent font-mono transition-opacity duration-200 hover:opacity-85\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tSimon.\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 text-xs mt-0.5\">\n\t\t\t\t\t\t\t© 2026 page-agent. All rights reserved.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div className=\"flex items-center\">\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md\"\n\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\tclassName=\"text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200 text-sm mr-4\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{isZh ? '使用条款与隐私' : 'Terms & Privacy'}\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\thref=\"https://x.com/simonluvramen\"\n\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\tclassName=\"text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200 mr-4\"\n\t\t\t\t\t\t\taria-label=\"X (Twitter)\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\trole=\"img\"\n\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\tclassName=\"w-4 h-4 fill-current\"\n\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<path d={siX.path} />\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent\"\n\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\tclassName=\"text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200\"\n\t\t\t\t\t\t\taria-label={isZh ? '访问 GitHub 仓库' : 'Visit GitHub repository'}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\trole=\"img\"\n\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\tclassName=\"w-5 h-5 fill-current\"\n\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<path d={siGithub.path} />\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</footer>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/Header.tsx",
    "content": "import { BookOpen, Menu, X } from 'lucide-react'\nimport { useState } from 'react'\nimport { siGithub } from 'simple-icons'\nimport { Link } from 'wouter'\n\nimport { formatStars, useGitHubStars } from '@/hooks/useGitHubStars'\nimport { useLanguage } from '@/i18n/context'\n\nimport LanguageSwitcher from './LanguageSwitcher'\nimport ThemeSwitcher from './ThemeSwitcher'\nimport { HyperText } from './ui/hyper-text'\n\nexport default function Header() {\n\tconst { isZh } = useLanguage()\n\tconst [mobileMenuOpen, setMobileMenuOpen] = useState(false)\n\tconst stars = useGitHubStars()\n\n\treturn (\n\t\t<>\n\t\t\t<header\n\t\t\t\tclassName=\"relative z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700\"\n\t\t\t\trole=\"banner\"\n\t\t\t>\n\t\t\t\t<div className=\"max-w-7xl mx-auto px-6 py-4\">\n\t\t\t\t\t<div className=\"flex items-center justify-between gap-2\">\n\t\t\t\t\t\t{/* Logo */}\n\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\thref=\"/\"\n\t\t\t\t\t\t\tclassName=\"flex items-center gap-2 sm:gap-3 group shrink-0\"\n\t\t\t\t\t\t\taria-label={isZh ? 'page-agent 首页' : 'page-agent home'}\n\t\t\t\t\t\t\tonClick={() => setMobileMenuOpen(false)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc=\"https://img.alicdn.com/imgextra/i2/O1CN01HB8ylu1uozANEMZw2_!!6000000006085-49-tps-128-128.webp\"\n\t\t\t\t\t\t\t\talt=\"PageAgent Logo\"\n\t\t\t\t\t\t\t\tclassName=\"w-10 h-10 rounded-xl group-hover:scale-110 transition-transform duration-200\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<span className=\"text-base sm:text-xl font-bold text-gray-900 dark:text-white leading-tight flex items-baseline gap-1.5\">\n\t\t\t\t\t\t\t\t\tpage-agent\n\t\t\t\t\t\t\t\t\t<span className=\"hidden sm:inline text-[10px] font-mono font-normal text-gray-400 dark:text-gray-500 tabular-nums before:content-['v']\">\n\t\t\t\t\t\t\t\t\t\t{import.meta.env.VERSION}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<HyperText\n\t\t\t\t\t\t\t\t\tas=\"p\"\n\t\t\t\t\t\t\t\t\tclassName=\"hidden sm:block text-xs text-gray-600 dark:text-gray-300 py-0 font-normal overflow-visible\"\n\t\t\t\t\t\t\t\t\tduration={600}\n\t\t\t\t\t\t\t\t\tanimateOnHover={true}\n\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\tAI Agent In Your Webpage\n\t\t\t\t\t\t\t\t</HyperText>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Link>\n\n\t\t\t\t\t\t{/* Mobile Icon Navigation (横向滚动) */}\n\t\t\t\t\t\t<nav\n\t\t\t\t\t\t\tclassName=\"md:hidden flex items-center gap-1 overflow-x-auto scrollbar-hide flex-1\"\n\t\t\t\t\t\t\trole=\"navigation\"\n\t\t\t\t\t\t\taria-label=\"Mobile navigation\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\thref=\"/docs/introduction/overview\"\n\t\t\t\t\t\t\t\tclassName=\"p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0\"\n\t\t\t\t\t\t\t\taria-label={isZh ? '文档' : 'Docs'}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<BookOpen className=\"w-5 h-5\" />\n\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\tclassName=\"flex items-center gap-1 p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0\"\n\t\t\t\t\t\t\t\taria-label=\"GitHub\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\t\trole=\"img\"\n\t\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\t\tclassName=\"w-5 h-5 fill-current\"\n\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<path d={siGithub.path} />\n\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t\t{stars !== null && (\n\t\t\t\t\t\t\t\t\t<span className=\"text-sm tabular-nums\">★ {formatStars(stars)}</span>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</nav>\n\n\t\t\t\t\t\t{/* Desktop Navigation */}\n\t\t\t\t\t\t<nav\n\t\t\t\t\t\t\tclassName=\"hidden md:flex items-center space-x-6\"\n\t\t\t\t\t\t\trole=\"navigation\"\n\t\t\t\t\t\t\taria-label={isZh ? '文档' : 'Docs'}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\thref=\"/docs/introduction/overview\"\n\t\t\t\t\t\t\t\tclassName=\"flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<BookOpen className=\"w-4 h-4\" />\n\t\t\t\t\t\t\t\t{isZh ? '文档' : 'Docs'}\n\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\tclassName=\"flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200\"\n\t\t\t\t\t\t\t\taria-label=\"GitHub\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\t\trole=\"img\"\n\t\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\t\tclassName=\"w-4 h-4 fill-current\"\n\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<path d={siGithub.path} />\n\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t\tGitHub\n\t\t\t\t\t\t\t\t{stars !== null && (\n\t\t\t\t\t\t\t\t\t<span className=\"text-sm font-medium tabular-nums \">★ {formatStars(stars)}</span>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<ThemeSwitcher />\n\t\t\t\t\t\t\t<LanguageSwitcher />\n\t\t\t\t\t\t</nav>\n\n\t\t\t\t\t\t{/* Mobile menu button */}\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclassName=\"md:hidden p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200 shrink-0\"\n\t\t\t\t\t\t\taria-label={isZh ? '打开导航栏' : 'Open navigation'}\n\t\t\t\t\t\t\taria-expanded={mobileMenuOpen}\n\t\t\t\t\t\t\taria-controls=\"mobile-menu\"\n\t\t\t\t\t\t\tonClick={() => setMobileMenuOpen(!mobileMenuOpen)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{mobileMenuOpen ? <X className=\"w-6 h-6\" /> : <Menu className=\"w-6 h-6\" />}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t{/* Mobile Navigation */}\n\t\t\t\t\t{mobileMenuOpen && (\n\t\t\t\t\t\t<nav\n\t\t\t\t\t\t\tid=\"mobile-menu\"\n\t\t\t\t\t\t\tclassName=\"md:hidden pt-4 pb-2 space-y-3 border-t border-gray-200 dark:border-gray-700 mt-4\"\n\t\t\t\t\t\t\trole=\"navigation\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\thref=\"/docs/introduction/overview\"\n\t\t\t\t\t\t\t\tclassName=\"flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200\"\n\t\t\t\t\t\t\t\tonClick={() => setMobileMenuOpen(false)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<BookOpen className=\"w-5 h-5\" />\n\t\t\t\t\t\t\t\t{isZh ? '文档' : 'Docs'}\n\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\tclassName=\"flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200\"\n\t\t\t\t\t\t\t\taria-label=\"GitHub\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\t\trole=\"img\"\n\t\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\t\tclassName=\"w-5 h-5 fill-current\"\n\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<path d={siGithub.path} />\n\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t\tGitHub\n\t\t\t\t\t\t\t\t{stars !== null && (\n\t\t\t\t\t\t\t\t\t<span className=\"text-xs tabular-nums text-gray-400 dark:text-gray-500\">\n\t\t\t\t\t\t\t\t\t\t★ {formatStars(stars)}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<div className=\"flex items-center gap-3 px-3 py-2\">\n\t\t\t\t\t\t\t\t<ThemeSwitcher />\n\t\t\t\t\t\t\t\t<LanguageSwitcher />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</nav>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</header>\n\t\t</>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/Heading.tsx",
    "content": "import { ComponentPropsWithoutRef, useEffect, useRef } from 'react'\n\nimport { cn } from '@/lib/utils'\n\ntype Level = 2 | 3\n\ninterface HeadingProps extends Omit<ComponentPropsWithoutRef<'h2'>, 'children'> {\n\tid: string\n\tlevel?: Level\n\tchildren: React.ReactNode\n}\n\nconst levelStyles = {\n\t2: { tag: 'h2', className: 'text-2xl font-semibold mb-4' },\n\t3: { tag: 'h3', className: 'text-xl font-semibold mb-3' },\n} as const\n\nexport function Heading({ id, level = 2, className, children, ...props }: HeadingProps) {\n\tconst ref = useRef<HTMLHeadingElement>(null)\n\tconst { tag: Tag, className: defaultClassName } = levelStyles[level]\n\n\tuseEffect(() => {\n\t\tif (window.location.hash === `#${id}`) {\n\t\t\tref.current?.scrollIntoView({ behavior: 'smooth' })\n\t\t}\n\t}, [id])\n\n\treturn (\n\t\t<Tag\n\t\t\tref={ref}\n\t\t\tid={id}\n\t\t\tclassName={cn('group relative scroll-mt-20', defaultClassName, className)}\n\t\t\t{...props}\n\t\t>\n\t\t\t<a\n\t\t\t\thref={`#${id}`}\n\t\t\t\tclassName=\"absolute -left-5 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 text-gray-400 hover:text-blue-500 transition-opacity no-underline\"\n\t\t\t\taria-label={`Link to ${id}`}\n\t\t\t>\n\t\t\t\t#\n\t\t\t</a>\n\t\t\t{children}\n\t\t</Tag>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/HighlightSyntax.module.css",
    "content": ".syntax {\n\twhite-space: pre-wrap;\n\tword-break: break-word;\n\toverflow-wrap: break-word;\n\tfont-family: monospace;\n\tfont-size: 13px;\n\tline-height: 1;\n\tcolor: #171717;\n}\n\n:global(.dark) .syntax {\n\tcolor: #e0e0e0;\n}\n\n/* JavaScript/TypeScript 关键字 */\n.keyword {\n\tcolor: #d73a49;\n\tfont-weight: 600;\n}\n\n:global(.dark) .keyword {\n\tcolor: #ff6b6b;\n}\n\n/* TypeScript 特定关键字 (interface, type, enum, etc.) */\n.tsKeyword {\n\tcolor: #af00db;\n\tfont-weight: 600;\n}\n\n:global(.dark) .tsKeyword {\n\tcolor: #c792ea;\n}\n\n/* TypeScript 内置类型 */\n.type {\n\tcolor: #267f99;\n\tfont-weight: 500;\n}\n\n:global(.dark) .type {\n\tcolor: #4ec9b0;\n}\n\n/* 字符串 */\n.string {\n\tcolor: #1d6eca;\n}\n\n:global(.dark) .string {\n\tcolor: #4fc3f7;\n}\n\n/* 数字 */\n.number {\n\tcolor: #00c583;\n}\n\n:global(.dark) .number {\n\tcolor: #66bb6a;\n}\n\n/* 布尔值和字面量 (true, false, null, undefined) */\n.literal {\n\tcolor: #0000ff;\n\tfont-weight: 500;\n}\n\n:global(.dark) .literal {\n\tcolor: #569cd6;\n}\n\n/* 注释 */\n.comment {\n\tcolor: #6a737d;\n\tfont-style: italic;\n}\n\n:global(.dark) .comment {\n\tcolor: #9e9e9e;\n}\n\n/* 装饰器 (@decorator) */\n.decorator {\n\tcolor: #e0aa00;\n\tfont-weight: 500;\n}\n\n:global(.dark) .decorator {\n\tcolor: #dcdcaa;\n}\n\n/* 箭头函数 (=>) */\n.arrow {\n\tcolor: #d73a49;\n\tfont-weight: bold;\n}\n\n:global(.dark) .arrow {\n\tcolor: #ff6b6b;\n}\n\n/* 标识符（变量名、函数名等） */\n.identifier {\n\tcolor: #171717;\n}\n\n:global(.dark) .identifier {\n\tcolor: #e0e0e0;\n}\n\n/* 属性访问 (.property) */\n.property {\n\tcolor: #0550ae;\n}\n\n:global(.dark) .property {\n\tcolor: #9cdcfe;\n}\n\n/* 运算符 */\n.operator {\n\tcolor: #5a5a5a;\n}\n\n:global(.dark) .operator {\n\tcolor: #d4d4d4;\n}\n"
  },
  {
    "path": "packages/website/src/components/HighlightSyntax.tsx",
    "content": "/**\n * js 语法高亮组件，适合在文章中演示代码片段\n */\nimport React from 'react'\n\nimport styles from './HighlightSyntax.module.css'\n\ninterface HighlightSyntaxProps {\n\tcode: string\n}\n\n// JavaScript/TypeScript 关键字\nconst keywords =\n\t'async|await|function|const|let|var|if|else|for|while|return|try|catch|finally|class|extends|from|import|export|default|undefined|throw|break|continue|switch|case|do|with|yield|delete|typeof|void|static|get|set|super|debugger'\n\n// TypeScript 特定关键字\nconst tsKeywords =\n\t'interface|type|enum|namespace|module|declare|abstract|implements|public|private|protected|readonly|as|satisfies|infer|keyof|is'\n\n// 布尔值和空值\nconst literals = 'true|false|null|undefined|NaN|Infinity'\n\n// TypeScript 内置类型\nconst tsTypes =\n\t'string|number|boolean|any|unknown|never|void|object|symbol|bigint|Array|Promise|Record|Partial|Required|Readonly|Pick|Omit|Exclude|Extract|NonNullable|ReturnType|Parameters|ConstructorParameters|InstanceType|ThisType|Uppercase|Lowercase|Capitalize|Uncapitalize'\n\n// 辅助函数：转义 HTML 特殊字符\nfunction escapeHtml(text: string): string {\n\treturn text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\n// 语法高亮函数，先提取 token 再转义和高亮\nfunction highlightSyntax(code: string): string {\n\t// 构建正则模式，包含更多 token 类型（在原始文本上匹配）\n\tconst pattern = new RegExp(\n\t\t'(' +\n\t\t\t// 1. 字符串（双引号、单引号、模板字符串）\n\t\t\t'\"([^\"\\\\\\\\]|\\\\\\\\.)*\"|' +\n\t\t\t\"'([^'\\\\\\\\]|\\\\\\\\.)*'|\" +\n\t\t\t'`([^`\\\\\\\\]|\\\\\\\\.)*`|' +\n\t\t\t// 2. 注释（单行和多行）\n\t\t\t'//[^\\\\n]*|' +\n\t\t\t'/\\\\*[\\\\s\\\\S]*?\\\\*/|' +\n\t\t\t// 3. 装饰器\n\t\t\t'@[a-zA-Z_$][\\\\w$]*|' +\n\t\t\t// 4. 数字（包括小数、十六进制、科学计数法）\n\t\t\t'\\\\b0[xX][0-9a-fA-F]+\\\\b|' +\n\t\t\t'\\\\b\\\\d+\\\\.?\\\\d*(?:[eE][+-]?\\\\d+)?\\\\b|' +\n\t\t\t// 5. TypeScript/JavaScript 关键字\n\t\t\t'\\\\b(?:' +\n\t\t\tkeywords +\n\t\t\t'|' +\n\t\t\ttsKeywords +\n\t\t\t'|' +\n\t\t\tliterals +\n\t\t\t')\\\\b|' +\n\t\t\t// 6. TypeScript 内置类型\n\t\t\t'\\\\b(?:' +\n\t\t\ttsTypes +\n\t\t\t')\\\\b|' +\n\t\t\t// 7. 箭头函数\n\t\t\t'=>|' +\n\t\t\t// 8. 函数调用（函数名后跟括号）\n\t\t\t'\\\\b[a-zA-Z_$][\\\\w$]*(?=\\\\()|' +\n\t\t\t// 9. 属性访问\n\t\t\t'\\\\.[a-zA-Z_$][\\\\w$]*|' +\n\t\t\t// 10. 运算符和特殊符号\n\t\t\t'[+\\\\-*/%&|^!~<>=?:]+|' +\n\t\t\t'[{}\\\\[\\\\]();,.]' +\n\t\t\t')',\n\t\t'g'\n\t)\n\n\tconst tokens: string[] = []\n\tlet lastIndex = 0\n\tlet match: RegExpExecArray | null\n\twhile ((match = pattern.exec(code)) !== null) {\n\t\tif (match.index > lastIndex) {\n\t\t\tconst gap = code.slice(lastIndex, match.index)\n\t\t\t// 将间隙按空白符分割，保留空白符\n\t\t\ttokens.push(...gap.split(/(\\s+)/))\n\t\t}\n\t\ttokens.push(match[0])\n\t\tlastIndex = pattern.lastIndex\n\t}\n\tif (lastIndex < code.length) {\n\t\ttokens.push(...code.slice(lastIndex).split(/(\\s+)/))\n\t}\n\n\tconst highlighted = tokens\n\t\t.map((token) => {\n\t\t\t// 空白符直接返回\n\t\t\tif (/^\\s+$/.test(token)) {\n\t\t\t\treturn token\n\t\t\t}\n\n\t\t\t// 1. 注释（单行和多行）\n\t\t\tif (/^\\/\\/.*$/.test(token) || /^\\/\\*[\\s\\S]*?\\*\\/$/.test(token)) {\n\t\t\t\treturn `<span class=\"${styles.comment}\">${escapeHtml(token)}</span>`\n\t\t\t}\n\n\t\t\t// 2. 字符串\n\t\t\tif (\n\t\t\t\t/^\"([^\"\\\\]|\\\\.)*\"$/.test(token) ||\n\t\t\t\t/^'([^'\\\\]|\\\\.)*'$/.test(token) ||\n\t\t\t\t/^`([^`\\\\]|\\\\.)*`$/.test(token)\n\t\t\t) {\n\t\t\t\treturn `<span class=\"${styles.string}\">${escapeHtml(token)}</span>`\n\t\t\t}\n\n\t\t\t// 3. 数字\n\t\t\tif (/^(0[xX][0-9a-fA-F]+|\\d+\\.?\\d*(?:[eE][+-]?\\d+)?)$/.test(token)) {\n\t\t\t\treturn `<span class=\"${styles.number}\">${escapeHtml(token)}</span>`\n\t\t\t}\n\n\t\t\t// 4. 布尔值和特殊字面量\n\t\t\tif (new RegExp(`^(?:${literals})$`).test(token)) {\n\t\t\t\treturn `<span class=\"${styles.literal}\">${escapeHtml(token)}</span>`\n\t\t\t}\n\n\t\t\t// 5. JavaScript/TypeScript 关键字\n\t\t\tif (new RegExp(`^(?:${keywords})$`).test(token)) {\n\t\t\t\treturn `<span class=\"${styles.keyword}\">${escapeHtml(token)}</span>`\n\t\t\t}\n\n\t\t\t// 6. TypeScript 特定关键字\n\t\t\tif (new RegExp(`^(?:${tsKeywords})$`).test(token)) {\n\t\t\t\treturn `<span class=\"${styles.tsKeyword}\">${escapeHtml(token)}</span>`\n\t\t\t}\n\n\t\t\t// 7. TypeScript 内置类型\n\t\t\tif (new RegExp(`^(?:${tsTypes})$`).test(token)) {\n\t\t\t\treturn `<span class=\"${styles.type}\">${escapeHtml(token)}</span>`\n\t\t\t}\n\n\t\t\t// 8. 装饰器\n\t\t\tif (/^@[a-zA-Z_$][\\w$]*$/.test(token)) {\n\t\t\t\treturn `<span class=\"${styles.decorator}\">${escapeHtml(token)}</span>`\n\t\t\t}\n\n\t\t\t// 9. 箭头函数\n\t\t\tif (token === '=>') {\n\t\t\t\treturn `<span class=\"${styles.arrow}\">${escapeHtml(token)}</span>`\n\t\t\t}\n\n\t\t\t// 10. 函数调用和标识符\n\t\t\tif (/^[a-zA-Z_$][\\w$]*$/.test(token)) {\n\t\t\t\treturn `<span class=\"${styles.identifier}\">${escapeHtml(token)}</span>`\n\t\t\t}\n\n\t\t\t// 11. 属性访问\n\t\t\tif (/^\\.[a-zA-Z_$][\\w$]*$/.test(token)) {\n\t\t\t\treturn `<span class=\"${styles.property}\">${escapeHtml(token)}</span>`\n\t\t\t}\n\n\t\t\t// 12. 运算符\n\t\t\tif (/^[+\\-*/%&|^!~<>=?:]+$/.test(token)) {\n\t\t\t\treturn `<span class=\"${styles.operator}\">${escapeHtml(token)}</span>`\n\t\t\t}\n\n\t\t\t// 13. 其他符号，需要转义\n\t\t\treturn escapeHtml(token)\n\t\t})\n\t\t.join('')\n\n\treturn highlighted\n}\n\nconst HighlightSyntaxClient: React.FC<HighlightSyntaxProps> = ({ code }) => {\n\tconst htmlContent = highlightSyntax(code)\n\n\t// eslint-disable-next-line react-dom/no-dangerously-set-innerhtml\n\treturn <code className={styles.syntax} dangerouslySetInnerHTML={{ __html: htmlContent }} />\n}\n\nexport default HighlightSyntaxClient\n"
  },
  {
    "path": "packages/website/src/components/JSConsole.module.css",
    "content": ".console {\n\tdisplay: flex;\n\tflex-direction: column;\n\tbackground-color: #ffffff;\n\tborder: 1px solid #e0e0e0;\n\tborder-radius: 8px;\n\tfont-family: monospace;\n\tfont-size: 12px;\n\tline-height: 1;\n\toverflow: hidden;\n\tscroll-behavior: none;\n\tbox-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n}\n\n.historyArea {\n\tflex: 1;\n\toverflow-y: auto;\n\tpadding: 12px;\n\tbackground-color: #fafafa;\n\tmin-height: 200px;\n\tdisplay: flex;\n\tflex-direction: column;\n\n\tscroll-behavior: contain;\n\n\t&::-webkit-scrollbar {\n\t\twidth: 6px;\n\t}\n\n\t&::-webkit-scrollbar-track {\n\t\tbackground: transparent;\n\t}\n\n\t&::-webkit-scrollbar-thumb {\n\t\tbackground-color: #d0d0d0;\n\t\tborder-radius: 3px;\n\t}\n\n\t&::-webkit-scrollbar-thumb:hover {\n\t\tbackground-color: #b0b0b0;\n\t}\n\n\t.historyItem {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\n\t\tfont-size: 12px;\n\t\tline-height: 1;\n\t\tpadding-bottom: 6px;\n\t\tborder-bottom: #ccdeeebd 1px solid;\n\t\tmargin-bottom: 6px;\n\n\t\tflex: 0 0 auto;\n\n\t\t&:last-child {\n\t\t\tmargin-bottom: 0;\n\t\t\tborder-bottom: none;\n\t\t}\n\n\t\t&.input {\n\t\t}\n\t\t&.output {\n\t\t}\n\n\t\t.content {\n\t\t\tmargin: 0;\n\t\t\twhite-space: pre-wrap;\n\t\t\tword-break: break-word;\n\t\t\tflex: 1;\n\t\t\tfont-family: inherit;\n\t\t\tfont-size: inherit;\n\t\t\tline-height: inherit;\n\t\t\tcolor: #2563eb;\n\t\t}\n\n\t\t/* 错误样式 */\n\t\t&.error .content {\n\t\t\tcolor: #dc2626;\n\t\t\tbackground-color: #fef2f2;\n\t\t\tpadding: 4px 8px;\n\t\t\tborder-radius: 4px;\n\t\t\tborder-left: 3px solid #dc2626;\n\t\t}\n\t}\n}\n\n.prompt {\n\tdisplay: flex;\n\theight: 100%;\n\talign-items: flex-start;\n\twidth: 12px;\n\tcolor: #666;\n\tmargin-right: 8px;\n\tfont-weight: 500;\n\tflex-shrink: 0;\n\tuser-select: none;\n}\n\n.executing {\n\tcolor: #f59e0b;\n\tfont-style: italic;\n\tanimation: pulse 1.5s ease-in-out infinite;\n}\n\n@keyframes pulse {\n\t0%,\n\t100% {\n\t\topacity: 1;\n\t}\n\t50% {\n\t\topacity: 0.5;\n\t}\n}\n\n.inputArea {\n\tdisplay: flex;\n\talign-items: center;\n\n\tpadding: 12px;\n\tbackground-color: #ffffff;\n\tborder-top: 1px solid #e0e0e0;\n\n\t.prompt {\n\t\tmargin-top: 8px;\n\t}\n\n\t.input {\n\t\tflex: auto;\n\t\tborder: none;\n\t\toutline: none;\n\t\tbackground: transparent;\n\t\tcolor: #333;\n\t\tresize: none;\n\n\t\tline-height: 20px;\n\t}\n\n\t.input::placeholder {\n\t\tcolor: #999;\n\t\tfont-style: italic;\n\t}\n\n\t.input:disabled {\n\t\topacity: 0.6;\n\t\tcursor: not-allowed;\n\t}\n}\n\n/* 响应式设计 */\n@media (max-width: 768px) {\n\t.console {\n\t\tfont-size: 12px;\n\t\tborder-radius: 6px;\n\t}\n\n\t.historyArea,\n\t.inputLine {\n\t\tpadding: 8px;\n\t}\n\n\t.prompt {\n\t\tmargin-right: 6px;\n\t}\n}\n"
  },
  {
    "path": "packages/website/src/components/JSConsole.tsx",
    "content": "/**\n * JS 调试台，适合在文档中直接让用户运行代码，并且实时查看运行结果\n */\n/* eslint-disable @typescript-eslint/no-base-to-string */\nimport { KeyboardEvent, useEffect, useImperativeHandle, useRef, useState } from 'react'\n\nimport HighlightSyntax from './HighlightSyntax'\n\nimport styles from './JSConsole.module.css'\n\n// 全局console拦截管理器\nclass ConsoleInterceptor {\n\tprivate static instance: ConsoleInterceptor\n\tprivate subscribers = new Set<(type: string, args: unknown[]) => void>()\n\tprivate originalConsole: {\n\t\tlog: typeof console.log\n\t\twarn: typeof console.warn\n\t\terror: typeof console.error\n\t}\n\tprivate isIntercepting = false\n\n\tprivate constructor() {\n\t\tthis.originalConsole = {\n\t\t\tlog: console.log.bind(console),\n\t\t\twarn: console.warn.bind(console),\n\t\t\terror: console.error.bind(console),\n\t\t}\n\t}\n\n\tstatic getInstance() {\n\t\tif (!ConsoleInterceptor.instance) {\n\t\t\tConsoleInterceptor.instance = new ConsoleInterceptor()\n\t\t}\n\t\treturn ConsoleInterceptor.instance\n\t}\n\n\tsubscribe(callback: (type: string, args: unknown[]) => void) {\n\t\tthis.subscribers.add(callback)\n\t\tthis.startIntercepting()\n\t}\n\n\tunsubscribe(callback: (type: string, args: unknown[]) => void) {\n\t\tthis.subscribers.delete(callback)\n\t\tif (this.subscribers.size === 0) {\n\t\t\tthis.stopIntercepting()\n\t\t}\n\t}\n\n\tprivate startIntercepting() {\n\t\tif (this.isIntercepting) return\n\n\t\tthis.isIntercepting = true\n\n\t\tconsole.log = (...args: unknown[]) => {\n\t\t\tthis.originalConsole.log(...args)\n\t\t\tthis.notifySubscribers('log', args)\n\t\t}\n\n\t\tconsole.warn = (...args: unknown[]) => {\n\t\t\tthis.originalConsole.warn(...args)\n\t\t\tthis.notifySubscribers('warn', args)\n\t\t}\n\n\t\tconsole.error = (...args: unknown[]) => {\n\t\t\tthis.originalConsole.error(...args)\n\t\t\tthis.notifySubscribers('error', args)\n\t\t}\n\t}\n\n\tprivate stopIntercepting() {\n\t\tif (!this.isIntercepting) return\n\n\t\tthis.isIntercepting = false\n\t\tconsole.log = this.originalConsole.log\n\t\tconsole.warn = this.originalConsole.warn\n\t\tconsole.error = this.originalConsole.error\n\t}\n\n\tprivate notifySubscribers(type: string, args: unknown[]) {\n\t\tthis.subscribers.forEach((callback) => {\n\t\t\tcallback(type, args)\n\t\t})\n\t}\n}\n\ninterface JSConsoleProps {\n\tcontext?: Record<string, unknown>\n\theight?: string\n\tonExecute?: (code: string, result: unknown) => void\n\tplaceholder?: string\n\tref?: React.Ref<JSConsoleRef>\n}\n\nexport interface JSConsoleRef {\n\texecuteCode: (code: string) => Promise<unknown>\n\tclear: () => void\n\tappendOutput: (content: string) => void\n}\n\ninterface OutputItem {\n\ttype: 'input' | 'output' | 'error' | 'log'\n\tcontent: string\n\ttimestamp: number\n}\n\nconst DEFAULT_CONTEXT = {}\n\nfunction JSConsole({\n\tcontext = DEFAULT_CONTEXT,\n\theight = '400px',\n\tonExecute,\n\tplaceholder = 'Enter JavaScript code...',\n\tref,\n}: JSConsoleProps) {\n\tconst [input, setInput] = useState('')\n\tconst [outputs, setOutputs] = useState<OutputItem[]>([])\n\tconst [isExecuting, setIsExecuting] = useState(false)\n\tconst inputRef = useRef<HTMLTextAreaElement>(null)\n\tconst outputRef = useRef<HTMLDivElement>(null)\n\n\t// 持久的执行上下文，用于多轮对话共享作用域\n\tconst executionContextRef = useRef<Record<string, unknown>>({})\n\n\t// 格式化结果\n\tconst formatResult = (value: unknown): string => {\n\t\tif (value === null) return 'null'\n\t\tif (value === undefined) return 'undefined'\n\t\tif (typeof value === 'string') return `\"${value}\"`\n\t\tif (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`\n\t\tif (typeof value === 'object') {\n\t\t\ttry {\n\t\t\t\treturn JSON.stringify(value, null, 2)\n\t\t\t} catch {\n\t\t\t\treturn value.toString()\n\t\t\t}\n\t\t}\n\t\treturn String(value)\n\t}\n\n\t// 全局console拦截处理\n\tuseEffect(() => {\n\t\tconst interceptor = ConsoleInterceptor.getInstance()\n\n\t\tconst handleGlobalConsole = (type: string, args: unknown[]) => {\n\t\t\tconst content = args.map((arg) => formatResult(arg)).join(' ')\n\n\t\t\tconst outputItem: OutputItem = {\n\t\t\t\ttype: type as any,\n\t\t\t\tcontent: content,\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t}\n\n\t\t\tsetOutputs((prev) => [...prev, outputItem])\n\n\t\t\t// 自动滚动到底部\n\t\t\tsetTimeout(() => {\n\t\t\t\tif (outputRef.current) {\n\t\t\t\t\toutputRef.current.scrollTop = outputRef.current.scrollHeight\n\t\t\t\t}\n\t\t\t}, 0)\n\t\t}\n\n\t\tinterceptor.subscribe(handleGlobalConsole)\n\n\t\treturn () => {\n\t\t\tinterceptor.unsubscribe(handleGlobalConsole)\n\t\t}\n\t}, [])\n\n\t// 执行代码\n\tconst executeCode = async (code: string): Promise<unknown> => {\n\t\tif (!code.trim()) return\n\n\t\tsetIsExecuting(true)\n\n\t\t// 添加输入到输出\n\t\tconst inputItem: OutputItem = {\n\t\t\ttype: 'input',\n\t\t\tcontent: code,\n\t\t\ttimestamp: Date.now(),\n\t\t}\n\n\t\tsetOutputs((prev) => [...prev, inputItem])\n\n\t\ttry {\n\t\t\t// 创建异步函数以支持 await\n\t\t\tconst AsyncFunction = Object.getPrototypeOf(async function () {}).constructor\n\n\t\t\t// 合并外部上下文和持久执行上下文\n\t\t\tconst allContext = { ...context, ...executionContextRef.current }\n\t\t\tconst contextKeys = Object.keys(allContext)\n\t\t\tconst contextValues = Object.values(allContext)\n\n\t\t\t// 注入 console.log 重定向\n\t\t\tconst logs: string[] = []\n\t\t\tconst mockConsole = {\n\t\t\t\tlog: (...args: unknown[]) => {\n\t\t\t\t\tlogs.push(args.map((arg) => formatResult(arg)).join(' '))\n\t\t\t\t},\n\t\t\t\terror: (...args: unknown[]) => {\n\t\t\t\t\tlogs.push('ERROR: ' + args.map((arg) => formatResult(arg)).join(' '))\n\t\t\t\t},\n\t\t\t\twarn: (...args: unknown[]) => {\n\t\t\t\t\tlogs.push('WARN: ' + args.map((arg) => formatResult(arg)).join(' '))\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t// 检测代码是否是表达式还是语句\n\t\t\tconst trimmedCode = code.trim()\n\t\t\tconst isExpression =\n\t\t\t\t!trimmedCode.includes(';') &&\n\t\t\t\t!trimmedCode.startsWith('const ') &&\n\t\t\t\t!trimmedCode.startsWith('let ') &&\n\t\t\t\t!trimmedCode.startsWith('var ') &&\n\t\t\t\t!trimmedCode.startsWith('function ') &&\n\t\t\t\t!trimmedCode.startsWith('class ') &&\n\t\t\t\t!trimmedCode.startsWith('if ') &&\n\t\t\t\t!trimmedCode.startsWith('for ') &&\n\t\t\t\t!trimmedCode.startsWith('while ') &&\n\t\t\t\t!trimmedCode.startsWith('try ') &&\n\t\t\t\t!trimmedCode.startsWith('{') &&\n\t\t\t\t!trimmedCode.includes('\\n')\n\n\t\t\t// 如果是表达式，自动添加 return\n\t\t\tconst codeToExecute = isExpression ? `return ${code}` : code\n\n\t\t\tconst wrappedCode = `\n\t\t\t\t\treturn (async function() {\n\t\t\t\t\t\t${codeToExecute}\n\t\t\t\t\t})();\n\t\t\t\t`\n\n\t\t\t// 执行代码\n\t\t\tconst func = new AsyncFunction('console', ...contextKeys, wrappedCode)\n\t\t\tconst result = await func(mockConsole, ...contextValues)\n\n\t\t\t// 添加 console.log 输出\n\t\t\tif (logs.length > 0) {\n\t\t\t\tconst logItem: OutputItem = {\n\t\t\t\t\ttype: 'log',\n\t\t\t\t\tcontent: logs.join('\\n'),\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t}\n\t\t\t\tsetOutputs((prev) => [...prev, logItem])\n\t\t\t}\n\n\t\t\t// 总是添加执行结果输出（包括 undefined）\n\t\t\tconst outputItem: OutputItem = {\n\t\t\t\ttype: 'output',\n\t\t\t\tcontent: formatResult(result),\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t}\n\t\t\tsetOutputs((prev) => [...prev, outputItem])\n\n\t\t\tonExecute?.(code, result)\n\t\t\treturn result\n\t\t} catch (error) {\n\t\t\tconst errorItem: OutputItem = {\n\t\t\t\ttype: 'error',\n\t\t\t\tcontent: error instanceof Error ? error.message : String(error),\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t}\n\t\t\tsetOutputs((prev) => [...prev, errorItem])\n\t\t\tthrow error\n\t\t} finally {\n\t\t\tsetIsExecuting(false)\n\t\t\t// 滚动到底部\n\t\t\tsetTimeout(() => {\n\t\t\t\tif (outputRef.current) {\n\t\t\t\t\toutputRef.current.scrollTop = outputRef.current.scrollHeight\n\t\t\t\t}\n\t\t\t}, 0)\n\t\t}\n\t}\n\n\t// 清空控制台\n\tconst clear = () => {\n\t\tsetOutputs([])\n\t\t// 同时清空执行上下文\n\t\texecutionContextRef.current = {}\n\t}\n\n\t// 添加输出\n\tconst appendOutput = (content: string) => {\n\t\tconst outputItem: OutputItem = {\n\t\t\ttype: 'output',\n\t\t\tcontent,\n\t\t\ttimestamp: Date.now(),\n\t\t}\n\t\tsetOutputs((prev) => [...prev, outputItem])\n\t}\n\n\t// 暴露方法给父组件\n\tuseImperativeHandle(ref, () => ({\n\t\texecuteCode,\n\t\tclear,\n\t\tappendOutput,\n\t}))\n\n\t// 处理键盘事件\n\tconst handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n\t\tif (e.key === 'Enter') {\n\t\t\tif (e.shiftKey) {\n\t\t\t\t// Shift+Enter 换行\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\t// Enter 执行\n\t\t\t\te.preventDefault()\n\t\t\t\tif (!isExecuting && input.trim()) {\n\t\t\t\t\texecuteCode(input)\n\t\t\t\t\tsetInput('')\n\t\t\t\t\tsetTimeout(() => inputRef.current?.focus(), 0)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction getPrompt(type: string) {\n\t\tlet prompt = ' '\n\t\tif (type === 'input') {\n\t\t\tprompt = '>'\n\t\t} else if (type === 'output') {\n\t\t\tprompt = '<'\n\t\t}\n\t\treturn prompt\n\t}\n\n\treturn (\n\t\t<div className={styles.console} style={{ height }}>\n\t\t\t{/* 历史记录和输入区域 */}\n\t\t\t<div className={styles.historyArea} ref={outputRef}>\n\t\t\t\t{outputs.map((item) => (\n\t\t\t\t\t<div key={item.timestamp} className={`${styles.historyItem} ${styles[item.type]}`}>\n\t\t\t\t\t\t<span className={styles.prompt}>{getPrompt(item.type)}</span>\n\t\t\t\t\t\t<pre className={styles.content}>\n\t\t\t\t\t\t\t<HighlightSyntax code={item.content} />\n\t\t\t\t\t\t</pre>\n\t\t\t\t\t</div>\n\t\t\t\t))}\n\t\t\t\t{isExecuting && (\n\t\t\t\t\t<div className={styles.historyItem}>\n\t\t\t\t\t\t<span className={styles.prompt}>{'> '}</span>\n\t\t\t\t\t\t<span className={styles.executing}>Executing...</span>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\n\t\t\t{/* 当前输入行 */}\n\t\t\t<div className={styles.inputArea}>\n\t\t\t\t<span className={styles.prompt}>{'> '}</span>\n\t\t\t\t<textarea\n\t\t\t\t\tref={inputRef}\n\t\t\t\t\tclassName={styles.input}\n\t\t\t\t\tvalue={input}\n\t\t\t\t\tonChange={(e) => setInput(e.target.value)}\n\t\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t\t\tplaceholder={placeholder}\n\t\t\t\t\tdisabled={isExecuting}\n\t\t\t\t\trows={1}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\theight: Math.min(Math.max(20, input.split('\\n').length * 20), 120),\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nexport default JSConsole\n"
  },
  {
    "path": "packages/website/src/components/LanguageSwitcher.tsx",
    "content": "import { useEffect, useRef, useState } from 'react'\n\nimport { useLanguage } from '@/i18n/context'\n\nexport default function LanguageSwitcher() {\n\tconst { language, isZh, setLanguage } = useLanguage()\n\tconst [isOpen, setIsOpen] = useState(false)\n\tconst dropdownRef = useRef<HTMLDivElement>(null)\n\n\tconst languages = [\n\t\t{ code: 'zh-CN' as const, label: '中文' },\n\t\t{ code: 'en-US' as const, label: 'English' },\n\t]\n\n\tconst currentLanguage = languages.find((lang) => lang.code === language) || languages[0]\n\n\tconst handleLanguageChange = (langCode: 'zh-CN' | 'en-US') => {\n\t\tsetLanguage(langCode)\n\t\tsetIsOpen(false)\n\t}\n\n\t// Close dropdown when clicking outside\n\tuseEffect(() => {\n\t\tconst handleClickOutside = (event: MouseEvent) => {\n\t\t\tif (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {\n\t\t\t\tsetIsOpen(false)\n\t\t\t}\n\t\t}\n\n\t\tif (isOpen) {\n\t\t\tdocument.addEventListener('mousedown', handleClickOutside)\n\t\t}\n\n\t\treturn () => {\n\t\t\tdocument.removeEventListener('mousedown', handleClickOutside)\n\t\t}\n\t}, [isOpen])\n\n\treturn (\n\t\t<div className=\"relative\" ref={dropdownRef}>\n\t\t\t<button\n\t\t\t\tonClick={() => setIsOpen(!isOpen)}\n\t\t\t\tclassName=\"flex cursor-pointer items-center gap-2 rounded-lg border border-gray-200 bg-gray-100 px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700\"\n\t\t\t\taria-label={isZh ? '切换语言' : 'Switch language'}\n\t\t\t\taria-expanded={isOpen}\n\t\t\t\taria-haspopup=\"true\"\n\t\t\t>\n\t\t\t\t<svg\n\t\t\t\t\tclassName=\"w-4 h-4\"\n\t\t\t\t\tfill=\"none\"\n\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t>\n\t\t\t\t\t<path\n\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\td=\"M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129\"\n\t\t\t\t\t/>\n\t\t\t\t</svg>\n\t\t\t\t<span>{currentLanguage.label}</span>\n\t\t\t\t<svg\n\t\t\t\t\tclassName={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}\n\t\t\t\t\tfill=\"none\"\n\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t>\n\t\t\t\t\t<path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 9l-7 7-7-7\" />\n\t\t\t\t</svg>\n\t\t\t</button>\n\n\t\t\t{isOpen && (\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"absolute right-0 mt-2 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50\"\n\t\t\t\t\trole=\"menu\"\n\t\t\t\t\taria-orientation=\"vertical\"\n\t\t\t\t>\n\t\t\t\t\t{languages.map((lang) => (\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tkey={lang.code}\n\t\t\t\t\t\t\tonClick={() => handleLanguageChange(lang.code)}\n\t\t\t\t\t\t\tclassName={`flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-left text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 ${\n\t\t\t\t\t\t\t\tlanguage === lang.code\n\t\t\t\t\t\t\t\t\t? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'\n\t\t\t\t\t\t\t\t\t: 'text-gray-700 dark:text-gray-300'\n\t\t\t\t\t\t\t}`}\n\t\t\t\t\t\t\trole=\"menuitem\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span>{lang.label}</span>\n\t\t\t\t\t\t\t{language === lang.code && (\n\t\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\t\tclassName=\"w-4 h-4 ml-auto\"\n\t\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\t\t\t\tstrokeWidth={2}\n\t\t\t\t\t\t\t\t\t\td=\"M5 13l4 4L19 7\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/ThemeSwitcher.tsx",
    "content": "import { useEffect, useState } from 'react'\n\ntype Theme = 'light' | 'dark'\n\nexport default function ThemeSwitcher() {\n\tconst [theme, setTheme] = useState<Theme>(() => {\n\t\t// 初始化时读取保存的主题\n\t\tif (typeof window !== 'undefined') {\n\t\t\tconst savedTheme = localStorage.getItem('theme') as Theme | null\n\t\t\tif (savedTheme === 'light' || savedTheme === 'dark') {\n\t\t\t\treturn savedTheme\n\t\t\t}\n\t\t\t// 默认跟随系统\n\t\t\treturn window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n\t\t}\n\t\treturn 'light'\n\t})\n\n\tuseEffect(() => {\n\t\t// 应用主题\n\t\tif (theme === 'dark') {\n\t\t\tdocument.documentElement.classList.add('dark')\n\t\t\tdocument.documentElement.style.colorScheme = 'dark'\n\t\t} else {\n\t\t\tdocument.documentElement.classList.remove('dark')\n\t\t\tdocument.documentElement.style.colorScheme = 'light'\n\t\t}\n\t\t// 保存到 localStorage\n\t\tlocalStorage.setItem('theme', theme)\n\t}, [theme])\n\n\t// 监听系统主题变化\n\tuseEffect(() => {\n\t\tconst mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')\n\n\t\tconst handleSystemThemeChange = (e: MediaQueryListEvent) => {\n\t\t\t// 只有在用户未手动设置时才自动跟随系统\n\t\t\tconst savedTheme = localStorage.getItem('theme')\n\t\t\tif (!savedTheme) {\n\t\t\t\tsetTheme(e.matches ? 'dark' : 'light')\n\t\t\t}\n\t\t}\n\n\t\tmediaQuery.addEventListener('change', handleSystemThemeChange)\n\t\treturn () => mediaQuery.removeEventListener('change', handleSystemThemeChange)\n\t}, [])\n\n\tconst toggleTheme = () => {\n\t\tsetTheme((prev) => (prev === 'light' ? 'dark' : 'light'))\n\t}\n\n\treturn (\n\t\t<button\n\t\t\tonClick={toggleTheme}\n\t\t\tclassName=\"relative inline-flex h-8 w-16 cursor-pointer items-center rounded-full transition-colors duration-300 ease-in-out focus:outline-none\"\n\t\t\tstyle={{\n\t\t\t\tbackgroundColor: theme === 'dark' ? '#1e293b' : '#e0f2fe',\n\t\t\t}}\n\t\t\taria-label={theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}\n\t\t\trole=\"switch\"\n\t\t\taria-checked={theme === 'dark'}\n\t\t>\n\t\t\t{/* 滑块 */}\n\t\t\t<span\n\t\t\t\tclassName={`inline-block h-6 w-6 rounded-full transition-all duration-300 ease-in-out shadow-md ${\n\t\t\t\t\ttheme === 'dark' ? 'translate-x-9' : 'translate-x-1'\n\t\t\t\t}`}\n\t\t\t\tstyle={{\n\t\t\t\t\tbackgroundColor: theme === 'dark' ? '#475569' : '#fbbf24',\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{/* 图标 */}\n\t\t\t\t<span className=\"flex items-center justify-center h-full w-full\">\n\t\t\t\t\t{theme === 'light' ? (\n\t\t\t\t\t\t// 太阳图标\n\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\tclassName=\"w-4 h-4 text-white\"\n\t\t\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\t\t\tviewBox=\"0 0 20 20\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<path\n\t\t\t\t\t\t\t\tfillRule=\"evenodd\"\n\t\t\t\t\t\t\t\td=\"M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z\"\n\t\t\t\t\t\t\t\tclipRule=\"evenodd\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t// 月亮图标\n\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\tclassName=\"w-4 h-4 text-slate-200\"\n\t\t\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\t\t\tviewBox=\"0 0 20 20\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<path d=\"M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z\" />\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t)}\n\t\t\t\t</span>\n\t\t\t</span>\n\n\t\t\t{/* 背景装饰 */}\n\t\t\t<span\n\t\t\t\tclassName=\"absolute inset-0 flex items-center justify-between px-2 pointer-events-none\"\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\t{/* 左侧太阳（浅色模式时显示） */}\n\t\t\t\t<span\n\t\t\t\t\tclassName={`transition-opacity duration-300 ${\n\t\t\t\t\t\ttheme === 'light' ? 'opacity-0' : 'opacity-40'\n\t\t\t\t\t}`}\n\t\t\t\t>\n\t\t\t\t\t<svg className=\"w-4 h-4 text-sky-400\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n\t\t\t\t\t\t<path\n\t\t\t\t\t\t\tfillRule=\"evenodd\"\n\t\t\t\t\t\t\td=\"M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z\"\n\t\t\t\t\t\t\tclipRule=\"evenodd\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</svg>\n\t\t\t\t</span>\n\t\t\t\t{/* 右侧月亮（深色模式时显示） */}\n\t\t\t\t<span\n\t\t\t\t\tclassName={`transition-opacity duration-300 ${\n\t\t\t\t\t\ttheme === 'dark' ? 'opacity-0' : 'opacity-40'\n\t\t\t\t\t}`}\n\t\t\t\t>\n\t\t\t\t\t<svg className=\"w-4 h-4 text-slate-400\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n\t\t\t\t\t\t<path d=\"M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z\" />\n\t\t\t\t\t</svg>\n\t\t\t\t</span>\n\t\t\t</span>\n\t\t</button>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/ui/alert.tsx",
    "content": "import { type VariantProps, cva } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nconst alertVariants = cva(\n\t'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',\n\t{\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: 'bg-card text-card-foreground',\n\t\t\t\tdestructive:\n\t\t\t\t\t'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: 'default',\n\t\t},\n\t}\n)\n\nfunction Alert({\n\tclassName,\n\tvariant,\n\t...props\n}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"alert\"\n\t\t\trole=\"alert\"\n\t\t\tclassName={cn(alertVariants({ variant }), className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"alert-title\"\n\t\t\tclassName={cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"alert-description\"\n\t\t\tclassName={cn(\n\t\t\t\t'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Alert, AlertTitle, AlertDescription }\n"
  },
  {
    "path": "packages/website/src/components/ui/animated-gradient-text.tsx",
    "content": "import { ComponentPropsWithoutRef } from 'react'\n\nimport { cn } from '@/lib/utils'\n\nexport interface AnimatedGradientTextProps extends ComponentPropsWithoutRef<'div'> {\n\tspeed?: number\n\tcolorFrom?: string\n\tcolorTo?: string\n}\n\nexport function AnimatedGradientText({\n\tchildren,\n\tclassName,\n\tspeed = 1,\n\tcolorFrom = '#ffaa40',\n\tcolorTo = '#9c40ff',\n\t...props\n}: AnimatedGradientTextProps) {\n\treturn (\n\t\t<span\n\t\t\tstyle={\n\t\t\t\t{\n\t\t\t\t\t'--bg-size': `${speed * 300}%`,\n\t\t\t\t\t'--color-from': colorFrom,\n\t\t\t\t\t'--color-to': colorTo,\n\t\t\t\t} as React.CSSProperties\n\t\t\t}\n\t\t\tclassName={cn(\n\t\t\t\t`animate-gradient inline bg-gradient-to-r from-[var(--color-from)] via-[var(--color-to)] to-[var(--color-from)] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{children}\n\t\t</span>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/ui/animated-shiny-text.tsx",
    "content": "import { CSSProperties, ComponentPropsWithoutRef, FC } from 'react'\n\nimport { cn } from '@/lib/utils'\n\nexport interface AnimatedShinyTextProps extends ComponentPropsWithoutRef<'span'> {\n\tshimmerWidth?: number\n}\n\nexport const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({\n\tchildren,\n\tclassName,\n\tshimmerWidth = 100,\n\t...props\n}) => {\n\treturn (\n\t\t<span\n\t\t\tstyle={\n\t\t\t\t{\n\t\t\t\t\t'--shiny-width': `${shimmerWidth}px`,\n\t\t\t\t} as CSSProperties\n\t\t\t}\n\t\t\tclassName={cn(\n\t\t\t\t'mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70',\n\n\t\t\t\t// Shine effect\n\t\t\t\t'animate-shiny-text [background-size:var(--shiny-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',\n\n\t\t\t\t// Shine gradient\n\t\t\t\t'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80',\n\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{children}\n\t\t</span>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/ui/aurora-text.tsx",
    "content": "import React, { memo } from 'react'\n\ninterface AuroraTextProps {\n\tchildren: React.ReactNode\n\tclassName?: string\n\tcolors?: string[]\n\tspeed?: number\n}\n\nexport const AuroraText = memo(\n\t({\n\t\tchildren,\n\t\tclassName = '',\n\t\tcolors = ['#FF0080', '#7928CA', '#0070F3', '#38bdf8'],\n\t\tspeed = 1,\n\t}: AuroraTextProps) => {\n\t\tconst gradientStyle = {\n\t\t\tbackgroundImage: `linear-gradient(135deg, ${colors.join(', ')}, ${colors[0]})`,\n\t\t\tWebkitBackgroundClip: 'text',\n\t\t\tWebkitTextFillColor: 'transparent',\n\t\t\tanimationDuration: `${10 / speed}s`,\n\t\t}\n\n\t\treturn (\n\t\t\t<span className={`relative inline-block ${className}`}>\n\t\t\t\t<span className=\"sr-only\">{children}</span>\n\t\t\t\t<span\n\t\t\t\t\tclassName=\"animate-aurora relative bg-[length:200%_auto] bg-clip-text text-transparent\"\n\t\t\t\t\tstyle={gradientStyle}\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t>\n\t\t\t\t\t{children}\n\t\t\t\t</span>\n\t\t\t</span>\n\t\t)\n\t}\n)\n\nAuroraText.displayName = 'AuroraText'\n"
  },
  {
    "path": "packages/website/src/components/ui/badge.tsx",
    "content": "import { Slot } from '@radix-ui/react-slot'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nconst badgeVariants = cva(\n\t'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',\n\t{\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',\n\t\t\t\tsecondary:\n\t\t\t\t\t'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',\n\t\t\t\tdestructive:\n\t\t\t\t\t'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',\n\t\t\t\toutline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: 'default',\n\t\t},\n\t}\n)\n\nfunction Badge({\n\tclassName,\n\tvariant,\n\tasChild = false,\n\t...props\n}: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n\tconst Comp = asChild ? Slot : 'span'\n\n\treturn <Comp data-slot=\"badge\" className={cn(badgeVariants({ variant }), className)} {...props} />\n}\n\nexport { Badge, badgeVariants }\n"
  },
  {
    "path": "packages/website/src/components/ui/bento-grid.tsx",
    "content": "import { ArrowRightIcon } from '@radix-ui/react-icons'\nimport { ComponentPropsWithoutRef, ReactNode } from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { cn } from '@/lib/utils'\n\ninterface BentoGridProps extends ComponentPropsWithoutRef<'div'> {\n\tchildren: ReactNode\n\tclassName?: string\n}\n\ninterface BentoCardProps extends ComponentPropsWithoutRef<'div'> {\n\tname: string\n\tclassName: string\n\tbackground: ReactNode\n\tIcon: React.ElementType\n\tdescription: string\n\thref: string\n\tcta: string\n}\n\nconst BentoGrid = ({ children, className, ...props }: BentoGridProps) => {\n\treturn (\n\t\t<div className={cn('grid w-full auto-rows-[22rem] grid-cols-3 gap-4', className)} {...props}>\n\t\t\t{children}\n\t\t</div>\n\t)\n}\n\nconst BentoCard = ({\n\tname,\n\tclassName,\n\tbackground,\n\tIcon,\n\tdescription,\n\thref,\n\tcta,\n\t...props\n}: BentoCardProps) => (\n\t<div\n\t\tkey={name}\n\t\tclassName={cn(\n\t\t\t'group relative col-span-3 flex flex-col justify-between overflow-hidden rounded-xl',\n\t\t\t// light styles\n\t\t\t'bg-background [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)]',\n\t\t\t// dark styles\n\t\t\t'dark:bg-background transform-gpu dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset] dark:[border:1px_solid_rgba(255,255,255,.1)]',\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t>\n\t\t<div>{background}</div>\n\t\t<div className=\"p-4\">\n\t\t\t<div className=\"pointer-events-none z-10 flex transform-gpu flex-col gap-1 transition-all duration-300 lg:group-hover:-translate-y-10\">\n\t\t\t\t<Icon className=\"h-12 w-12 origin-left transform-gpu text-neutral-700 transition-all duration-300 ease-in-out group-hover:scale-75\" />\n\t\t\t\t<h3 className=\"text-xl font-semibold text-neutral-700 dark:text-neutral-300\">{name}</h3>\n\t\t\t\t<p className=\"max-w-lg text-neutral-400\">{description}</p>\n\t\t\t</div>\n\n\t\t\t<div\n\t\t\t\tclassName={cn(\n\t\t\t\t\t'pointer-events-none flex w-full translate-y-0 transform-gpu flex-row items-center transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100 lg:hidden'\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t<Button variant=\"link\" asChild size=\"sm\" className=\"pointer-events-auto p-0\">\n\t\t\t\t\t<a href={href}>\n\t\t\t\t\t\t{cta}\n\t\t\t\t\t\t<ArrowRightIcon className=\"ms-2 h-4 w-4 rtl:rotate-180\" />\n\t\t\t\t\t</a>\n\t\t\t\t</Button>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t'pointer-events-none absolute bottom-0 hidden w-full translate-y-10 transform-gpu flex-row items-center p-4 opacity-0 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100 lg:flex'\n\t\t\t)}\n\t\t>\n\t\t\t<Button variant=\"link\" asChild size=\"sm\" className=\"pointer-events-auto p-0\">\n\t\t\t\t<a href={href}>\n\t\t\t\t\t{cta}\n\t\t\t\t\t<ArrowRightIcon className=\"ms-2 h-4 w-4 rtl:rotate-180\" />\n\t\t\t\t</a>\n\t\t\t</Button>\n\t\t</div>\n\n\t\t<div className=\"pointer-events-none absolute inset-0 transform-gpu transition-all duration-300 group-hover:bg-black/[.03] group-hover:dark:bg-neutral-800/10\" />\n\t</div>\n)\n\nexport { BentoCard, BentoGrid }\n"
  },
  {
    "path": "packages/website/src/components/ui/blur-fade.tsx",
    "content": "import {\n\tAnimatePresence,\n\tMotionProps,\n\tUseInViewOptions,\n\tVariants,\n\tmotion,\n\tuseInView,\n} from 'motion/react'\nimport { useRef } from 'react'\n\ntype MarginType = UseInViewOptions['margin']\n\ninterface BlurFadeProps extends MotionProps {\n\tchildren: React.ReactNode\n\tclassName?: string\n\tvariant?: {\n\t\thidden: { y: number }\n\t\tvisible: { y: number }\n\t}\n\tduration?: number\n\tdelay?: number\n\toffset?: number\n\tdirection?: 'up' | 'down' | 'left' | 'right'\n\tinView?: boolean\n\tinViewMargin?: MarginType\n\tblur?: string\n}\n\nexport function BlurFade({\n\tchildren,\n\tclassName,\n\tvariant,\n\tduration = 0.4,\n\tdelay = 0,\n\toffset = 6,\n\tdirection = 'down',\n\tinView = false,\n\tinViewMargin = '-50px',\n\tblur = '6px',\n\t...props\n}: BlurFadeProps) {\n\tconst ref = useRef(null)\n\tconst inViewResult = useInView(ref, { once: true, margin: inViewMargin })\n\tconst isInView = !inView || inViewResult\n\tconst defaultVariants: Variants = {\n\t\thidden: {\n\t\t\t[direction === 'left' || direction === 'right' ? 'x' : 'y']:\n\t\t\t\tdirection === 'right' || direction === 'down' ? -offset : offset,\n\t\t\topacity: 0,\n\t\t\tfilter: `blur(${blur})`,\n\t\t},\n\t\tvisible: {\n\t\t\t[direction === 'left' || direction === 'right' ? 'x' : 'y']: 0,\n\t\t\topacity: 1,\n\t\t\tfilter: `blur(0px)`,\n\t\t},\n\t}\n\tconst combinedVariants = variant || defaultVariants\n\treturn (\n\t\t<AnimatePresence>\n\t\t\t<motion.div\n\t\t\t\tref={ref}\n\t\t\t\tinitial=\"hidden\"\n\t\t\t\tanimate={isInView ? 'visible' : 'hidden'}\n\t\t\t\texit=\"hidden\"\n\t\t\t\tvariants={combinedVariants}\n\t\t\t\ttransition={{\n\t\t\t\t\tdelay: 0.04 + delay,\n\t\t\t\t\tduration,\n\t\t\t\t\tease: 'easeOut',\n\t\t\t\t}}\n\t\t\t\tclassName={className}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</motion.div>\n\t\t</AnimatePresence>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/ui/button.tsx",
    "content": "import { Slot } from '@radix-ui/react-slot'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nconst buttonVariants = cva(\n\t\"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n\t{\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: 'bg-primary text-primary-foreground hover:bg-primary/90',\n\t\t\t\tdestructive:\n\t\t\t\t\t'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',\n\t\t\t\toutline:\n\t\t\t\t\t'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',\n\t\t\t\tsecondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',\n\t\t\t\tghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',\n\t\t\t\tlink: 'text-primary underline-offset-4 hover:underline',\n\t\t\t},\n\t\t\tsize: {\n\t\t\t\tdefault: 'h-9 px-4 py-2 has-[>svg]:px-3',\n\t\t\t\tsm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',\n\t\t\t\tlg: 'h-10 rounded-md px-6 has-[>svg]:px-4',\n\t\t\t\ticon: 'size-9',\n\t\t\t\t'icon-sm': 'size-8',\n\t\t\t\t'icon-lg': 'size-10',\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: 'default',\n\t\t\tsize: 'default',\n\t\t},\n\t}\n)\n\nfunction Button({\n\tclassName,\n\tvariant = 'default',\n\tsize = 'default',\n\tasChild = false,\n\t...props\n}: React.ComponentProps<'button'> &\n\tVariantProps<typeof buttonVariants> & {\n\t\tasChild?: boolean\n\t}) {\n\tconst Comp = asChild ? Slot : 'button'\n\n\treturn (\n\t\t<Comp\n\t\t\tdata-slot=\"button\"\n\t\t\tdata-variant={variant}\n\t\t\tdata-size={size}\n\t\t\tclassName={cn(buttonVariants({ variant, size, className }))}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "packages/website/src/components/ui/highlighter.tsx",
    "content": "import { useInView } from 'motion/react'\nimport { useEffect, useRef } from 'react'\nimport type React from 'react'\nimport { annotate } from 'rough-notation'\nimport { type RoughAnnotation } from 'rough-notation/lib/model'\n\ntype AnnotationAction =\n\t| 'highlight'\n\t| 'underline'\n\t| 'box'\n\t| 'circle'\n\t| 'strike-through'\n\t| 'crossed-off'\n\t| 'bracket'\n\ninterface HighlighterProps {\n\tchildren: React.ReactNode\n\taction?: AnnotationAction\n\tcolor?: string\n\tstrokeWidth?: number\n\tanimationDuration?: number\n\titerations?: number\n\tpadding?: number\n\tmultiline?: boolean\n\tisView?: boolean\n}\n\nexport function Highlighter({\n\tchildren,\n\taction = 'highlight',\n\tcolor = '#ffd1dc',\n\tstrokeWidth = 1.5,\n\tanimationDuration = 600,\n\titerations = 2,\n\tpadding = 2,\n\tmultiline = true,\n\tisView = false,\n}: HighlighterProps) {\n\tconst elementRef = useRef<HTMLSpanElement>(null)\n\tconst annotationRef = useRef<RoughAnnotation | null>(null)\n\n\tconst isInView = useInView(elementRef, {\n\t\tonce: true,\n\t\tmargin: '-10%',\n\t})\n\n\t// If isView is false, always show. If isView is true, wait for inView\n\tconst shouldShow = !isView || isInView\n\n\tuseEffect(() => {\n\t\tif (!shouldShow) return\n\n\t\tconst element = elementRef.current\n\t\tif (!element) return\n\n\t\tconst annotationConfig = {\n\t\t\ttype: action,\n\t\t\tcolor,\n\t\t\tstrokeWidth,\n\t\t\tanimationDuration,\n\t\t\titerations,\n\t\t\tpadding,\n\t\t\tmultiline,\n\t\t}\n\n\t\tconst annotation = annotate(element, annotationConfig)\n\n\t\tannotationRef.current = annotation\n\t\tannotationRef.current.show()\n\n\t\tconst resizeObserver = new ResizeObserver(() => {\n\t\t\tannotation.hide()\n\t\t\tannotation.show()\n\t\t})\n\n\t\tresizeObserver.observe(element)\n\t\tresizeObserver.observe(document.body)\n\n\t\treturn () => {\n\t\t\tif (element) {\n\t\t\t\tannotate(element, { type: action }).remove()\n\t\t\t\tresizeObserver.disconnect()\n\t\t\t}\n\t\t}\n\t}, [shouldShow, action, color, strokeWidth, animationDuration, iterations, padding, multiline])\n\n\treturn (\n\t\t<span ref={elementRef} className=\"relative inline-block bg-transparent\">\n\t\t\t{children}\n\t\t</span>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/ui/hyper-text.tsx",
    "content": "import { AnimatePresence, MotionProps, motion } from 'motion/react'\nimport { useEffect, useRef, useState } from 'react'\n\nimport { cn } from '@/lib/utils'\n\ntype CharacterSet = string[] | readonly string[]\n\ninterface HyperTextProps extends MotionProps {\n\t/** The text content to be animated */\n\tchildren: string\n\t/** Optional className for styling */\n\tclassName?: string\n\t/** Duration of the animation in milliseconds */\n\tduration?: number\n\t/** Delay before animation starts in milliseconds */\n\tdelay?: number\n\t/** Component to render as - defaults to div */\n\tas?: React.ElementType\n\t/** Whether to start animation when element comes into view */\n\tstartOnView?: boolean\n\t/** Whether to trigger animation on hover */\n\tanimateOnHover?: boolean\n\t/** Custom character set for scramble effect. Defaults to uppercase alphabet */\n\tcharacterSet?: CharacterSet\n}\n\nconst DEFAULT_CHARACTER_SET = Object.freeze(\n\t'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')\n) as readonly string[]\n\nconst getRandomInt = (max: number): number => Math.floor(Math.random() * max)\n\nexport function HyperText({\n\tchildren,\n\tclassName,\n\tduration = 800,\n\tdelay = 0,\n\tas: Component = 'div',\n\tstartOnView = false,\n\tanimateOnHover = true,\n\tcharacterSet = DEFAULT_CHARACTER_SET,\n\t...props\n}: HyperTextProps) {\n\tconst MotionComponent = motion.create(Component, {\n\t\tforwardMotionProps: true,\n\t})\n\n\tconst [displayText, setDisplayText] = useState<string[]>(() => children.split(''))\n\tconst [isAnimating, setIsAnimating] = useState(false)\n\tconst iterationCount = useRef(0)\n\tconst elementRef = useRef<HTMLElement>(null)\n\n\tconst handleAnimationTrigger = () => {\n\t\tif (animateOnHover && !isAnimating) {\n\t\t\titerationCount.current = 0\n\t\t\tsetIsAnimating(true)\n\t\t}\n\t}\n\n\t// Handle animation start based on view or delay\n\tuseEffect(() => {\n\t\tif (!startOnView) {\n\t\t\tconst startTimeout = setTimeout(() => {\n\t\t\t\tsetIsAnimating(true)\n\t\t\t}, delay)\n\t\t\treturn () => clearTimeout(startTimeout)\n\t\t}\n\n\t\tconst observer = new IntersectionObserver(\n\t\t\t([entry]) => {\n\t\t\t\tif (entry.isIntersecting) {\n\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\tsetIsAnimating(true)\n\t\t\t\t\t}, delay)\n\t\t\t\t\tobserver.disconnect()\n\t\t\t\t}\n\t\t\t},\n\t\t\t{ threshold: 0.1, rootMargin: '-30% 0px -30% 0px' }\n\t\t)\n\n\t\tif (elementRef.current) {\n\t\t\tobserver.observe(elementRef.current)\n\t\t}\n\n\t\treturn () => observer.disconnect()\n\t}, [delay, startOnView])\n\n\t// Handle scramble animation\n\tuseEffect(() => {\n\t\tif (!isAnimating) return\n\n\t\tconst maxIterations = children.length\n\t\tconst startTime = performance.now()\n\t\tlet animationFrameId: number\n\n\t\tconst animate = (currentTime: number) => {\n\t\t\tconst elapsed = currentTime - startTime\n\t\t\tconst progress = Math.min(elapsed / duration, 1)\n\n\t\t\titerationCount.current = progress * maxIterations\n\n\t\t\tsetDisplayText((currentText) =>\n\t\t\t\tcurrentText.map((letter, index) =>\n\t\t\t\t\tletter === ' '\n\t\t\t\t\t\t? letter\n\t\t\t\t\t\t: index <= iterationCount.current\n\t\t\t\t\t\t\t? children[index]\n\t\t\t\t\t\t\t: characterSet[getRandomInt(characterSet.length)]\n\t\t\t\t)\n\t\t\t)\n\n\t\t\tif (progress < 1) {\n\t\t\t\tanimationFrameId = requestAnimationFrame(animate)\n\t\t\t} else {\n\t\t\t\tsetIsAnimating(false)\n\t\t\t}\n\t\t}\n\n\t\tanimationFrameId = requestAnimationFrame(animate)\n\n\t\treturn () => cancelAnimationFrame(animationFrameId)\n\t}, [children, duration, isAnimating, characterSet])\n\n\treturn (\n\t\t<MotionComponent\n\t\t\tref={elementRef}\n\t\t\tclassName={cn('overflow-hidden py-2 text-4xl font-bold', className)}\n\t\t\tonMouseEnter={handleAnimationTrigger}\n\t\t\t{...props}\n\t\t>\n\t\t\t<AnimatePresence>\n\t\t\t\t{displayText.map((letter, index) => (\n\t\t\t\t\t<motion.span key={index} className={cn('font-mono', letter === ' ' ? 'w-3' : '')}>\n\t\t\t\t\t\t{letter.toUpperCase()}\n\t\t\t\t\t</motion.span>\n\t\t\t\t))}\n\t\t\t</AnimatePresence>\n\t\t</MotionComponent>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/ui/kbd.tsx",
    "content": "import { cn } from '@/lib/utils'\n\nfunction Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {\n\treturn (\n\t\t<kbd\n\t\t\tdata-slot=\"kbd\"\n\t\t\tclassName={cn(\n\t\t\t\t'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none',\n\t\t\t\t\"[&_svg:not([class*='size-'])]:size-3\",\n\t\t\t\t'[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<kbd\n\t\t\tdata-slot=\"kbd-group\"\n\t\t\tclassName={cn('inline-flex items-center gap-1', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Kbd, KbdGroup }\n"
  },
  {
    "path": "packages/website/src/components/ui/magic-card.tsx",
    "content": "import { motion, useMotionTemplate, useMotionValue } from 'motion/react'\nimport React, { useCallback, useEffect } from 'react'\n\nimport { cn } from '@/lib/utils'\n\ninterface MagicCardProps {\n\tchildren?: React.ReactNode\n\tclassName?: string\n\tgradientSize?: number\n\tgradientColor?: string\n\tgradientOpacity?: number\n\tgradientFrom?: string\n\tgradientTo?: string\n}\n\nexport function MagicCard({\n\tchildren,\n\tclassName,\n\tgradientSize = 200,\n\tgradientColor = '#262626',\n\tgradientOpacity = 0.8,\n\tgradientFrom = '#9E7AFF',\n\tgradientTo = '#FE8BBB',\n}: MagicCardProps) {\n\tconst mouseX = useMotionValue(-gradientSize)\n\tconst mouseY = useMotionValue(-gradientSize)\n\tconst reset = useCallback(() => {\n\t\tmouseX.set(-gradientSize)\n\t\tmouseY.set(-gradientSize)\n\t}, [gradientSize, mouseX, mouseY])\n\n\tconst handlePointerMove = useCallback(\n\t\t(e: React.PointerEvent<HTMLDivElement>) => {\n\t\t\tconst rect = e.currentTarget.getBoundingClientRect()\n\t\t\tmouseX.set(e.clientX - rect.left)\n\t\t\tmouseY.set(e.clientY - rect.top)\n\t\t},\n\t\t[mouseX, mouseY]\n\t)\n\n\tuseEffect(() => {\n\t\treset()\n\t}, [reset])\n\n\tuseEffect(() => {\n\t\tconst handleGlobalPointerOut = (e: PointerEvent) => {\n\t\t\tif (!e.relatedTarget) {\n\t\t\t\treset()\n\t\t\t}\n\t\t}\n\n\t\tconst handleVisibility = () => {\n\t\t\tif (document.visibilityState !== 'visible') {\n\t\t\t\treset()\n\t\t\t}\n\t\t}\n\n\t\twindow.addEventListener('pointerout', handleGlobalPointerOut)\n\t\twindow.addEventListener('blur', reset)\n\t\tdocument.addEventListener('visibilitychange', handleVisibility)\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener('pointerout', handleGlobalPointerOut)\n\t\t\twindow.removeEventListener('blur', reset)\n\t\t\tdocument.removeEventListener('visibilitychange', handleVisibility)\n\t\t}\n\t}, [reset])\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn('group relative rounded-[inherit]', className)}\n\t\t\tonPointerMove={handlePointerMove}\n\t\t\tonPointerLeave={reset}\n\t\t\tonPointerEnter={reset}\n\t\t>\n\t\t\t<motion.div\n\t\t\t\tclassName=\"bg-border pointer-events-none absolute inset-0 rounded-[inherit] duration-300 group-hover:opacity-100\"\n\t\t\t\tstyle={{\n\t\t\t\t\tbackground: useMotionTemplate`\n          radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px,\n          ${gradientFrom}, \n          ${gradientTo}, \n          var(--border) 100%\n          )\n          `,\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<div className=\"absolute inset-px rounded-[inherit] bg-white dark:bg-neutral-900\" />\n\t\t\t<motion.div\n\t\t\t\tclassName=\"pointer-events-none absolute inset-px rounded-[inherit] opacity-0 transition-opacity duration-300 group-hover:opacity-100\"\n\t\t\t\tstyle={{\n\t\t\t\t\tbackground: useMotionTemplate`\n            radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px, ${gradientColor}, transparent 100%)\n          `,\n\t\t\t\t\topacity: gradientOpacity,\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<div className=\"relative\">{children}</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/ui/marquee.tsx",
    "content": "import { ComponentPropsWithoutRef } from 'react'\n\nimport { cn } from '@/lib/utils'\n\ninterface MarqueeProps extends ComponentPropsWithoutRef<'div'> {\n\t/**\n\t * Optional CSS class name to apply custom styles\n\t */\n\tclassName?: string\n\t/**\n\t * Whether to reverse the animation direction\n\t * @default false\n\t */\n\treverse?: boolean\n\t/**\n\t * Whether to pause the animation on hover\n\t * @default false\n\t */\n\tpauseOnHover?: boolean\n\t/**\n\t * Content to be displayed in the marquee\n\t */\n\tchildren: React.ReactNode\n\t/**\n\t * Whether to animate vertically instead of horizontally\n\t * @default false\n\t */\n\tvertical?: boolean\n\t/**\n\t * Number of times to repeat the content\n\t * @default 4\n\t */\n\trepeat?: number\n}\n\nexport function Marquee({\n\tclassName,\n\treverse = false,\n\tpauseOnHover = false,\n\tchildren,\n\tvertical = false,\n\trepeat = 4,\n\t...props\n}: MarqueeProps) {\n\treturn (\n\t\t<div\n\t\t\t{...props}\n\t\t\tclassName={cn(\n\t\t\t\t'group flex [gap:var(--gap)] overflow-hidden p-2 [--duration:40s] [--gap:1rem]',\n\t\t\t\t{\n\t\t\t\t\t'flex-row': !vertical,\n\t\t\t\t\t'flex-col': vertical,\n\t\t\t\t},\n\t\t\t\tclassName\n\t\t\t)}\n\t\t>\n\t\t\t{Array(repeat)\n\t\t\t\t.fill(0)\n\t\t\t\t.map((_, i) => (\n\t\t\t\t\t<div\n\t\t\t\t\t\tkey={i}\n\t\t\t\t\t\tclassName={cn('flex shrink-0 justify-around [gap:var(--gap)]', {\n\t\t\t\t\t\t\t'animate-marquee flex-row': !vertical,\n\t\t\t\t\t\t\t'animate-marquee-vertical flex-col': vertical,\n\t\t\t\t\t\t\t'group-hover:[animation-play-state:paused]': pauseOnHover,\n\t\t\t\t\t\t\t'[animation-direction:reverse]': reverse,\n\t\t\t\t\t\t})}\n\t\t\t\t\t>\n\t\t\t\t\t\t{children}\n\t\t\t\t\t</div>\n\t\t\t\t))}\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/ui/neon-gradient-card.tsx",
    "content": "import { CSSProperties, ReactElement, ReactNode, useEffect, useRef, useState } from 'react'\n\nimport { cn } from '@/lib/utils'\n\ninterface NeonColorsProps {\n\tfirstColor: string\n\tsecondColor: string\n}\n\ninterface NeonGradientCardProps extends React.HTMLAttributes<HTMLDivElement> {\n\t/**\n\t * @default <div />\n\t * @type ReactElement\n\t * @description\n\t * The component to be rendered as the card\n\t * */\n\tas?: ReactElement\n\t/**\n\t * @default \"\"\n\t * @type string\n\t * @description\n\t * The className of the card\n\t */\n\tclassName?: string\n\n\t/**\n\t * @default \"\"\n\t * @type ReactNode\n\t * @description\n\t * The children of the card\n\t * */\n\tchildren?: ReactNode\n\n\t/**\n\t * @default 5\n\t * @type number\n\t * @description\n\t * The size of the border in pixels\n\t * */\n\tborderSize?: number\n\n\t/**\n\t * @default 20\n\t * @type number\n\t * @description\n\t * The size of the radius in pixels\n\t * */\n\tborderRadius?: number\n\n\t/**\n\t * @default \"{ firstColor: '#ff00aa', secondColor: '#00FFF1' }\"\n\t * @type string\n\t * @description\n\t * The colors of the neon gradient\n\t * */\n\tneonColors?: NeonColorsProps\n}\n\nexport const NeonGradientCard: React.FC<NeonGradientCardProps> = ({\n\tclassName,\n\tchildren,\n\tborderSize = 2,\n\tborderRadius = 20,\n\tneonColors = {\n\t\tfirstColor: '#ff00aa',\n\t\tsecondColor: '#00FFF1',\n\t},\n\t...props\n}) => {\n\tconst containerRef = useRef<HTMLDivElement>(null)\n\tconst [dimensions, setDimensions] = useState({ width: 0, height: 0 })\n\n\tuseEffect(() => {\n\t\tconst updateDimensions = () => {\n\t\t\tif (containerRef.current) {\n\t\t\t\tconst { offsetWidth, offsetHeight } = containerRef.current\n\t\t\t\tsetDimensions({ width: offsetWidth, height: offsetHeight })\n\t\t\t}\n\t\t}\n\n\t\tupdateDimensions()\n\t\twindow.addEventListener('resize', updateDimensions)\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener('resize', updateDimensions)\n\t\t}\n\t}, [])\n\n\tuseEffect(() => {\n\t\tif (containerRef.current) {\n\t\t\tconst { offsetWidth, offsetHeight } = containerRef.current\n\t\t\tsetDimensions({ width: offsetWidth, height: offsetHeight })\n\t\t}\n\t}, [children])\n\n\treturn (\n\t\t<div\n\t\t\tref={containerRef}\n\t\t\tstyle={\n\t\t\t\t{\n\t\t\t\t\t'--border-size': `${borderSize}px`,\n\t\t\t\t\t'--border-radius': `${borderRadius}px`,\n\t\t\t\t\t'--neon-first-color': neonColors.firstColor,\n\t\t\t\t\t'--neon-second-color': neonColors.secondColor,\n\t\t\t\t\t'--card-width': `${dimensions.width}px`,\n\t\t\t\t\t'--card-height': `${dimensions.height}px`,\n\t\t\t\t\t'--card-content-radius': `${borderRadius - borderSize}px`,\n\t\t\t\t\t'--pseudo-element-background-image': `linear-gradient(0deg, ${neonColors.firstColor}, ${neonColors.secondColor})`,\n\t\t\t\t\t'--pseudo-element-width': `${dimensions.width + borderSize * 2}px`,\n\t\t\t\t\t'--pseudo-element-height': `${dimensions.height + borderSize * 2}px`,\n\t\t\t\t\t'--after-blur': `${dimensions.width / 6}px`,\n\t\t\t\t} as CSSProperties\n\t\t\t}\n\t\t\tclassName={cn('relative z-10 size-full rounded-[var(--border-radius)]', className)}\n\t\t\t{...props}\n\t\t>\n\t\t\t<div\n\t\t\t\tclassName={cn(\n\t\t\t\t\t'relative size-full min-h-[inherit] rounded-[var(--card-content-radius)] bg-gray-100',\n\t\t\t\t\t'before:absolute before:-top-[var(--border-size)] before:-left-[var(--border-size)] before:-z-10 before:block',\n\t\t\t\t\t\"before:h-[var(--pseudo-element-height)] before:w-[var(--pseudo-element-width)] before:rounded-[var(--border-radius)] before:content-['']\",\n\t\t\t\t\t'before:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] before:bg-[length:100%_200%]',\n\t\t\t\t\t'before:animate-background-position-spin',\n\t\t\t\t\t'after:absolute after:-top-[var(--border-size)] after:-left-[var(--border-size)] after:-z-10 after:block',\n\t\t\t\t\t\"after:h-[var(--pseudo-element-height)] after:w-[var(--pseudo-element-width)] after:rounded-[var(--border-radius)] after:blur-[var(--after-blur)] after:content-['']\",\n\t\t\t\t\t'after:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] after:bg-[length:100%_200%] after:opacity-80',\n\t\t\t\t\t'after:animate-background-position-spin',\n\t\t\t\t\t'dark:bg-neutral-900',\n\t\t\t\t\t'break-words'\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/ui/particles.tsx",
    "content": "import React, { ComponentPropsWithoutRef, useEffect, useRef, useState } from 'react'\n\nimport { cn } from '@/lib/utils'\n\ninterface MousePosition {\n\tx: number\n\ty: number\n}\n\nfunction MousePosition(): MousePosition {\n\tconst [mousePosition, setMousePosition] = useState<MousePosition>({\n\t\tx: 0,\n\t\ty: 0,\n\t})\n\n\tuseEffect(() => {\n\t\tconst handleMouseMove = (event: MouseEvent) => {\n\t\t\tsetMousePosition({ x: event.clientX, y: event.clientY })\n\t\t}\n\n\t\twindow.addEventListener('mousemove', handleMouseMove)\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener('mousemove', handleMouseMove)\n\t\t}\n\t}, [])\n\n\treturn mousePosition\n}\n\ninterface ParticlesProps extends ComponentPropsWithoutRef<'div'> {\n\tclassName?: string\n\tquantity?: number\n\tstaticity?: number\n\tease?: number\n\tsize?: number\n\trefresh?: boolean\n\tcolor?: string\n\tvx?: number\n\tvy?: number\n}\n\nfunction hexToRgb(hex: string): number[] {\n\thex = hex.replace('#', '')\n\n\tif (hex.length === 3) {\n\t\thex = hex\n\t\t\t.split('')\n\t\t\t.map((char) => char + char)\n\t\t\t.join('')\n\t}\n\n\tconst hexInt = parseInt(hex, 16)\n\tconst red = (hexInt >> 16) & 255\n\tconst green = (hexInt >> 8) & 255\n\tconst blue = hexInt & 255\n\treturn [red, green, blue]\n}\n\ntype Circle = {\n\tx: number\n\ty: number\n\ttranslateX: number\n\ttranslateY: number\n\tsize: number\n\talpha: number\n\ttargetAlpha: number\n\tdx: number\n\tdy: number\n\tmagnetism: number\n}\n\nexport const Particles: React.FC<ParticlesProps> = ({\n\tclassName = '',\n\tquantity = 100,\n\tstaticity = 50,\n\tease = 50,\n\tsize = 0.4,\n\trefresh = false,\n\tcolor = '#ffffff',\n\tvx = 0,\n\tvy = 0,\n\t...props\n}) => {\n\tconst canvasRef = useRef<HTMLCanvasElement>(null)\n\tconst canvasContainerRef = useRef<HTMLDivElement>(null)\n\tconst context = useRef<CanvasRenderingContext2D | null>(null)\n\tconst circles = useRef<Circle[]>([])\n\tconst mousePosition = MousePosition()\n\tconst mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 })\n\tconst canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 })\n\tconst dpr = typeof window !== 'undefined' ? window.devicePixelRatio : 1\n\tconst rafID = useRef<number | null>(null)\n\tconst resizeTimeout = useRef<NodeJS.Timeout | null>(null)\n\n\tuseEffect(() => {\n\t\tif (canvasRef.current) {\n\t\t\tcontext.current = canvasRef.current.getContext('2d')\n\t\t}\n\t\tinitCanvas()\n\t\tanimate()\n\n\t\tconst handleResize = () => {\n\t\t\tif (resizeTimeout.current) {\n\t\t\t\tclearTimeout(resizeTimeout.current)\n\t\t\t}\n\t\t\tresizeTimeout.current = setTimeout(() => {\n\t\t\t\tinitCanvas()\n\t\t\t}, 200)\n\t\t}\n\n\t\twindow.addEventListener('resize', handleResize)\n\n\t\treturn () => {\n\t\t\tif (rafID.current != null) {\n\t\t\t\twindow.cancelAnimationFrame(rafID.current)\n\t\t\t}\n\t\t\tif (resizeTimeout.current) {\n\t\t\t\tclearTimeout(resizeTimeout.current)\n\t\t\t}\n\t\t\twindow.removeEventListener('resize', handleResize)\n\t\t}\n\t}, [color])\n\n\tuseEffect(() => {\n\t\tonMouseMove()\n\t}, [mousePosition.x, mousePosition.y])\n\n\tuseEffect(() => {\n\t\tinitCanvas()\n\t}, [refresh])\n\n\tconst initCanvas = () => {\n\t\tresizeCanvas()\n\t\tdrawParticles()\n\t}\n\n\tconst onMouseMove = () => {\n\t\tif (canvasRef.current) {\n\t\t\tconst rect = canvasRef.current.getBoundingClientRect()\n\t\t\tconst { w, h } = canvasSize.current\n\t\t\tconst x = mousePosition.x - rect.left - w / 2\n\t\t\tconst y = mousePosition.y - rect.top - h / 2\n\t\t\tconst inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2\n\t\t\tif (inside) {\n\t\t\t\tmouse.current.x = x\n\t\t\t\tmouse.current.y = y\n\t\t\t}\n\t\t}\n\t}\n\n\tconst resizeCanvas = () => {\n\t\tif (canvasContainerRef.current && canvasRef.current && context.current) {\n\t\t\tcanvasSize.current.w = canvasContainerRef.current.offsetWidth\n\t\t\tcanvasSize.current.h = canvasContainerRef.current.offsetHeight\n\n\t\t\tcanvasRef.current.width = canvasSize.current.w * dpr\n\t\t\tcanvasRef.current.height = canvasSize.current.h * dpr\n\t\t\tcanvasRef.current.style.width = `${canvasSize.current.w}px`\n\t\t\tcanvasRef.current.style.height = `${canvasSize.current.h}px`\n\t\t\tcontext.current.scale(dpr, dpr)\n\n\t\t\t// Clear existing particles and create new ones with exact quantity\n\t\t\tcircles.current = []\n\t\t\tfor (let i = 0; i < quantity; i++) {\n\t\t\t\tconst circle = circleParams()\n\t\t\t\tdrawCircle(circle)\n\t\t\t}\n\t\t}\n\t}\n\n\tconst circleParams = (): Circle => {\n\t\tconst x = Math.floor(Math.random() * canvasSize.current.w)\n\t\tconst y = Math.floor(Math.random() * canvasSize.current.h)\n\t\tconst translateX = 0\n\t\tconst translateY = 0\n\t\tconst pSize = Math.floor(Math.random() * 2) + size\n\t\tconst alpha = 0\n\t\tconst targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))\n\t\tconst dx = (Math.random() - 0.5) * 0.1\n\t\tconst dy = (Math.random() - 0.5) * 0.1\n\t\tconst magnetism = 0.1 + Math.random() * 4\n\t\treturn {\n\t\t\tx,\n\t\t\ty,\n\t\t\ttranslateX,\n\t\t\ttranslateY,\n\t\t\tsize: pSize,\n\t\t\talpha,\n\t\t\ttargetAlpha,\n\t\t\tdx,\n\t\t\tdy,\n\t\t\tmagnetism,\n\t\t}\n\t}\n\n\tconst rgb = hexToRgb(color)\n\n\tconst drawCircle = (circle: Circle, update = false) => {\n\t\tif (context.current) {\n\t\t\tconst { x, y, translateX, translateY, size, alpha } = circle\n\t\t\tcontext.current.translate(translateX, translateY)\n\t\t\tcontext.current.beginPath()\n\t\t\tcontext.current.arc(x, y, size, 0, 2 * Math.PI)\n\t\t\tcontext.current.fillStyle = `rgba(${rgb.join(', ')}, ${alpha})`\n\t\t\tcontext.current.fill()\n\t\t\tcontext.current.setTransform(dpr, 0, 0, dpr, 0, 0)\n\n\t\t\tif (!update) {\n\t\t\t\tcircles.current.push(circle)\n\t\t\t}\n\t\t}\n\t}\n\n\tconst clearContext = () => {\n\t\tif (context.current) {\n\t\t\tcontext.current.clearRect(0, 0, canvasSize.current.w, canvasSize.current.h)\n\t\t}\n\t}\n\n\tconst drawParticles = () => {\n\t\tclearContext()\n\t\tconst particleCount = quantity\n\t\tfor (let i = 0; i < particleCount; i++) {\n\t\t\tconst circle = circleParams()\n\t\t\tdrawCircle(circle)\n\t\t}\n\t}\n\n\tconst remapValue = (\n\t\tvalue: number,\n\t\tstart1: number,\n\t\tend1: number,\n\t\tstart2: number,\n\t\tend2: number\n\t): number => {\n\t\tconst remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2\n\t\treturn remapped > 0 ? remapped : 0\n\t}\n\n\tconst animate = () => {\n\t\tclearContext()\n\t\tcircles.current.forEach((circle: Circle, i: number) => {\n\t\t\t// Handle the alpha value\n\t\t\tconst edge = [\n\t\t\t\tcircle.x + circle.translateX - circle.size, // distance from left edge\n\t\t\t\tcanvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge\n\t\t\t\tcircle.y + circle.translateY - circle.size, // distance from top edge\n\t\t\t\tcanvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge\n\t\t\t]\n\t\t\tconst closestEdge = edge.reduce((a, b) => Math.min(a, b))\n\t\t\tconst remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2))\n\t\t\tif (remapClosestEdge > 1) {\n\t\t\t\tcircle.alpha += 0.02\n\t\t\t\tif (circle.alpha > circle.targetAlpha) {\n\t\t\t\t\tcircle.alpha = circle.targetAlpha\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcircle.alpha = circle.targetAlpha * remapClosestEdge\n\t\t\t}\n\t\t\tcircle.x += circle.dx + vx\n\t\t\tcircle.y += circle.dy + vy\n\t\t\tcircle.translateX +=\n\t\t\t\t(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / ease\n\t\t\tcircle.translateY +=\n\t\t\t\t(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / ease\n\n\t\t\tdrawCircle(circle, true)\n\n\t\t\t// circle gets out of the canvas\n\t\t\tif (\n\t\t\t\tcircle.x < -circle.size ||\n\t\t\t\tcircle.x > canvasSize.current.w + circle.size ||\n\t\t\t\tcircle.y < -circle.size ||\n\t\t\t\tcircle.y > canvasSize.current.h + circle.size\n\t\t\t) {\n\t\t\t\t// remove the circle from the array\n\t\t\t\tcircles.current.splice(i, 1)\n\t\t\t\t// create a new circle\n\t\t\t\tconst newCircle = circleParams()\n\t\t\t\tdrawCircle(newCircle)\n\t\t\t}\n\t\t})\n\t\trafID.current = window.requestAnimationFrame(animate)\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn('pointer-events-none', className)}\n\t\t\tref={canvasContainerRef}\n\t\t\taria-hidden=\"true\"\n\t\t\t{...props}\n\t\t>\n\t\t\t<canvas ref={canvasRef} className=\"size-full\" />\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/ui/separator.tsx",
    "content": "import * as SeparatorPrimitive from '@radix-ui/react-separator'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Separator({\n\tclassName,\n\torientation = 'horizontal',\n\tdecorative = true,\n\t...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n\treturn (\n\t\t<SeparatorPrimitive.Root\n\t\t\tdata-slot=\"separator\"\n\t\t\tdecorative={decorative}\n\t\t\torientation={orientation}\n\t\t\tclassName={cn(\n\t\t\t\t'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Separator }\n"
  },
  {
    "path": "packages/website/src/components/ui/sonner.tsx",
    "content": "import {\n\tCircleCheckIcon,\n\tInfoIcon,\n\tLoader2Icon,\n\tOctagonXIcon,\n\tTriangleAlertIcon,\n} from 'lucide-react'\nimport { useTheme } from 'next-themes'\nimport { Toaster as Sonner, type ToasterProps } from 'sonner'\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n\tconst { theme = 'system' } = useTheme()\n\n\treturn (\n\t\t<Sonner\n\t\t\ttheme={theme as ToasterProps['theme']}\n\t\t\tclassName=\"toaster group\"\n\t\t\ticons={{\n\t\t\t\tsuccess: <CircleCheckIcon className=\"size-4\" />,\n\t\t\t\tinfo: <InfoIcon className=\"size-4\" />,\n\t\t\t\twarning: <TriangleAlertIcon className=\"size-4\" />,\n\t\t\t\terror: <OctagonXIcon className=\"size-4\" />,\n\t\t\t\tloading: <Loader2Icon className=\"size-4 animate-spin\" />,\n\t\t\t}}\n\t\t\tstyle={\n\t\t\t\t{\n\t\t\t\t\t'--normal-bg': 'var(--popover)',\n\t\t\t\t\t'--normal-text': 'var(--popover-foreground)',\n\t\t\t\t\t'--normal-border': 'var(--border)',\n\t\t\t\t\t'--border-radius': 'var(--radius)',\n\t\t\t\t} as React.CSSProperties\n\t\t\t}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Toaster }\n"
  },
  {
    "path": "packages/website/src/components/ui/sparkles-text.tsx",
    "content": "import { motion } from 'motion/react'\nimport { CSSProperties, ReactElement, useEffect, useState } from 'react'\n\nimport { cn } from '@/lib/utils'\n\ninterface Sparkle {\n\tid: string\n\tx: string\n\ty: string\n\tcolor: string\n\tdelay: number\n\tscale: number\n\tlifespan: number\n}\n\nconst Sparkle: React.FC<Sparkle> = ({ id, x, y, color, delay, scale }) => {\n\treturn (\n\t\t<motion.svg\n\t\t\tkey={id}\n\t\t\tclassName=\"pointer-events-none absolute z-20\"\n\t\t\tinitial={{ opacity: 0, left: x, top: y }}\n\t\t\tanimate={{\n\t\t\t\topacity: [0, 1, 0],\n\t\t\t\tscale: [0, scale, 0],\n\t\t\t\trotate: [75, 120, 150],\n\t\t\t}}\n\t\t\ttransition={{ duration: 0.8, repeat: Infinity, delay }}\n\t\t\twidth=\"21\"\n\t\t\theight=\"21\"\n\t\t\tviewBox=\"0 0 21 21\"\n\t\t>\n\t\t\t<path\n\t\t\t\td=\"M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z\"\n\t\t\t\tfill={color}\n\t\t\t/>\n\t\t</motion.svg>\n\t)\n}\n\ninterface SparklesTextProps {\n\t/**\n\t * @default <div />\n\t * @type ReactElement\n\t * @description\n\t * The component to be rendered as the text\n\t * */\n\tas?: ReactElement\n\n\t/**\n\t * @default \"\"\n\t * @type string\n\t * @description\n\t * The className of the text\n\t */\n\tclassName?: string\n\n\t/**\n\t * @required\n\t * @type ReactNode\n\t * @description\n\t * The content to be displayed\n\t * */\n\tchildren: React.ReactNode\n\n\t/**\n\t * @default 10\n\t * @type number\n\t * @description\n\t * The count of sparkles\n\t * */\n\tsparklesCount?: number\n\n\t/**\n\t * @default \"{first: '#9E7AFF', second: '#FE8BBB'}\"\n\t * @type string\n\t * @description\n\t * The colors of the sparkles\n\t * */\n\tcolors?: {\n\t\tfirst: string\n\t\tsecond: string\n\t}\n}\n\nexport const SparklesText: React.FC<SparklesTextProps> = ({\n\tchildren,\n\tcolors = { first: '#9E7AFF', second: '#FE8BBB' },\n\tclassName,\n\tsparklesCount = 10,\n\t...props\n}) => {\n\tconst [sparkles, setSparkles] = useState<Sparkle[]>([])\n\n\tuseEffect(() => {\n\t\tconst generateStar = (): Sparkle => {\n\t\t\tconst starX = `${Math.random() * 100}%`\n\t\t\tconst starY = `${Math.random() * 100}%`\n\t\t\tconst color = Math.random() > 0.5 ? colors.first : colors.second\n\t\t\tconst delay = Math.random() * 2\n\t\t\tconst scale = Math.random() * 1 + 0.3\n\t\t\tconst lifespan = Math.random() * 10 + 5\n\t\t\tconst id = `${starX}-${starY}-${Date.now()}`\n\t\t\treturn { id, x: starX, y: starY, color, delay, scale, lifespan }\n\t\t}\n\n\t\tconst initializeStars = () => {\n\t\t\tconst newSparkles = Array.from({ length: sparklesCount }, generateStar)\n\t\t\tsetSparkles(newSparkles)\n\t\t}\n\n\t\tconst updateStars = () => {\n\t\t\tsetSparkles((currentSparkles) =>\n\t\t\t\tcurrentSparkles.map((star) => {\n\t\t\t\t\tif (star.lifespan <= 0) {\n\t\t\t\t\t\treturn generateStar()\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn { ...star, lifespan: star.lifespan - 0.1 }\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\n\t\tinitializeStars()\n\t\tconst interval = setInterval(updateStars, 100)\n\n\t\treturn () => clearInterval(interval)\n\t}, [colors.first, colors.second, sparklesCount])\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn('text-6xl font-bold', className)}\n\t\t\t{...props}\n\t\t\tstyle={\n\t\t\t\t{\n\t\t\t\t\t'--sparkles-first-color': `${colors.first}`,\n\t\t\t\t\t'--sparkles-second-color': `${colors.second}`,\n\t\t\t\t} as CSSProperties\n\t\t\t}\n\t\t>\n\t\t\t<span className=\"relative inline-block\">\n\t\t\t\t{sparkles.map((sparkle) => (\n\t\t\t\t\t<Sparkle key={sparkle.id} {...sparkle} />\n\t\t\t\t))}\n\t\t\t\t<strong className=\"bg-linear-to-r from-[var(--sparkles-first-color)] to-[var(--sparkles-second-color)] bg-clip-text text-transparent\">\n\t\t\t\t\t{children}\n\t\t\t\t</strong>\n\t\t\t</span>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/components/ui/spinner.tsx",
    "content": "import { Loader2Icon } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Spinner({ className, ...props }: React.ComponentProps<'svg'>) {\n\treturn (\n\t\t<Loader2Icon\n\t\t\trole=\"status\"\n\t\t\taria-label=\"Loading\"\n\t\t\tclassName={cn('size-4 animate-spin', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Spinner }\n"
  },
  {
    "path": "packages/website/src/components/ui/switch.tsx",
    "content": "import * as SwitchPrimitive from '@radix-ui/react-switch'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {\n\treturn (\n\t\t<SwitchPrimitive.Root\n\t\t\tdata-slot=\"switch\"\n\t\t\tclassName={cn(\n\t\t\t\t'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t>\n\t\t\t<SwitchPrimitive.Thumb\n\t\t\t\tdata-slot=\"switch-thumb\"\n\t\t\t\tclassName={cn(\n\t\t\t\t\t'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0'\n\t\t\t\t)}\n\t\t\t/>\n\t\t</SwitchPrimitive.Root>\n\t)\n}\n\nexport { Switch }\n"
  },
  {
    "path": "packages/website/src/components/ui/text-animate.tsx",
    "content": "import { AnimatePresence, MotionProps, Variants, motion } from 'motion/react'\nimport { ElementType, memo } from 'react'\n\nimport { cn } from '@/lib/utils'\n\ntype AnimationType = 'text' | 'word' | 'character' | 'line'\ntype AnimationVariant =\n\t| 'fadeIn'\n\t| 'blurIn'\n\t| 'blurInUp'\n\t| 'blurInDown'\n\t| 'slideUp'\n\t| 'slideDown'\n\t| 'slideLeft'\n\t| 'slideRight'\n\t| 'scaleUp'\n\t| 'scaleDown'\n\ninterface TextAnimateProps extends MotionProps {\n\t/**\n\t * The text content to animate\n\t */\n\tchildren: string\n\t/**\n\t * The class name to be applied to the component\n\t */\n\tclassName?: string\n\t/**\n\t * The class name to be applied to each segment\n\t */\n\tsegmentClassName?: string\n\t/**\n\t * The delay before the animation starts\n\t */\n\tdelay?: number\n\t/**\n\t * The duration of the animation\n\t */\n\tduration?: number\n\t/**\n\t * Custom motion variants for the animation\n\t */\n\tvariants?: Variants\n\t/**\n\t * The element type to render\n\t */\n\tas?: ElementType\n\t/**\n\t * How to split the text (\"text\", \"word\", \"character\")\n\t */\n\tby?: AnimationType\n\t/**\n\t * Whether to start animation when component enters viewport\n\t */\n\tstartOnView?: boolean\n\t/**\n\t * Whether to animate only once\n\t */\n\tonce?: boolean\n\t/**\n\t * The animation preset to use\n\t */\n\tanimation?: AnimationVariant\n\t/**\n\t * Whether to enable accessibility features (default: true)\n\t */\n\taccessible?: boolean\n}\n\nconst staggerTimings: Record<AnimationType, number> = {\n\ttext: 0.06,\n\tword: 0.05,\n\tcharacter: 0.03,\n\tline: 0.06,\n}\n\nconst defaultContainerVariants = {\n\thidden: { opacity: 1 },\n\tshow: {\n\t\topacity: 1,\n\t\ttransition: {\n\t\t\tdelayChildren: 0,\n\t\t\tstaggerChildren: 0.05,\n\t\t},\n\t},\n\texit: {\n\t\topacity: 0,\n\t\ttransition: {\n\t\t\tstaggerChildren: 0.05,\n\t\t\tstaggerDirection: -1,\n\t\t},\n\t},\n}\n\nconst defaultItemVariants: Variants = {\n\thidden: { opacity: 0 },\n\tshow: {\n\t\topacity: 1,\n\t},\n\texit: {\n\t\topacity: 0,\n\t},\n}\n\nconst defaultItemAnimationVariants: Record<\n\tAnimationVariant,\n\t{ container: Variants; item: Variants }\n> = {\n\tfadeIn: {\n\t\tcontainer: defaultContainerVariants,\n\t\titem: {\n\t\t\thidden: { opacity: 0, y: 20 },\n\t\t\tshow: {\n\t\t\t\topacity: 1,\n\t\t\t\ty: 0,\n\t\t\t\ttransition: {\n\t\t\t\t\tduration: 0.3,\n\t\t\t\t},\n\t\t\t},\n\t\t\texit: {\n\t\t\t\topacity: 0,\n\t\t\t\ty: 20,\n\t\t\t\ttransition: { duration: 0.3 },\n\t\t\t},\n\t\t},\n\t},\n\tblurIn: {\n\t\tcontainer: defaultContainerVariants,\n\t\titem: {\n\t\t\thidden: { opacity: 0, filter: 'blur(10px)' },\n\t\t\tshow: {\n\t\t\t\topacity: 1,\n\t\t\t\tfilter: 'blur(0px)',\n\t\t\t\ttransition: {\n\t\t\t\t\tduration: 0.3,\n\t\t\t\t},\n\t\t\t},\n\t\t\texit: {\n\t\t\t\topacity: 0,\n\t\t\t\tfilter: 'blur(10px)',\n\t\t\t\ttransition: { duration: 0.3 },\n\t\t\t},\n\t\t},\n\t},\n\tblurInUp: {\n\t\tcontainer: defaultContainerVariants,\n\t\titem: {\n\t\t\thidden: { opacity: 0, filter: 'blur(10px)', y: 20 },\n\t\t\tshow: {\n\t\t\t\topacity: 1,\n\t\t\t\tfilter: 'blur(0px)',\n\t\t\t\ty: 0,\n\t\t\t\ttransition: {\n\t\t\t\t\ty: { duration: 0.3 },\n\t\t\t\t\topacity: { duration: 0.4 },\n\t\t\t\t\tfilter: { duration: 0.3 },\n\t\t\t\t},\n\t\t\t},\n\t\t\texit: {\n\t\t\t\topacity: 0,\n\t\t\t\tfilter: 'blur(10px)',\n\t\t\t\ty: 20,\n\t\t\t\ttransition: {\n\t\t\t\t\ty: { duration: 0.3 },\n\t\t\t\t\topacity: { duration: 0.4 },\n\t\t\t\t\tfilter: { duration: 0.3 },\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\tblurInDown: {\n\t\tcontainer: defaultContainerVariants,\n\t\titem: {\n\t\t\thidden: { opacity: 0, filter: 'blur(10px)', y: -20 },\n\t\t\tshow: {\n\t\t\t\topacity: 1,\n\t\t\t\tfilter: 'blur(0px)',\n\t\t\t\ty: 0,\n\t\t\t\ttransition: {\n\t\t\t\t\ty: { duration: 0.3 },\n\t\t\t\t\topacity: { duration: 0.4 },\n\t\t\t\t\tfilter: { duration: 0.3 },\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\tslideUp: {\n\t\tcontainer: defaultContainerVariants,\n\t\titem: {\n\t\t\thidden: { y: 20, opacity: 0 },\n\t\t\tshow: {\n\t\t\t\ty: 0,\n\t\t\t\topacity: 1,\n\t\t\t\ttransition: {\n\t\t\t\t\tduration: 0.3,\n\t\t\t\t},\n\t\t\t},\n\t\t\texit: {\n\t\t\t\ty: -20,\n\t\t\t\topacity: 0,\n\t\t\t\ttransition: {\n\t\t\t\t\tduration: 0.3,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\tslideDown: {\n\t\tcontainer: defaultContainerVariants,\n\t\titem: {\n\t\t\thidden: { y: -20, opacity: 0 },\n\t\t\tshow: {\n\t\t\t\ty: 0,\n\t\t\t\topacity: 1,\n\t\t\t\ttransition: { duration: 0.3 },\n\t\t\t},\n\t\t\texit: {\n\t\t\t\ty: 20,\n\t\t\t\topacity: 0,\n\t\t\t\ttransition: { duration: 0.3 },\n\t\t\t},\n\t\t},\n\t},\n\tslideLeft: {\n\t\tcontainer: defaultContainerVariants,\n\t\titem: {\n\t\t\thidden: { x: 20, opacity: 0 },\n\t\t\tshow: {\n\t\t\t\tx: 0,\n\t\t\t\topacity: 1,\n\t\t\t\ttransition: { duration: 0.3 },\n\t\t\t},\n\t\t\texit: {\n\t\t\t\tx: -20,\n\t\t\t\topacity: 0,\n\t\t\t\ttransition: { duration: 0.3 },\n\t\t\t},\n\t\t},\n\t},\n\tslideRight: {\n\t\tcontainer: defaultContainerVariants,\n\t\titem: {\n\t\t\thidden: { x: -20, opacity: 0 },\n\t\t\tshow: {\n\t\t\t\tx: 0,\n\t\t\t\topacity: 1,\n\t\t\t\ttransition: { duration: 0.3 },\n\t\t\t},\n\t\t\texit: {\n\t\t\t\tx: 20,\n\t\t\t\topacity: 0,\n\t\t\t\ttransition: { duration: 0.3 },\n\t\t\t},\n\t\t},\n\t},\n\tscaleUp: {\n\t\tcontainer: defaultContainerVariants,\n\t\titem: {\n\t\t\thidden: { scale: 0.5, opacity: 0 },\n\t\t\tshow: {\n\t\t\t\tscale: 1,\n\t\t\t\topacity: 1,\n\t\t\t\ttransition: {\n\t\t\t\t\tduration: 0.3,\n\t\t\t\t\tscale: {\n\t\t\t\t\t\ttype: 'spring',\n\t\t\t\t\t\tdamping: 15,\n\t\t\t\t\t\tstiffness: 300,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texit: {\n\t\t\t\tscale: 0.5,\n\t\t\t\topacity: 0,\n\t\t\t\ttransition: { duration: 0.3 },\n\t\t\t},\n\t\t},\n\t},\n\tscaleDown: {\n\t\tcontainer: defaultContainerVariants,\n\t\titem: {\n\t\t\thidden: { scale: 1.5, opacity: 0 },\n\t\t\tshow: {\n\t\t\t\tscale: 1,\n\t\t\t\topacity: 1,\n\t\t\t\ttransition: {\n\t\t\t\t\tduration: 0.3,\n\t\t\t\t\tscale: {\n\t\t\t\t\t\ttype: 'spring',\n\t\t\t\t\t\tdamping: 15,\n\t\t\t\t\t\tstiffness: 300,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texit: {\n\t\t\t\tscale: 1.5,\n\t\t\t\topacity: 0,\n\t\t\t\ttransition: { duration: 0.3 },\n\t\t\t},\n\t\t},\n\t},\n}\n\nconst TextAnimateBase = ({\n\tchildren,\n\tdelay = 0,\n\tduration = 0.3,\n\tvariants,\n\tclassName,\n\tsegmentClassName,\n\tas: Component = 'p',\n\tstartOnView = true,\n\tonce = false,\n\tby = 'word',\n\tanimation = 'fadeIn',\n\taccessible = true,\n\t...props\n}: TextAnimateProps) => {\n\tconst MotionComponent = motion.create(Component)\n\n\tlet segments: string[] = []\n\tswitch (by) {\n\t\tcase 'word':\n\t\t\tsegments = children.split(/(\\s+)/)\n\t\t\tbreak\n\t\tcase 'character':\n\t\t\tsegments = children.split('')\n\t\t\tbreak\n\t\tcase 'line':\n\t\t\tsegments = children.split('\\n')\n\t\t\tbreak\n\t\tcase 'text':\n\t\tdefault:\n\t\t\tsegments = [children]\n\t\t\tbreak\n\t}\n\n\tconst finalVariants = variants\n\t\t? {\n\t\t\t\tcontainer: {\n\t\t\t\t\thidden: { opacity: 0 },\n\t\t\t\t\tshow: {\n\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\ttransition: {\n\t\t\t\t\t\t\topacity: { duration: 0.01, delay },\n\t\t\t\t\t\t\tdelayChildren: delay,\n\t\t\t\t\t\t\tstaggerChildren: duration / segments.length,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\texit: {\n\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\ttransition: {\n\t\t\t\t\t\t\tstaggerChildren: duration / segments.length,\n\t\t\t\t\t\t\tstaggerDirection: -1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\titem: variants,\n\t\t\t}\n\t\t: animation\n\t\t\t? {\n\t\t\t\t\tcontainer: {\n\t\t\t\t\t\t...defaultItemAnimationVariants[animation].container,\n\t\t\t\t\t\tshow: {\n\t\t\t\t\t\t\t...defaultItemAnimationVariants[animation].container.show,\n\t\t\t\t\t\t\ttransition: {\n\t\t\t\t\t\t\t\tdelayChildren: delay,\n\t\t\t\t\t\t\t\tstaggerChildren: duration / segments.length,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\texit: {\n\t\t\t\t\t\t\t...defaultItemAnimationVariants[animation].container.exit,\n\t\t\t\t\t\t\ttransition: {\n\t\t\t\t\t\t\t\tstaggerChildren: duration / segments.length,\n\t\t\t\t\t\t\t\tstaggerDirection: -1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\titem: defaultItemAnimationVariants[animation].item,\n\t\t\t\t}\n\t\t\t: { container: defaultContainerVariants, item: defaultItemVariants }\n\n\treturn (\n\t\t<AnimatePresence mode=\"popLayout\">\n\t\t\t<MotionComponent\n\t\t\t\tvariants={finalVariants.container as Variants}\n\t\t\t\tinitial=\"hidden\"\n\t\t\t\twhileInView={startOnView ? 'show' : undefined}\n\t\t\t\tanimate={startOnView ? undefined : 'show'}\n\t\t\t\texit=\"exit\"\n\t\t\t\tclassName={cn('whitespace-pre-wrap', className)}\n\t\t\t\tviewport={{ once }}\n\t\t\t\taria-label={accessible ? children : undefined}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t{accessible && <span className=\"sr-only\">{children}</span>}\n\t\t\t\t{segments.map((segment, i) => (\n\t\t\t\t\t<motion.span\n\t\t\t\t\t\tkey={`${by}-${segment}-${i}`}\n\t\t\t\t\t\tvariants={finalVariants.item}\n\t\t\t\t\t\tcustom={i * staggerTimings[by]}\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\tby === 'line' ? 'block' : 'inline-block whitespace-pre',\n\t\t\t\t\t\t\tby === 'character' && '',\n\t\t\t\t\t\t\tsegmentClassName\n\t\t\t\t\t\t)}\n\t\t\t\t\t\taria-hidden={accessible ? true : undefined}\n\t\t\t\t\t>\n\t\t\t\t\t\t{segment}\n\t\t\t\t\t</motion.span>\n\t\t\t\t))}\n\t\t\t</MotionComponent>\n\t\t</AnimatePresence>\n\t)\n}\n\n// Export the memoized version\nexport const TextAnimate = memo(TextAnimateBase)\n"
  },
  {
    "path": "packages/website/src/components/ui/tooltip.tsx",
    "content": "import * as TooltipPrimitive from '@radix-ui/react-tooltip'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction TooltipProvider({\n\tdelayDuration = 0,\n\t...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n\treturn (\n\t\t<TooltipPrimitive.Provider\n\t\t\tdata-slot=\"tooltip-provider\"\n\t\t\tdelayDuration={delayDuration}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n\treturn (\n\t\t<TooltipProvider>\n\t\t\t<TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n\t\t</TooltipProvider>\n\t)\n}\n\nfunction TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n\treturn <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />\n}\n\nfunction TooltipContent({\n\tclassName,\n\tsideOffset = 0,\n\tchildren,\n\t...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n\treturn (\n\t\t<TooltipPrimitive.Portal>\n\t\t\t<TooltipPrimitive.Content\n\t\t\t\tdata-slot=\"tooltip-content\"\n\t\t\t\tsideOffset={sideOffset}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t\t<TooltipPrimitive.Arrow className=\"bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]\" />\n\t\t\t</TooltipPrimitive.Content>\n\t\t</TooltipPrimitive.Portal>\n\t)\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }\n"
  },
  {
    "path": "packages/website/src/components/ui/typing-animation.tsx",
    "content": "import { MotionProps, motion, useInView } from 'motion/react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\n\nimport { cn } from '@/lib/utils'\n\ninterface TypingAnimationProps extends MotionProps {\n\tchildren?: string\n\twords?: string[]\n\tclassName?: string\n\tduration?: number\n\ttypeSpeed?: number\n\tdeleteSpeed?: number\n\tdelay?: number\n\tpauseDelay?: number\n\tloop?: boolean\n\tas?: React.ElementType\n\tstartOnView?: boolean\n\tshowCursor?: boolean\n\tblinkCursor?: boolean\n\tcursorStyle?: 'line' | 'block' | 'underscore'\n}\n\nexport function TypingAnimation({\n\tchildren,\n\twords,\n\tclassName,\n\tduration = 100,\n\ttypeSpeed,\n\tdeleteSpeed,\n\tdelay = 0,\n\tpauseDelay = 1000,\n\tloop = false,\n\tas: Component = 'span',\n\tstartOnView = true,\n\tshowCursor = true,\n\tblinkCursor = true,\n\tcursorStyle = 'line',\n\t...props\n}: TypingAnimationProps) {\n\tconst MotionComponent = motion.create(Component, {\n\t\tforwardMotionProps: true,\n\t})\n\n\tconst [displayedText, setDisplayedText] = useState<string>('')\n\tconst [currentWordIndex, setCurrentWordIndex] = useState(0)\n\tconst [currentCharIndex, setCurrentCharIndex] = useState(0)\n\tconst [phase, setPhase] = useState<'typing' | 'pause' | 'deleting'>('typing')\n\tconst elementRef = useRef<HTMLElement | null>(null)\n\tconst isInView = useInView(elementRef as React.RefObject<Element>, {\n\t\tamount: 0.3,\n\t\tonce: true,\n\t})\n\n\tconst wordsToAnimate = useMemo(() => words || (children ? [children] : []), [words, children])\n\tconst hasMultipleWords = wordsToAnimate.length > 1\n\n\tconst typingSpeed = typeSpeed || duration\n\tconst deletingSpeed = deleteSpeed || typingSpeed / 2\n\n\tconst shouldStart = startOnView ? isInView : true\n\n\tuseEffect(() => {\n\t\tif (!shouldStart || wordsToAnimate.length === 0) return\n\n\t\tconst timeoutDelay =\n\t\t\tdelay > 0 && displayedText === ''\n\t\t\t\t? delay\n\t\t\t\t: phase === 'typing'\n\t\t\t\t\t? typingSpeed\n\t\t\t\t\t: phase === 'deleting'\n\t\t\t\t\t\t? deletingSpeed\n\t\t\t\t\t\t: pauseDelay\n\n\t\tconst timeout = setTimeout(() => {\n\t\t\tconst currentWord = wordsToAnimate[currentWordIndex] || ''\n\t\t\tconst graphemes = Array.from(currentWord)\n\n\t\t\tswitch (phase) {\n\t\t\t\tcase 'typing':\n\t\t\t\t\tif (currentCharIndex < graphemes.length) {\n\t\t\t\t\t\tsetDisplayedText(graphemes.slice(0, currentCharIndex + 1).join(''))\n\t\t\t\t\t\tsetCurrentCharIndex(currentCharIndex + 1)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (hasMultipleWords || loop) {\n\t\t\t\t\t\t\tconst isLastWord = currentWordIndex === wordsToAnimate.length - 1\n\t\t\t\t\t\t\tif (!isLastWord || loop) {\n\t\t\t\t\t\t\t\tsetPhase('pause')\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase 'pause':\n\t\t\t\t\tsetPhase('deleting')\n\t\t\t\t\tbreak\n\n\t\t\t\tcase 'deleting':\n\t\t\t\t\tif (currentCharIndex > 0) {\n\t\t\t\t\t\tsetDisplayedText(graphemes.slice(0, currentCharIndex - 1).join(''))\n\t\t\t\t\t\tsetCurrentCharIndex(currentCharIndex - 1)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst nextIndex = (currentWordIndex + 1) % wordsToAnimate.length\n\t\t\t\t\t\tsetCurrentWordIndex(nextIndex)\n\t\t\t\t\t\tsetPhase('typing')\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t}\n\t\t}, timeoutDelay)\n\n\t\treturn () => clearTimeout(timeout)\n\t}, [\n\t\tshouldStart,\n\t\tphase,\n\t\tcurrentCharIndex,\n\t\tcurrentWordIndex,\n\t\tdisplayedText,\n\t\twordsToAnimate,\n\t\thasMultipleWords,\n\t\tloop,\n\t\ttypingSpeed,\n\t\tdeletingSpeed,\n\t\tpauseDelay,\n\t\tdelay,\n\t])\n\n\tconst currentWordGraphemes = Array.from(wordsToAnimate[currentWordIndex] || '')\n\tconst isComplete =\n\t\t!loop &&\n\t\tcurrentWordIndex === wordsToAnimate.length - 1 &&\n\t\tcurrentCharIndex >= currentWordGraphemes.length &&\n\t\tphase !== 'deleting'\n\n\tconst shouldShowCursor =\n\t\tshowCursor &&\n\t\t!isComplete &&\n\t\t(hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)\n\n\tconst getCursorChar = () => {\n\t\tswitch (cursorStyle) {\n\t\t\tcase 'block':\n\t\t\t\treturn '▌'\n\t\t\tcase 'underscore':\n\t\t\t\treturn '_'\n\t\t\tcase 'line':\n\t\t\tdefault:\n\t\t\t\treturn '|'\n\t\t}\n\t}\n\n\treturn (\n\t\t<MotionComponent\n\t\t\tref={elementRef}\n\t\t\tclassName={cn('leading-[5rem] tracking-[-0.02em]', className)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{displayedText}\n\t\t\t{shouldShowCursor && (\n\t\t\t\t<span className={cn('inline-block', blinkCursor && 'animate-blink-cursor')}>\n\t\t\t\t\t{getCursorChar()}\n\t\t\t\t</span>\n\t\t\t)}\n\t\t</MotionComponent>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/constants.ts",
    "content": "// Demo build (auto-init with demo LLM, for quick testing)\nexport const CDN_DEMO_URL =\n\t'https://cdn.jsdelivr.net/npm/page-agent@1.6.0/dist/iife/page-agent.demo.js'\nexport const CDN_DEMO_CN_URL =\n\t'https://registry.npmmirror.com/page-agent/1.6.0/files/dist/iife/page-agent.demo.js'\n\n// Demo LLM for website testing (homepage quick trial uses flash)\nexport const DEMO_MODEL = 'qwen3.5-flash'\nexport const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'\n// export const DEMO_API_KEY = ''\n"
  },
  {
    "path": "packages/website/src/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n\treadonly VERSION: string\n}\n\ndeclare module '*.module.css' {\n\tconst classes: Record<string, string>\n\texport default classes\n}\n"
  },
  {
    "path": "packages/website/src/hooks/useGitHubStars.ts",
    "content": "import { useEffect, useState } from 'react'\n\nconst STATS_URL = 'https://page-agent.github.io/gh-stats/stats.json'\n\nlet cached: number | null = null\n\nexport function useGitHubStars() {\n\tconst [stars, setStars] = useState(cached)\n\n\tuseEffect(() => {\n\t\tif (cached !== null) return\n\t\tfetch(STATS_URL)\n\t\t\t.then((r) => r.json())\n\t\t\t.then((data) => {\n\t\t\t\tcached = data.stargazers_count ?? null\n\t\t\t\tsetStars(cached)\n\t\t\t})\n\t\t\t.catch(() => {})\n\t}, [])\n\n\treturn stars\n}\n\nexport function formatStars(n: number): string {\n\tif (n >= 1000) return `${(n / 1000).toFixed(1).replace(/\\.0$/, '')}k`\n\treturn String(n)\n}\n"
  },
  {
    "path": "packages/website/src/i18n/context.tsx",
    "content": "import { ReactNode, createContext, use, useState } from 'react'\n\ntype Lang = 'en-US' | 'zh-CN'\n\nconst LanguageContext = createContext<{\n\tlanguage: Lang\n\tisZh: boolean\n\tsetLanguage: (lang: Lang) => void\n} | null>(null)\n\nexport function LanguageProvider({ children }: { children: ReactNode }) {\n\tconst [language, setLang] = useState<Lang>(() => {\n\t\tconst stored = localStorage.getItem('language') as Lang\n\t\tif (stored === 'zh-CN' || stored === 'en-US') return stored\n\t\treturn navigator.language.startsWith('zh') ? 'zh-CN' : 'en-US'\n\t})\n\n\tconst setLanguage = (lang: Lang) => {\n\t\tsetLang(lang)\n\t\tlocalStorage.setItem('language', lang)\n\t}\n\n\treturn (\n\t\t<LanguageContext value={{ language, isZh: language === 'zh-CN', setLanguage }}>\n\t\t\t{children}\n\t\t</LanguageContext>\n\t)\n}\n\n// eslint-disable-next-line react-refresh/only-export-components\nexport function useLanguage() {\n\tconst ctx = use(LanguageContext)\n\tif (!ctx) throw new Error('useLanguage must be used within LanguageProvider')\n\treturn ctx\n}\n"
  },
  {
    "path": "packages/website/src/index.css",
    "content": "@config '../tailwind.config.js';\n@import 'tailwindcss';\n@import 'tw-animate-css';\n\n@custom-variant dark (&:is(.dark *));\n\n/* 启用 class-based dark mode for Tailwind v4 */\n@variant dark (.dark &);\n\n:root {\n\t--background: oklch(1 0 0);\n\t--foreground: oklch(0.145 0 0);\n\t/* 主题色渐变 */\n\t--theme-color-1: rgb(88, 192, 252);\n\t--theme-color-2: rgb(189, 69, 251);\n\n\t/* shadcn */\n\t--radius: 0.625rem;\n\t--card: oklch(1 0 0);\n\t--card-foreground: oklch(0.145 0 0);\n\t--popover: oklch(1 0 0);\n\t--popover-foreground: oklch(0.145 0 0);\n\t--primary: oklch(0.205 0 0);\n\t--primary-foreground: oklch(0.985 0 0);\n\t--secondary: oklch(0.97 0 0);\n\t--secondary-foreground: oklch(0.205 0 0);\n\t--muted: oklch(0.97 0 0);\n\t--muted-foreground: oklch(0.556 0 0);\n\t--accent: oklch(0.97 0 0);\n\t--accent-foreground: oklch(0.205 0 0);\n\t--destructive: oklch(0.577 0.245 27.325);\n\t--border: oklch(0.922 0 0);\n\t--input: oklch(0.922 0 0);\n\t--ring: oklch(0.708 0 0);\n\t--chart-1: oklch(0.646 0.222 41.116);\n\t--chart-2: oklch(0.6 0.118 184.704);\n\t--chart-3: oklch(0.398 0.07 227.392);\n\t--chart-4: oklch(0.828 0.189 84.429);\n\t--chart-5: oklch(0.769 0.188 70.08);\n\t--sidebar: oklch(0.985 0 0);\n\t--sidebar-foreground: oklch(0.145 0 0);\n\t--sidebar-primary: oklch(0.205 0 0);\n\t--sidebar-primary-foreground: oklch(0.985 0 0);\n\t--sidebar-accent: oklch(0.97 0 0);\n\t--sidebar-accent-foreground: oklch(0.205 0 0);\n\t--sidebar-border: oklch(0.922 0 0);\n\t--sidebar-ring: oklch(0.708 0 0);\n}\n\n/* class-based dark mode - 应用到 html.dark */\nhtml.dark,\n:root.dark {\n\t--background: #0a0a0a;\n\t--foreground: #ededed;\n}\n\n/* 同时支持系统偏好 */\n/* @media (prefers-color-scheme: dark) {\n\thtml:not(.light),\n\t:root:not(.light) {\n\t\t--background: #0a0a0a;\n\t\t--foreground: #ededed;\n\t}\n} */\n\n/* 添加 Tailwind 自定义颜色 */\n@theme {\n\t--color-background: var(--background);\n\t--color-foreground: var(--foreground);\n}\n\nbody {\n\tbackground: var(--background);\n\tcolor: var(--foreground);\n\tfont-family:\n\t\tsystem-ui,\n\t\t-apple-system,\n\t\tBlinkMacSystemFont,\n\t\t'Segoe UI',\n\t\tRoboto,\n\t\t'Noto Sans',\n\t\t'Liberation Sans',\n\t\tsans-serif,\n\t\t'Apple Color Emoji',\n\t\t'Segoe UI Emoji';\n}\n\n/* 文档正文排版优化 */\n.prose {\n\tletter-spacing: 0.01em;\n\tfont-weight: 380;\n}\n\n.prose p {\n\tline-height: 1.6;\n}\n\n/* 标题使用中等字重（相对细体更重，但比默认 bold 更轻） */\n.prose h1,\n.prose h2,\n.prose h3,\n.prose h4,\n.prose h5,\n.prose h6 {\n\tfont-weight: 480;\n}\n\n/* strong/b 也用中等字重 */\n.prose strong,\n.prose b {\n\tfont-weight: 480;\n}\n\n/* 确保文档页面标题在暗色模式下可见 - 只针对 prose 内的标题 */\n.prose h1,\n.prose h2,\n.prose h3,\n.prose h4,\n.prose h5,\n.prose h6 {\n\tcolor: rgba(23, 23, 23, 0.85);\n}\n\n.dark .prose h1,\n.dark .prose h2,\n.dark .prose h3,\n.dark .prose h4,\n.dark .prose h5,\n.dark .prose h6 {\n\tcolor: rgba(255, 255, 255, 0.9);\n}\n\ntable,\nth,\ntd {\n\tcolor: #171717;\n}\n\n.dark table,\n.dark th,\n.dark td {\n\tcolor: #ededed;\n}\n\n/* 文档页深色模式优化 */\n.dark .prose {\n\tcolor: rgba(255, 255, 255, 0.7);\n}\n\n/* @media (prefers-color-scheme: dark) {\n\t:root:not(.light) .prose {\n\t\tcolor: rgba(255, 255, 255, 0.7);\n\t}\n} */\n\n.dark .dark\\:prose-invert {\n\t--tw-prose-body: rgba(255, 255, 255, 0.7);\n\t--tw-prose-headings: rgba(255, 255, 255, 0.95);\n\t--tw-prose-lead: rgba(255, 255, 255, 0.7);\n\t--tw-prose-links: rgba(147, 197, 253, 0.9);\n\t--tw-prose-bold: rgba(255, 255, 255, 0.9);\n\t--tw-prose-counters: rgba(255, 255, 255, 0.6);\n\t--tw-prose-bullets: rgba(255, 255, 255, 0.5);\n\t--tw-prose-hr: rgba(255, 255, 255, 0.2);\n\t--tw-prose-quotes: rgba(255, 255, 255, 0.8);\n\t--tw-prose-quote-borders: rgba(255, 255, 255, 0.3);\n\t--tw-prose-captions: rgba(255, 255, 255, 0.6);\n\t--tw-prose-code: rgba(255, 255, 255, 0.9);\n\t--tw-prose-pre-code: rgba(255, 255, 255, 0.95);\n\t--tw-prose-pre-bg: rgba(0, 0, 0, 0.5);\n\t--tw-prose-th-borders: rgba(255, 255, 255, 0.3);\n\t--tw-prose-td-borders: rgba(255, 255, 255, 0.2);\n}\n\n/* @media (prefers-color-scheme: dark) {\n\t:root:not(.light) .dark\\:prose-invert {\n\t\t--tw-prose-body: rgba(255, 255, 255, 0.7);\n\t\t--tw-prose-headings: rgba(255, 255, 255, 0.95);\n\t\t--tw-prose-lead: rgba(255, 255, 255, 0.7);\n\t\t--tw-prose-links: rgba(147, 197, 253, 0.9);\n\t\t--tw-prose-bold: rgba(255, 255, 255, 0.9);\n\t\t--tw-prose-counters: rgba(255, 255, 255, 0.6);\n\t\t--tw-prose-bullets: rgba(255, 255, 255, 0.5);\n\t\t--tw-prose-hr: rgba(255, 255, 255, 0.2);\n\t\t--tw-prose-quotes: rgba(255, 255, 255, 0.8);\n\t\t--tw-prose-quote-borders: rgba(255, 255, 255, 0.3);\n\t\t--tw-prose-captions: rgba(255, 255, 255, 0.6);\n\t\t--tw-prose-code: rgba(255, 255, 255, 0.9);\n\t\t--tw-prose-pre-code: rgba(255, 255, 255, 0.95);\n\t\t--tw-prose-pre-bg: rgba(0, 0, 0, 0.5);\n\t\t--tw-prose-th-borders: rgba(255, 255, 255, 0.3);\n\t\t--tw-prose-td-borders: rgba(255, 255, 255, 0.2);\n\t}\n} */\n\n/* 标题更清晰 */\n.dark .prose h1,\n.dark .prose h2,\n.dark .prose h3,\n.dark .prose h4 {\n\tcolor: rgba(255, 255, 255, 0.95);\n}\n\n/* @media (prefers-color-scheme: dark) {\n\t:root:not(.light) .prose h1,\n\t:root:not(.light) .prose h2,\n\t:root:not(.light) .prose h3,\n\t:root:not(.light) .prose h4 {\n\t\tcolor: rgba(255, 255, 255, 0.95);\n\t}\n} */\n\n/* 链接更清晰 */\n.dark .prose a {\n\tcolor: rgba(147, 197, 253, 0.9);\n}\n\n/* @media (prefers-color-scheme: dark) {\n\t:root:not(.light) .prose a {\n\t\tcolor: rgba(147, 197, 253, 0.9);\n\t}\n} */\n\n/* 代码块背景更黑 */\n.dark .prose pre {\n\tbackground-color: rgba(0, 0, 0, 0.6);\n}\n\n/* @media (prefers-color-scheme: dark) {\n\t:root:not(.light) .prose pre {\n\t\tbackground-color: rgba(0, 0, 0, 0.6);\n\t}\n} */\n\n/* 表格样式 */\n.dark .prose table {\n\tcolor: rgba(255, 255, 255, 0.7);\n}\n\n/* @media (prefers-color-scheme: dark) {\n\t:root:not(.light) .prose table {\n\t\tcolor: rgba(255, 255, 255, 0.7);\n\t}\n} */\n\n.dark .prose thead {\n\tcolor: rgba(255, 255, 255, 0.9);\n\tborder-bottom-color: rgba(255, 255, 255, 0.3);\n}\n\n/* @media (prefers-color-scheme: dark) {\n\t:root:not(.light) .prose thead {\n\t\tcolor: rgba(255, 255, 255, 0.9);\n\t\tborder-bottom-color: rgba(255, 255, 255, 0.3);\n\t}\n} */\n\n.dark .prose tbody tr {\n\tborder-bottom-color: rgba(255, 255, 255, 0.2);\n}\n\n/* @media (prefers-color-scheme: dark) {\n\t:root:not(.light) .prose tbody tr {\n\t\tborder-bottom-color: rgba(255, 255, 255, 0.2);\n\t}\n} */\n\n/* 隐藏滚动条，但保持滚动功能 */\n.scrollbar-hide {\n\t-ms-overflow-style: none; /* IE and Edge */\n\tscrollbar-width: none; /* Firefox */\n}\n\n.scrollbar-hide::-webkit-scrollbar {\n\tdisplay: none; /* Chrome, Safari and Opera */\n}\n\n/* shadcn */\n@theme inline {\n\t--radius-sm: calc(var(--radius) - 4px);\n\t--radius-md: calc(var(--radius) - 2px);\n\t--radius-lg: var(--radius);\n\t--radius-xl: calc(var(--radius) + 4px);\n\t--radius-2xl: calc(var(--radius) + 8px);\n\t--radius-3xl: calc(var(--radius) + 12px);\n\t--radius-4xl: calc(var(--radius) + 16px);\n\t--color-background: var(--background);\n\t--color-foreground: var(--foreground);\n\t--color-card: var(--card);\n\t--color-card-foreground: var(--card-foreground);\n\t--color-popover: var(--popover);\n\t--color-popover-foreground: var(--popover-foreground);\n\t--color-primary: var(--primary);\n\t--color-primary-foreground: var(--primary-foreground);\n\t--color-secondary: var(--secondary);\n\t--color-secondary-foreground: var(--secondary-foreground);\n\t--color-muted: var(--muted);\n\t--color-muted-foreground: var(--muted-foreground);\n\t--color-accent: var(--accent);\n\t--color-accent-foreground: var(--accent-foreground);\n\t--color-destructive: var(--destructive);\n\t--color-border: var(--border);\n\t--color-input: var(--input);\n\t--color-ring: var(--ring);\n\t--color-chart-1: var(--chart-1);\n\t--color-chart-2: var(--chart-2);\n\t--color-chart-3: var(--chart-3);\n\t--color-chart-4: var(--chart-4);\n\t--color-chart-5: var(--chart-5);\n\t--color-sidebar: var(--sidebar);\n\t--color-sidebar-foreground: var(--sidebar-foreground);\n\t--color-sidebar-primary: var(--sidebar-primary);\n\t--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n\t--color-sidebar-accent: var(--sidebar-accent);\n\t--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n\t--color-sidebar-border: var(--sidebar-border);\n\t--color-sidebar-ring: var(--sidebar-ring);\n\n\t/* magic ui */\n\t--animate-blink-cursor: blink-cursor 1.2s step-end infinite;\n\t@keyframes blink-cursor {\n\t\t0%,\n\t\t49% {\n\t\t\topacity: 1;\n\t\t}\n\t\t50%,\n\t\t100% {\n\t\t\topacity: 0;\n\t\t}\n\t}\n\t--animate-aurora: aurora 8s ease-in-out infinite alternate;\n\t@keyframes aurora {\n\t\t0% {\n\t\t\tbackground-position: 0% 50%;\n\t\t\ttransform: rotate(-5deg) scale(0.9);\n\t\t}\n\t\t25% {\n\t\t\tbackground-position: 50% 100%;\n\t\t\ttransform: rotate(5deg) scale(1.1);\n\t\t}\n\t\t50% {\n\t\t\tbackground-position: 100% 50%;\n\t\t\ttransform: rotate(-3deg) scale(0.95);\n\t\t}\n\t\t75% {\n\t\t\tbackground-position: 50% 0%;\n\t\t\ttransform: rotate(3deg) scale(1.05);\n\t\t}\n\t\t100% {\n\t\t\tbackground-position: 0% 50%;\n\t\t\ttransform: rotate(-5deg) scale(0.9);\n\t\t}\n\t}\n\t--animate-shiny-text: shiny-text 8s infinite;\n\t@keyframes shiny-text {\n\t\t0%,\n\t\t90%,\n\t\t100% {\n\t\t\tbackground-position: calc(-100% - var(--shiny-width)) 0;\n\t\t}\n\t\t30%,\n\t\t60% {\n\t\t\tbackground-position: calc(100% + var(--shiny-width)) 0;\n\t\t}\n\t}\n\t--animate-gradient: gradient 8s linear infinite;\n\t@keyframes gradient {\n\t\tto {\n\t\t\tbackground-position: var(--bg-size, 300%) 0;\n\t\t}\n\t}\n\t--animate-background-position-spin: background-position-spin 3000ms infinite alternate;\n\t@keyframes background-position-spin {\n\t\t0% {\n\t\t\tbackground-position: top center;\n\t\t}\n\t\t100% {\n\t\t\tbackground-position: bottom center;\n\t\t}\n\t}\n\t--animate-marquee: marquee var(--duration) infinite linear;\n\t--animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;\n\t@keyframes marquee {\n\t\tfrom {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t\tto {\n\t\t\ttransform: translateX(calc(-100% - var(--gap)));\n\t\t}\n\t}\n\t@keyframes marquee-vertical {\n\t\tfrom {\n\t\t\ttransform: translateY(0);\n\t\t}\n\t\tto {\n\t\t\ttransform: translateY(calc(-100% - var(--gap)));\n\t\t}\n\t}\n}\n\n/* shadcn dark mode */\n.dark {\n\t--background: oklch(0.145 0 0);\n\t--foreground: oklch(0.985 0 0);\n\t--card: oklch(0.205 0 0);\n\t--card-foreground: oklch(0.985 0 0);\n\t--popover: oklch(0.205 0 0);\n\t--popover-foreground: oklch(0.985 0 0);\n\t--primary: oklch(0.922 0 0);\n\t--primary-foreground: oklch(0.205 0 0);\n\t--secondary: oklch(0.269 0 0);\n\t--secondary-foreground: oklch(0.985 0 0);\n\t--muted: oklch(0.269 0 0);\n\t--muted-foreground: oklch(0.708 0 0);\n\t--accent: oklch(0.269 0 0);\n\t--accent-foreground: oklch(0.985 0 0);\n\t--destructive: oklch(0.704 0.191 22.216);\n\t--border: oklch(1 0 0 / 10%);\n\t--input: oklch(1 0 0 / 15%);\n\t--ring: oklch(0.556 0 0);\n\t--chart-1: oklch(0.488 0.243 264.376);\n\t--chart-2: oklch(0.696 0.17 162.48);\n\t--chart-3: oklch(0.769 0.188 70.08);\n\t--chart-4: oklch(0.627 0.265 303.9);\n\t--chart-5: oklch(0.645 0.246 16.439);\n\t--sidebar: oklch(0.205 0 0);\n\t--sidebar-foreground: oklch(0.985 0 0);\n\t--sidebar-primary: oklch(0.488 0.243 264.376);\n\t--sidebar-primary-foreground: oklch(0.985 0 0);\n\t--sidebar-accent: oklch(0.269 0 0);\n\t--sidebar-accent-foreground: oklch(0.985 0 0);\n\t--sidebar-border: oklch(1 0 0 / 10%);\n\t--sidebar-ring: oklch(0.556 0 0);\n}\n\n/* shadcn base */\n@layer base {\n\t* {\n\t\t@apply border-border outline-ring/50;\n\t}\n\tbody {\n\t\t@apply bg-background text-foreground;\n\t}\n}\n"
  },
  {
    "path": "packages/website/src/lib/useDocumentTitle.ts",
    "content": "import { useEffect } from 'react'\n\nconst DEFAULT_TITLE = 'PageAgent - The GUI Agent Living in Your Webpage'\n\nexport function useDocumentTitle(title?: string) {\n\tuseEffect(() => {\n\t\tdocument.title = title ? `${title} - PageAgent` : DEFAULT_TITLE\n\t}, [title])\n}\n"
  },
  {
    "path": "packages/website/src/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "packages/website/src/main.tsx",
    "content": "import { createRoot } from 'react-dom/client'\nimport { Router } from 'wouter'\n\nimport { LanguageProvider } from './i18n/context'\nimport { default as PagesRouter } from './router'\n\nimport './index.css'\n\n// Redirect legacy hash routes (e.g. /#/docs/foo) to clean paths\nconst { hash } = window.location\nif (hash.length > 1 && hash.includes('/')) {\n\tconst path = hash.replace(/^#\\/?/, '/')\n\thistory.replaceState(null, '', '/page-agent' + path)\n}\n\ncreateRoot(document.getElementById('root')!).render(\n\t<LanguageProvider>\n\t\t<Router base=\"/page-agent\">\n\t\t\t<PagesRouter />\n\t\t</Router>\n\t</LanguageProvider>\n)\n"
  },
  {
    "path": "packages/website/src/pages/docs/Layout.tsx",
    "content": "import { ReactNode } from 'react'\nimport { siGooglechrome } from 'simple-icons'\nimport { Link, useLocation } from 'wouter'\n\nimport { SparklesText } from '@/components/ui/sparkles-text'\nimport { useLanguage } from '@/i18n/context'\nimport { useDocumentTitle } from '@/lib/useDocumentTitle'\n\ninterface DocsLayoutProps {\n\tchildren: ReactNode\n}\n\ninterface NavItem {\n\ttitle: string\n\tpath: string\n}\n\ninterface NavSection {\n\ttitle: string\n\titems: NavItem[]\n}\n\nexport default function DocsLayout({ children }: DocsLayoutProps) {\n\tconst { isZh } = useLanguage()\n\tconst [location] = useLocation()\n\n\tconst navigationSections: NavSection[] = [\n\t\t{\n\t\t\ttitle: isZh ? '介绍' : 'Introduction',\n\t\t\titems: [\n\t\t\t\t{ title: isZh ? '概览' : 'Overview', path: '/introduction/overview' },\n\t\t\t\t{ title: isZh ? '快速开始' : 'Quick Start', path: '/introduction/quick-start' },\n\t\t\t\t{ title: isZh ? '使用限制' : 'Limitations', path: '/introduction/limitations' },\n\t\t\t\t{\n\t\t\t\t\ttitle: isZh ? '故障排查' : 'Troubleshooting',\n\t\t\t\t\tpath: '/introduction/troubleshooting',\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\t{\n\t\t\ttitle: isZh ? '功能特性' : 'Features',\n\t\t\titems: [\n\t\t\t\t{ title: isZh ? '模型' : 'Models', path: '/features/models' },\n\t\t\t\t{ title: isZh ? '自定义工具' : 'Custom Tools', path: '/features/custom-tools' },\n\t\t\t\t{ title: isZh ? '知识注入' : 'Instructions', path: '/features/custom-instructions' },\n\t\t\t\t{ title: isZh ? '数据脱敏' : 'Data Masking', path: '/features/data-masking' },\n\t\t\t\t{ title: isZh ? 'Chrome 扩展' : 'Chrome Extension', path: '/features/chrome-extension' },\n\t\t\t\t{\n\t\t\t\t\ttitle: isZh ? '接入第三方 Agent' : 'Third-party Agent',\n\t\t\t\t\tpath: '/features/third-party-agent',\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\t{\n\t\t\ttitle: isZh ? '高级' : 'Advanced',\n\t\t\titems: [\n\t\t\t\t{ title: 'PageAgent', path: '/advanced/page-agent' },\n\t\t\t\t{ title: 'PageAgentCore', path: '/advanced/page-agent-core' },\n\t\t\t\t{ title: 'PageController', path: '/advanced/page-controller' },\n\t\t\t\t{ title: isZh ? '自定义 UI' : 'Custom UI', path: '/advanced/custom-ui' },\n\t\t\t\t{\n\t\t\t\t\ttitle: '🚧 ' + (isZh ? '安全与权限' : 'Security & Permissions'),\n\t\t\t\t\tpath: '/advanced/security-permissions',\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t]\n\n\tconst activeTitle = navigationSections\n\t\t.flatMap((s) => s.items)\n\t\t.find((item) => item.path === location)?.title\n\n\tuseDocumentTitle(activeTitle)\n\n\treturn (\n\t\t<div className=\"max-w-7xl mx-auto px-6 py-8 overflow-x-auto\">\n\t\t\t<div className=\"flex gap-8 min-w-225\">\n\t\t\t\t{/* Sidebar */}\n\t\t\t\t<aside className=\"w-64 shrink-0\" role=\"complementary\" aria-label=\"文档导航\">\n\t\t\t\t\t<div className=\"sticky\">\n\t\t\t\t\t\t<nav className=\"space-y-8\" role=\"navigation\" aria-label=\"文档章节\">\n\t\t\t\t\t\t\t{navigationSections.map((section) => (\n\t\t\t\t\t\t\t\t<section key={section.title}>\n\t\t\t\t\t\t\t\t\t<h3 className=\"font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-3\">\n\t\t\t\t\t\t\t\t\t\t{section.title}\n\t\t\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t\t\t<ul className=\"space-y-2\" role=\"list\">\n\t\t\t\t\t\t\t\t\t\t{section.items.map((item) => {\n\t\t\t\t\t\t\t\t\t\t\tconst isActive = location === item.path\n\t\t\t\t\t\t\t\t\t\t\tconst isChromeExtension = item.path === '/features/chrome-extension'\n\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t<li key={item.path}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\t\t\t\thref={item.path}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`block px-3 py-2 rounded-lg transition-colors duration-200 ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tisActive\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-medium'\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800'\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\taria-current={isActive ? 'page' : undefined}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isChromeExtension ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"flex items-center gap-1.5\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"w-3.5 h-3.5 shrink-0\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<path d={siGooglechrome.path} fill=\"currentColor\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<SparklesText\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-[length:inherit] font-[inherit] font-medium\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsparklesCount={3}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{item.title}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</SparklesText>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\titem.title\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t</section>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</nav>\n\t\t\t\t\t</div>\n\t\t\t\t</aside>\n\n\t\t\t\t{/* Main Content */}\n\t\t\t\t<main className=\"flex-1 min-w-0\" id=\"main-content\" role=\"main\">\n\t\t\t\t\t<div className=\"prose dark:prose-invert max-w-none\">{children}</div>\n\t\t\t\t</main>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/advanced/custom-ui/page.tsx",
    "content": "import { APIDivider, APIReference } from '@/components/APIReference'\nimport CodeEditor from '@/components/CodeEditor'\nimport { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\nexport default function CustomUIDocs() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<div>\n\t\t\t<h1 className=\"text-4xl font-bold mb-6\">{isZh ? '自定义 UI' : 'Custom UI'}</h1>\n\n\t\t\t<p className=\"text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed\">\n\t\t\t\t{isZh\n\t\t\t\t\t? 'PageAgent 的核心逻辑（PageAgentCore）和 UI 完全解耦，通过事件通讯。你可以用自己的 UI 替换内置 Panel。'\n\t\t\t\t\t: 'PageAgent core logic (PageAgentCore) is fully decoupled from UI through events. You can replace the built-in Panel with your own UI.'}\n\t\t\t</p>\n\n\t\t\t{/* Architecture */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"architecture\">{isZh ? '架构' : 'Architecture'}</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? 'PageAgent 由三个独立模块组成，可自由组合：'\n\t\t\t\t\t\t: 'PageAgent consists of three independent modules that can be freely combined:'}\n\t\t\t\t</p>\n\t\t\t\t<ul className=\"list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4\">\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<strong>PageAgentCore</strong> -{' '}\n\t\t\t\t\t\t{isZh ? '核心 Agent 逻辑，不包含 UI' : 'Core agent logic, no UI'}\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<strong>PageController</strong> -{' '}\n\t\t\t\t\t\t{isZh ? 'DOM 操作和视觉反馈' : 'DOM operations and visual feedback'}\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<strong>UI (Panel)</strong> -{' '}\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '用户界面，可替换为自定义实现'\n\t\t\t\t\t\t\t: 'User interface, replaceable with custom implementation'}\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\n\t\t\t<APIDivider title={isZh ? '事件系统' : 'Event System'} />\n\n\t\t\t{/* Two Event Streams */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"two-event-streams\">{isZh ? '两个事件流' : 'Two Event Streams'}</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? 'PageAgentCore 提供两种不同性质的事件流，方便 UI 渲染：'\n\t\t\t\t\t\t: 'PageAgentCore provides two distinct event streams for UI rendering:'}\n\t\t\t\t</p>\n\n\t\t\t\t{/* Comparison Table */}\n\t\t\t\t<div className=\"overflow-x-auto mb-6\">\n\t\t\t\t\t<table className=\"w-full border-collapse border border-gray-300 dark:border-gray-600\">\n\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t<tr className=\"bg-gray-100 dark:bg-gray-800\">\n\t\t\t\t\t\t\t\t<th className=\"border border-gray-300 dark:border-gray-600 px-4 py-2 text-left\"></th>\n\t\t\t\t\t\t\t\t<th className=\"border border-gray-300 dark:border-gray-600 px-4 py-2 text-left\">\n\t\t\t\t\t\t\t\t\tHistorical Events\n\t\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t\t<th className=\"border border-gray-300 dark:border-gray-600 px-4 py-2 text-left\">\n\t\t\t\t\t\t\t\t\tActivity Events\n\t\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-2\">\n\t\t\t\t\t\t\t\t\t{isZh ? '事件名' : 'Event Name'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-2\">\n\t\t\t\t\t\t\t\t\t<code>historychange</code>\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-2\">\n\t\t\t\t\t\t\t\t\t<code>activity</code>\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-2\">\n\t\t\t\t\t\t\t\t\t{isZh ? '持久性' : 'Persistence'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-2\">\n\t\t\t\t\t\t\t\t\t{isZh ? '持久化到 agent.history' : 'Persisted in agent.history'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-2\">\n\t\t\t\t\t\t\t\t\t{isZh ? '瞬态' : 'Transient'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-2\">\n\t\t\t\t\t\t\t\t\t{isZh ? '传给 LLM' : 'Sent to LLM'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-2\">\n\t\t\t\t\t\t\t\t\t{isZh ? '是' : 'Yes'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-2\">\n\t\t\t\t\t\t\t\t\t{isZh ? '否' : 'No'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-2\">\n\t\t\t\t\t\t\t\t\t{isZh ? '用途' : 'Purpose'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-2\">\n\t\t\t\t\t\t\t\t\t{isZh ? '构成 Agent 记忆，显示历史步骤' : 'Forms agent memory, displays history'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-2\">\n\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t? '实时 UI 反馈（如 loading 状态）'\n\t\t\t\t\t\t\t\t\t\t: 'Real-time UI feedback (e.g., loading state)'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</tbody>\n\t\t\t\t\t</table>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t{/* All Events */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"all-events\">{isZh ? '所有事件' : 'All Events'}</Heading>\n\t\t\t\t<APIReference\n\t\t\t\t\tproperties={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'statuschange',\n\t\t\t\t\t\t\ttype: 'Event',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? 'Agent 状态变化 (idle → running → completed/error)'\n\t\t\t\t\t\t\t\t: 'Agent status changes (idle → running → completed/error)',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'historychange',\n\t\t\t\t\t\t\ttype: 'Event',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '历史事件更新，读取 agent.history 获取完整历史'\n\t\t\t\t\t\t\t\t: 'History updated, read agent.history for full history',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'activity',\n\t\t\t\t\t\t\ttype: 'CustomEvent<AgentActivity>',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '实时活动反馈：thinking, executing, executed, retrying, error'\n\t\t\t\t\t\t\t\t: 'Real-time activity: thinking, executing, executed, retrying, error',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'dispose',\n\t\t\t\t\t\t\ttype: 'Event',\n\t\t\t\t\t\t\tdescription: isZh ? 'Agent 被销毁' : 'Agent is disposed',\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t{/* HistoricalEvent Types */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"historicalevent\">HistoricalEvent</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh ? 'agent.history 数组中的事件类型：' : 'Event types in agent.history array:'}\n\t\t\t\t</p>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`type HistoricalEvent =\n  | { type: 'step'; stepIndex: number; reflection: AgentReflection; action: Action }\n  | { type: 'observation'; content: string }\n  | { type: 'user_takeover' }\n  | { type: 'retry'; message: string; attempt: number; maxAttempts: number }\n  | { type: 'error'; message: string }`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t{/* AgentActivity Types */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"agentactivity\">AgentActivity</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh ? 'activity 事件的 detail 类型：' : 'The detail type of activity events:'}\n\t\t\t\t</p>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`type AgentActivity =\n  | { type: 'thinking' }\n  | { type: 'executing'; tool: string; input: unknown }\n  | { type: 'executed'; tool: string; input: unknown; output: string; duration: number }\n  | { type: 'retrying'; attempt: number; maxAttempts: number }\n  | { type: 'error'; message: string }`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t<APIDivider title={isZh ? 'React 示例' : 'React Example'} />\n\n\t\t\t{/* React Hooks Example */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"using-react-hooks\">{isZh ? '使用 React Hooks' : 'Using React Hooks'}</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh ? '监听事件并更新 React 状态：' : 'Listen to events and update React state:'}\n\t\t\t\t</p>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"tsx\"\n\t\t\t\t\tcode={`function useAgent(agent: PageAgentCore) {\n  const [status, setStatus] = useState(agent.status)\n  const [history, setHistory] = useState(agent.history)\n  const [activity, setActivity] = useState<AgentActivity | null>(null)\n\n  useEffect(() => {\n    const onStatus = () => setStatus(agent.status)\n    const onHistory = () => setHistory([...agent.history])\n    const onActivity = (e: Event) => setActivity((e as CustomEvent).detail)\n\n    agent.addEventListener('statuschange', onStatus)\n    agent.addEventListener('historychange', onHistory)\n    agent.addEventListener('activity', onActivity)\n\n    return () => {\n      agent.removeEventListener('statuschange', onStatus)\n      agent.removeEventListener('historychange', onHistory)\n      agent.removeEventListener('activity', onActivity)\n    }\n  }, [agent])\n\n  return { status, history, activity }\n}`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t<APIDivider title={isZh ? '完整组装示例' : 'Complete Assembly Example'} />\n\n\t\t\t{/* Assembly Example */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"assembling-core-controller-custom-ui\">\n\t\t\t\t\t{isZh ? '组装 Core + Controller + 自定义 UI' : 'Assembling Core + Controller + Custom UI'}\n\t\t\t\t</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '参考内置 PageAgent 的实现方式，用自定义 UI 替换 Panel：'\n\t\t\t\t\t\t: 'Following the built-in PageAgent pattern, replace Panel with custom UI:'}\n\t\t\t\t</p>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`import { PageAgentCore } from '@page-agent/core'\nimport { PageController } from '@page-agent/page-controller'\n\n// 1. Create PageController\nconst pageController = new PageController({ enableMask: true })\n\n// 2. Create PageAgentCore with controller\nconst agent = new PageAgentCore({\n  pageController,\n  baseURL: 'https://api.openai.com/v1',\n  apiKey: 'your-api-key',\n  model: 'gpt-5.2',\n})\n\n// 3. Mount your custom UI\nconst root = createRoot(document.getElementById('my-ui')!)\nroot.render(<MyAgentUI agent={agent} />)\n\n// 4. Handle user input (optional)\nagent.onAskUser = async (question) => window.prompt(question) || ''\n\n// 5. Execute task\nawait agent.execute('Fill the form with test data')\n\n// 6. Cleanup\nagent.dispose()`}\n\t\t\t\t/>\n\t\t\t</section>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/advanced/page-agent/page.tsx",
    "content": "import { Link } from 'wouter'\n\nimport CodeEditor from '@/components/CodeEditor'\nimport { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\nexport default function PageAgentDocs() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<div>\n\t\t\t<h1 className=\"text-4xl font-bold mb-6\">PageAgent</h1>\n\n\t\t\t<p className=\"text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed\">\n\t\t\t\t{isZh\n\t\t\t\t\t? 'PageAgent 是带有内置 UI 面板的完整 Agent 类。它继承自 PageAgentCore，并自动创建交互面板和 PageController。'\n\t\t\t\t\t: 'PageAgent is the complete Agent class with built-in UI panel. It extends PageAgentCore and automatically creates an interactive panel and PageController.'}\n\t\t\t</p>\n\n\t\t\t{/* When to use */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"when-to-use-pageagent\">\n\t\t\t\t\t{isZh ? '何时使用 PageAgent' : 'When to Use PageAgent'}\n\t\t\t\t</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '在大多数场景下，你应该使用 PageAgent。它提供了开箱即用的完整体验：'\n\t\t\t\t\t\t: 'In most cases, you should use PageAgent. It provides a complete out-of-the-box experience:'}\n\t\t\t\t</p>\n\t\t\t\t<ul className=\"list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-6\">\n\t\t\t\t\t<li>\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '自动创建 PageController，处理 DOM 提取和元素操作'\n\t\t\t\t\t\t\t: 'Automatically creates PageController for DOM extraction and element actions'}\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '内置 UI 面板，显示任务进度、Agent 思考过程和操作结果'\n\t\t\t\t\t\t\t: 'Built-in UI panel showing task progress, agent thinking, and action results'}\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '支持 ask_user 工具，Agent 可以向用户提问'\n\t\t\t\t\t\t\t: 'Supports ask_user tool for agent to ask questions to users'}\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\n\t\t\t{/* Basic Usage */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"basic-usage\">{isZh ? '基本用法' : 'Basic Usage'}</Heading>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`import { PageAgent } from 'page-agent'\n\nconst agent = new PageAgent({\n  // LLM Configuration (required)\n  baseURL: 'https://api.openai.com/v1',\n  apiKey: 'your-api-key',\n  model: 'gpt-5.2',\n  \n  // Optional settings\n  language: 'en-US',\n})\n\n// Execute a task\nconst result = await agent.execute('Click the login button')\n\nconsole.log(result.success) // true or false\nconsole.log(result.data)    // Task result description\nconsole.log(result.history) // Full execution history`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t{/* Class Definition */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"class-definition\">{isZh ? '类定义' : 'Class Definition'}</Heading>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`class PageAgent extends PageAgentCore {\n  panel: Panel\n  pageController: PageController\n  constructor(config: PageAgentConfig)\n}`}\n\t\t\t\t/>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mt-4\">\n\t\t\t\t\t{isZh ? (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\tPageAgent 继承自{' '}\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\thref=\"/advanced/page-agent-core\"\n\t\t\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 hover:underline\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tPageAgentCore\n\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t，所有核心方法和事件都可用。配置项合并了{' '}\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\thref=\"/advanced/page-agent-core#configuration\"\n\t\t\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 hover:underline\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tAgentConfig\n\t\t\t\t\t\t\t</Link>{' '}\n\t\t\t\t\t\t\t和{' '}\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\thref=\"/advanced/page-controller#configuration\"\n\t\t\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 hover:underline\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tPageControllerConfig\n\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t。\n\t\t\t\t\t\t</>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\tPageAgent extends{' '}\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\thref=\"/advanced/page-agent-core\"\n\t\t\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 hover:underline\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tPageAgentCore\n\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t. All core methods and events are available. Config merges{' '}\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\thref=\"/advanced/page-agent-core#configuration\"\n\t\t\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 hover:underline\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tAgentConfig\n\t\t\t\t\t\t\t</Link>{' '}\n\t\t\t\t\t\t\tand{' '}\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\thref=\"/advanced/page-controller#configuration\"\n\t\t\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 hover:underline\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tPageControllerConfig\n\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t.\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\t\t\t\t</p>\n\t\t\t</section>\n\n\t\t\t{/* Panel */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"panel\">{isZh ? 'UI 面板' : 'UI Panel'}</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? 'PageAgent 自动创建一个 Panel 实例。你可以通过 panel 属性控制 UI：'\n\t\t\t\t\t\t: 'PageAgent automatically creates a Panel instance. You can control the UI via the panel property:'}\n\t\t\t\t</p>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`// Show/hide the panel\nagent.panel.show()\nagent.panel.hide()\n\n// Expand/collapse history view\nagent.panel.expand()\nagent.panel.collapse()\n\n// Reset panel state\nagent.panel.reset()\n\n// Dispose panel (called automatically when agent disposes)\nagent.panel.dispose()`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t{/* Comparison with PageAgentCore */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"pageagent-vs-pageagentcore\">\n\t\t\t\t\t{isZh ? 'PageAgent vs PageAgentCore' : 'PageAgent vs PageAgentCore'}\n\t\t\t\t</Heading>\n\t\t\t\t<div className=\"overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700\">\n\t\t\t\t\t<table className=\"w-full text-sm\">\n\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t<tr className=\"bg-gray-50 dark:bg-gray-800/50\">\n\t\t\t\t\t\t\t\t<th className=\"px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300\"></th>\n\t\t\t\t\t\t\t\t<th className=\"px-4 py-3 text-center font-medium text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t\t\t\tPageAgent\n\t\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t\t<th className=\"px-4 py-3 text-center font-medium text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t\t\t\tPageAgentCore\n\t\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t<tbody className=\"divide-y divide-gray-100 dark:divide-gray-800\">\n\t\t\t\t\t\t\t<tr className=\"bg-white dark:bg-gray-900\">\n\t\t\t\t\t\t\t\t<td className=\"px-4 py-3 text-gray-600 dark:text-gray-400\">\n\t\t\t\t\t\t\t\t\t{isZh ? 'UI 面板' : 'UI Panel'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"px-4 py-3 text-center text-green-600 dark:text-green-400\">✓</td>\n\t\t\t\t\t\t\t\t<td className=\"px-4 py-3 text-center text-gray-400 dark:text-gray-600\">-</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t<tr className=\"bg-white dark:bg-gray-900\">\n\t\t\t\t\t\t\t\t<td className=\"px-4 py-3 text-gray-600 dark:text-gray-400\">\n\t\t\t\t\t\t\t\t\t{isZh ? '自动创建 PageController' : 'Auto-creates PageController'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"px-4 py-3 text-center text-green-600 dark:text-green-400\">✓</td>\n\t\t\t\t\t\t\t\t<td className=\"px-4 py-3 text-center text-gray-400 dark:text-gray-600\">-</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t<tr className=\"bg-white dark:bg-gray-900\">\n\t\t\t\t\t\t\t\t<td className=\"px-4 py-3 text-gray-600 dark:text-gray-400\">\n\t\t\t\t\t\t\t\t\t{isZh ? 'Headless 模式' : 'Headless Mode'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"px-4 py-3 text-center text-gray-400 dark:text-gray-600\">-</td>\n\t\t\t\t\t\t\t\t<td className=\"px-4 py-3 text-center text-green-600 dark:text-green-400\">✓</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t<tr className=\"bg-white dark:bg-gray-900\">\n\t\t\t\t\t\t\t\t<td className=\"px-4 py-3 text-gray-600 dark:text-gray-400\">\n\t\t\t\t\t\t\t\t\t{isZh ? '适用场景' : 'Use Case'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"px-4 py-3 text-center text-gray-600 dark:text-gray-400\">\n\t\t\t\t\t\t\t\t\t{isZh ? '网页集成' : 'Web integration'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"px-4 py-3 text-center text-gray-600 dark:text-gray-400\">\n\t\t\t\t\t\t\t\t\t{isZh ? '自定义 UI / 无头' : 'Custom UI / Headless'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</tbody>\n\t\t\t\t\t</table>\n\t\t\t\t</div>\n\t\t\t</section>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/advanced/page-agent-core/page.tsx",
    "content": "import { Link } from 'wouter'\n\nimport { APIDivider, APIReference, TypeRef } from '@/components/APIReference'\nimport CodeEditor from '@/components/CodeEditor'\nimport { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\nexport default function PageAgentCoreDocs() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<div>\n\t\t\t<h1 className=\"text-4xl font-bold mb-6\">PageAgentCore</h1>\n\n\t\t\t<p className=\"text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed\">\n\t\t\t\t{isZh\n\t\t\t\t\t? 'PageAgentCore 是不带 UI 的核心 Agent 类。用于需要自定义 UI 或无头运行的场景。'\n\t\t\t\t\t: 'PageAgentCore is the core Agent class without UI. Use it for custom UI or headless scenarios.'}\n\t\t\t</p>\n\n\t\t\t{/* When to use */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"when-to-use-pageagentcore\">\n\t\t\t\t\t{isZh ? '何时使用 PageAgentCore' : 'When to Use PageAgentCore'}\n\t\t\t\t</Heading>\n\t\t\t\t<ul className=\"list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2\">\n\t\t\t\t\t<li>{isZh ? '需要自定义 UI 界面' : 'Need a custom UI interface'}</li>\n\t\t\t\t\t<li>{isZh ? '在自动化测试中无头运行' : 'Running headless in automated tests'}</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '在非浏览器环境运行（需自定义 PageController）'\n\t\t\t\t\t\t\t: 'Running in non-browser environments (requires custom PageController)'}\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '将 PageAgent 嵌入其他 Agent 系统'\n\t\t\t\t\t\t\t: 'Embedding PageAgent in other agent systems'}\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\n\t\t\t{/* Basic Usage */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"basic-usage\">{isZh ? '基本用法' : 'Basic Usage'}</Heading>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`import { PageAgentCore } from '@page-agent/core'\nimport { PageController } from '@page-agent/page-controller'\n\nconst agent = new PageAgentCore({\n  pageController: new PageController({ enableMask: true }),\n  baseURL: 'https://api.openai.com/v1',\n  apiKey: 'your-api-key',\n  model: 'gpt-5.2',\n})\n\n// Listen to events for UI display\nagent.addEventListener('statuschange', () => {\n  console.log('Status:', agent.status)\n})\n\nagent.addEventListener('activity', (e) => {\n  const activity = (e as CustomEvent).detail\n  console.log('Activity:', activity.type)\n})\n\n// Execute task\nconst result = await agent.execute('Fill in the form with test data')`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t<APIDivider title={isZh ? '配置' : 'Configuration'} />\n\n\t\t\t{/* Configuration */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"configuration\">PageAgentCoreConfig</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? 'PageAgentCoreConfig = AgentConfig & { pageController: PageController }。AgentConfig 包含以下配置项：'\n\t\t\t\t\t\t: 'PageAgentCoreConfig = AgentConfig & { pageController: PageController }. AgentConfig contains the following options:'}\n\t\t\t\t</p>\n\n\t\t\t\t{/* PageController */}\n\t\t\t\t<h3 className=\"text-lg font-semibold mt-6 mb-3 text-gray-800 dark:text-gray-200\">\n\t\t\t\t\tPageController\n\t\t\t\t</h3>\n\t\t\t\t<APIReference\n\t\t\t\t\tproperties={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'pageController',\n\t\t\t\t\t\t\ttype: 'PageController',\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tdescription: isZh ? (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\thref=\"/advanced/page-controller\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 hover:underline\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tPageController\n\t\t\t\t\t\t\t\t\t</Link>{' '}\n\t\t\t\t\t\t\t\t\t实例，用于 DOM 操作和元素交互。\n\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\thref=\"/advanced/page-controller\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 hover:underline\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tPageController\n\t\t\t\t\t\t\t\t\t</Link>{' '}\n\t\t\t\t\t\t\t\t\tinstance for DOM operations and element interaction.\n\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\n\t\t\t\t{/* LLM Config */}\n\t\t\t\t<h3 className=\"text-lg font-semibold mt-6 mb-3 text-gray-800 dark:text-gray-200\">\n\t\t\t\t\t{isZh ? 'LLM 配置' : 'LLM Config'}\n\t\t\t\t</h3>\n\t\t\t\t<APIReference\n\t\t\t\t\tproperties={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'baseURL',\n\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? 'LLM API 的基础 URL（如 https://api.openai.com/v1）'\n\t\t\t\t\t\t\t\t: 'Base URL of the LLM API (e.g., https://api.openai.com/v1)',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'model',\n\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '模型名称（如 gpt-5.2, anthropic/claude-4.5-haiku）'\n\t\t\t\t\t\t\t\t: 'Model name (e.g., gpt-5.2, anthropic/claude-4.5-haiku)',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'apiKey',\n\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tdescription: 'LLM AK',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'temperature',\n\t\t\t\t\t\t\ttype: 'number',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '模型温度参数，控制输出随机性'\n\t\t\t\t\t\t\t\t: 'Model temperature, controls output randomness',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'maxRetries',\n\t\t\t\t\t\t\ttype: 'number',\n\t\t\t\t\t\t\tdefaultValue: '3',\n\t\t\t\t\t\t\tdescription: isZh ? 'API 调用失败时的最大重试次数' : 'Maximum retries on API failure',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'disableNamedToolChoice',\n\t\t\t\t\t\t\ttype: 'boolean',\n\t\t\t\t\t\t\tdefaultValue: 'false',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '禁用命名 tool_choice，始终使用 \"required\" 字符串。适用于不支持 tool_choice 对象格式的 LLM 服务。'\n\t\t\t\t\t\t\t\t: 'Disable named tool_choice, always use \"required\" string. For LLM services that don\\'t support the object format of tool_choice.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'customFetch',\n\t\t\t\t\t\t\ttype: 'typeof fetch',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '自定义 fetch 函数，用于定制 headers、credentials、代理等'\n\t\t\t\t\t\t\t\t: 'Custom fetch function for customizing headers, credentials, proxy, etc.',\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\n\t\t\t\t{/* Agent Config */}\n\t\t\t\t<h3 className=\"text-lg font-semibold mt-6 mb-3 text-gray-800 dark:text-gray-200\">\n\t\t\t\t\t{isZh ? 'Agent 配置' : 'Agent Config'}\n\t\t\t\t</h3>\n\t\t\t\t<APIReference\n\t\t\t\t\tproperties={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'language',\n\t\t\t\t\t\t\ttype: \"'en-US' | 'zh-CN'\",\n\t\t\t\t\t\t\tdefaultValue: \"'en-US'\",\n\t\t\t\t\t\t\tdescription: isZh ? 'Agent 输出语言' : 'Agent output language',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'maxSteps',\n\t\t\t\t\t\t\ttype: 'number',\n\t\t\t\t\t\t\tdefaultValue: '40',\n\t\t\t\t\t\t\tdescription: isZh ? '每个任务的最大步骤数' : 'Maximum number of steps per task',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'customTools',\n\t\t\t\t\t\t\ttype: 'Record<string, PageAgentTool | null>',\n\t\t\t\t\t\t\tstatus: 'experimental',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '自定义工具，可扩展或覆盖内置工具。设为 null 可移除工具。'\n\t\t\t\t\t\t\t\t: 'Custom tools to extend or override built-in tools. Set to null to remove a tool.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'instructions',\n\t\t\t\t\t\t\ttype: 'InstructionsConfig',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '指导 Agent 行为的指令配置，见下方类型定义'\n\t\t\t\t\t\t\t\t: 'Instructions to guide agent behavior, see type definition below',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'transformPageContent',\n\t\t\t\t\t\t\ttype: '(content: string) => string | Promise<string>',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '发送给 LLM 前转换页面内容，可用于数据脱敏'\n\t\t\t\t\t\t\t\t: 'Transform page content before sending to LLM, useful for data masking',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'customSystemPrompt',\n\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\tstatus: 'experimental',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '完全覆盖默认系统提示词。谨慎使用。'\n\t\t\t\t\t\t\t\t: 'Completely override the default system prompt. Use with caution.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'experimentalScript\\nExecutionTool',\n\t\t\t\t\t\t\ttype: 'boolean',\n\t\t\t\t\t\t\tdefaultValue: 'false',\n\t\t\t\t\t\t\tstatus: 'experimental',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '启用实验性 JavaScript 执行工具'\n\t\t\t\t\t\t\t\t: 'Enable experimental JavaScript execution tool',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'experimentalLlmsTxt',\n\t\t\t\t\t\t\ttype: 'boolean',\n\t\t\t\t\t\t\tdefaultValue: 'false',\n\t\t\t\t\t\t\tstatus: 'experimental',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '从当前站点根目录获取 /llms.txt 并作为上下文提供给 LLM'\n\t\t\t\t\t\t\t\t: 'Fetch /llms.txt from site origin and include as LLM context',\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\n\t\t\t\t{/* Lifecycle Hooks */}\n\t\t\t\t<h3 className=\"text-lg font-semibold mt-6 mb-3 text-gray-800 dark:text-gray-200\">\n\t\t\t\t\t{isZh ? '生命周期钩子' : 'Lifecycle Hooks'}\n\t\t\t\t\t<span className=\"ml-2 text-xs font-normal text-amber-600 dark:text-amber-400\">\n\t\t\t\t\t\texperimental\n\t\t\t\t\t</span>\n\t\t\t\t</h3>\n\t\t\t\t<div className=\"bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 mb-4\">\n\t\t\t\t\t<p className=\"text-amber-800 dark:text-amber-200 text-sm\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '这些接口高度实验性，可能在未来版本中发生变化。'\n\t\t\t\t\t\t\t: 'These APIs are highly experimental and may change in future versions. '}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<APIReference\n\t\t\t\t\tproperties={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'onBeforeStep',\n\t\t\t\t\t\t\ttype: '(agent, stepCount) => void | Promise<void>',\n\t\t\t\t\t\t\tdescription: isZh ? '每个步骤执行前调用' : 'Called before each step execution',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'onAfterStep',\n\t\t\t\t\t\t\ttype: '(agent, history) => void | Promise<void>',\n\t\t\t\t\t\t\tdescription: isZh ? '每个步骤执行后调用' : 'Called after each step execution',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'onBeforeTask',\n\t\t\t\t\t\t\ttype: '(agent) => void | Promise<void>',\n\t\t\t\t\t\t\tdescription: isZh ? '任务开始前调用' : 'Called before task starts',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'onAfterTask',\n\t\t\t\t\t\t\ttype: '(agent, result) => void | Promise<void>',\n\t\t\t\t\t\t\tdescription: isZh ? '任务结束后调用' : 'Called after task ends',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'onDispose',\n\t\t\t\t\t\t\ttype: '(agent, reason?) => void',\n\t\t\t\t\t\t\tdescription: isZh ? 'Agent 销毁时调用' : 'Called when agent is disposed',\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t<APIDivider title={isZh ? '属性与方法' : 'Properties & Methods'} />\n\n\t\t\t{/* Properties */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"properties\">{isZh ? '属性' : 'Properties'}</Heading>\n\t\t\t\t<APIReference\n\t\t\t\t\tproperties={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'status',\n\t\t\t\t\t\t\ttype: \"'idle' | 'running' | 'completed' | 'error'\",\n\t\t\t\t\t\t\tdescription: isZh ? '当前 Agent 执行状态' : 'Current agent execution status',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'history',\n\t\t\t\t\t\t\ttype: 'HistoricalEvent[]',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '历史事件数组，构成 Agent 的记忆'\n\t\t\t\t\t\t\t\t: 'Array of historical events, forms agent memory',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'task',\n\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\tdescription: isZh ? '当前正在执行的任务' : 'Current task being executed',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'pageController',\n\t\t\t\t\t\t\ttype: 'PageController',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? 'PageController 实例，用于 DOM 操作'\n\t\t\t\t\t\t\t\t: 'PageController instance for DOM operations',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'tools',\n\t\t\t\t\t\t\ttype: 'Map<string, PageAgentTool>',\n\t\t\t\t\t\t\tdescription: isZh ? '可用工具的 Map' : 'Map of available tools',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'onAskUser',\n\t\t\t\t\t\t\ttype: '(question: string) => Promise<string>',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? 'Agent 需要用户输入时的回调。未设置则禁用 ask_user 工具。'\n\t\t\t\t\t\t\t\t: 'Callback when agent needs user input. If not set, ask_user tool is disabled.',\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t{/* Methods */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"methods\">{isZh ? '方法' : 'Methods'}</Heading>\n\t\t\t\t<APIReference\n\t\t\t\t\tvariant=\"methods\"\n\t\t\t\t\tproperties={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'execute(task)',\n\t\t\t\t\t\t\ttype: 'Promise<ExecutionResult>',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '执行任务并返回结果。包含 success、data 和 history 字段。'\n\t\t\t\t\t\t\t\t: 'Execute a task and return result. Contains success, data, and history fields.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'stop()',\n\t\t\t\t\t\t\ttype: 'void',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '停止当前任务。Agent 仍可复用。'\n\t\t\t\t\t\t\t\t: 'Stop the current task. Agent remains reusable.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'dispose()',\n\t\t\t\t\t\t\ttype: 'void',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '销毁 Agent 并清理资源'\n\t\t\t\t\t\t\t\t: 'Dispose the agent and clean up resources',\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t{/* Events */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"events\">{isZh ? '事件' : 'Events'}</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh ? (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\tPageAgentCore 继承自 <TypeRef>EventTarget</TypeRef>，提供以下事件：\n\t\t\t\t\t\t</>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\tPageAgentCore extends <TypeRef>EventTarget</TypeRef> and provides the following\n\t\t\t\t\t\t\tevents:\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\t\t\t\t</p>\n\t\t\t\t<APIReference\n\t\t\t\t\tproperties={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'statuschange',\n\t\t\t\t\t\t\ttype: 'Event',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? 'Agent 状态变化时触发 (idle → running → completed/error)'\n\t\t\t\t\t\t\t\t: 'Fired when agent status changes (idle → running → completed/error)',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'historychange',\n\t\t\t\t\t\t\ttype: 'Event',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '历史事件更新时触发（持久化事件，构成 Agent 记忆）'\n\t\t\t\t\t\t\t\t: 'Fired when history events are updated (persistent, part of agent memory)',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'activity',\n\t\t\t\t\t\t\ttype: 'CustomEvent<AgentActivity>',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '实时活动反馈（短暂状态，仅用于 UI）。类型包括：thinking, executing, executed, retrying, error'\n\t\t\t\t\t\t\t\t: 'Real-time activity feedback (transient, UI only). Types: thinking, executing, executed, retrying, error',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'dispose',\n\t\t\t\t\t\t\ttype: 'Event',\n\t\t\t\t\t\t\tdescription: isZh ? 'Agent 被销毁时触发' : 'Fired when agent is disposed',\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t<APIDivider title={isZh ? '类型定义' : 'Type Definitions'} />\n\n\t\t\t{/* ExecutionResult */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"executionresult\">ExecutionResult</Heading>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`interface ExecutionResult {\n  success: boolean\n  data: string\n  history: HistoricalEvent[]\n}`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t{/* AgentActivity */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"agentactivity\">AgentActivity</Heading>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`type AgentActivity =\n  | { type: 'thinking' }\n  | { type: 'executing'; tool: string; input: unknown }\n  | { type: 'executed'; tool: string; input: unknown; output: string; duration: number }\n  | { type: 'retrying'; attempt: number; maxAttempts: number }\n  | { type: 'error'; message: string }`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t{/* InstructionsConfig */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"instructionsconfig\">InstructionsConfig</Heading>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`interface InstructionsConfig {\n  /** Global system-level instructions, applied to all tasks */\n  system?: string\n\n  /**\n   * Dynamic page-level instructions callback.\n   * Called before each step to get instructions for the current page.\n   */\n  getPageInstructions?: (url: string) => string | undefined\n}`}\n\t\t\t\t/>\n\t\t\t</section>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/advanced/page-controller/page.tsx",
    "content": "import { Link } from 'wouter'\n\nimport { APIDivider, APIReference } from '@/components/APIReference'\nimport CodeEditor from '@/components/CodeEditor'\nimport { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\nexport default function PageControllerDocs() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<div>\n\t\t\t<h1 className=\"text-4xl font-bold mb-6\">PageController</h1>\n\n\t\t\t<p className=\"text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed\">\n\t\t\t\t{isZh\n\t\t\t\t\t? 'PageController 负责 DOM 提取和元素交互，独立于 LLM。它将页面状态结构化为 LLM 可消费的格式，并执行元素级操作。'\n\t\t\t\t\t: 'PageController handles DOM extraction and element interaction, independent of LLM. It structures page state into LLM-consumable format and executes element-level actions.'}\n\t\t\t</p>\n\n\t\t\t{/* Basic Usage */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"basic-usage\">{isZh ? '基本用法' : 'Basic Usage'}</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? 'PageAgent 接受 PageController 配置项：'\n\t\t\t\t\t\t: 'PageAgent accepts PageController options:'}\n\t\t\t\t</p>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`import { PageAgent } from 'page-agent'\n\nconst agent = new PageAgent({\n  baseURL: 'https://api.openai.com/v1',\n  apiKey: 'your-api-key',\n  model: 'gpt-5.2',\n\n  // PageController options\n  enableMask: true,\n  viewportExpansion: 0,\n})`}\n\t\t\t\t/>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mt-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? 'PageAgentCore 接受 PageController 实例：'\n\t\t\t\t\t\t: 'PageAgentCore accepts a PageController instance:'}\n\t\t\t\t</p>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`import { PageAgentCore } from '@page-agent/core'\nimport { PageController } from '@page-agent/page-controller'\n\nconst pageController = new PageController({\n  enableMask: true,\n  viewportExpansion: -1,  // extract full page\n})\n\nconst agent = new PageAgentCore({\n  pageController,\n  baseURL: 'https://api.openai.com/v1',\n  apiKey: 'your-api-key',\n  model: 'gpt-5.2',\n})`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t<APIDivider title={isZh ? '配置' : 'Configuration'} />\n\n\t\t\t{/* Configuration */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"configuration\">PageControllerConfig</Heading>\n\t\t\t\t<APIReference\n\t\t\t\t\tproperties={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'enableMask',\n\t\t\t\t\t\t\ttype: 'boolean',\n\t\t\t\t\t\t\tdefaultValue: 'false',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '启用视觉遮罩覆盖层，在自动化期间阻止用户操作页面。通过 PageAgent 创建时默认为 true。'\n\t\t\t\t\t\t\t\t: 'Enable visual mask overlay that blocks user interaction during automation. Defaults to true when created via PageAgent.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'viewportExpansion',\n\t\t\t\t\t\t\ttype: 'number',\n\t\t\t\t\t\t\tdefaultValue: '0',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '向视口外扩展提取的像素数。设为 -1 表示提取整个页面。'\n\t\t\t\t\t\t\t\t: 'Pixels to expand extraction beyond viewport. Set to -1 to extract the entire page.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'interactiveBlacklist',\n\t\t\t\t\t\t\ttype: '(Element | (() => Element))[]',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '要排除的交互元素列表。支持元素引用或返回元素的函数（延迟求值）。'\n\t\t\t\t\t\t\t\t: 'Elements to exclude from interaction. Supports element references or functions returning elements (lazy evaluation).',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'interactiveWhitelist',\n\t\t\t\t\t\t\ttype: '(Element | (() => Element))[]',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '要强制包含的交互元素列表。支持元素引用或返回元素的函数。'\n\t\t\t\t\t\t\t\t: 'Elements to force include for interaction. Supports element references or functions returning elements.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'includeAttributes',\n\t\t\t\t\t\t\ttype: 'string[]',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '在 DOM 提取中包含的额外 HTML 属性。支持通配符 *（如 data-* 匹配所有 data- 开头的属性）。默认已包含常见属性如 role, aria-label 等。'\n\t\t\t\t\t\t\t\t: 'Additional HTML attributes to include in DOM extraction. Supports wildcard * (e.g. data-* matches all data- prefixed attributes). Common attributes like role, aria-label are included by default.',\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t<APIDivider title={isZh ? '方法' : 'Methods'} />\n\n\t\t\t{/* Methods */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"methods\">{isZh ? '方法' : 'Methods'}</Heading>\n\n\t\t\t\t<h3 className=\"text-lg font-semibold mt-6 mb-3\">{isZh ? '状态查询' : 'State Queries'}</h3>\n\t\t\t\t<APIReference\n\t\t\t\t\tvariant=\"methods\"\n\t\t\t\t\tproperties={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'getBrowserState()',\n\t\t\t\t\t\t\ttype: 'Promise<BrowserState>',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '获取结构化的浏览器状态（URL、标题、简化 HTML 等），自动调用 updateTree() 刷新 DOM。这是 Agent 在每步使用的主要方法。'\n\t\t\t\t\t\t\t\t: 'Get structured browser state (URL, title, simplified HTML, etc.), automatically calls updateTree() to refresh DOM. This is the primary method the agent uses each step.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'updateTree()',\n\t\t\t\t\t\t\ttype: 'Promise<string>',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '刷新 DOM 树并返回简化 HTML。通常不需要手动调用 —— getBrowserState() 会自动调用。'\n\t\t\t\t\t\t\t\t: 'Refresh DOM tree and return simplified HTML. Usually not needed manually — getBrowserState() calls it automatically.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'getCurrentUrl()',\n\t\t\t\t\t\t\ttype: 'Promise<string>',\n\t\t\t\t\t\t\tdescription: isZh ? '获取当前页面 URL。' : 'Get current page URL.',\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\n\t\t\t\t<h3 className=\"text-lg font-semibold mt-6 mb-3\">{isZh ? '元素操作' : 'Element Actions'}</h3>\n\t\t\t\t<APIReference\n\t\t\t\t\tvariant=\"methods\"\n\t\t\t\t\tproperties={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'clickElement(index)',\n\t\t\t\t\t\t\ttype: 'Promise<ActionResult>',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '按索引点击元素。索引来自简化 HTML 中的 [N] 标记。'\n\t\t\t\t\t\t\t\t: 'Click element by index. Index comes from [N] markers in simplified HTML.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'inputText(index, text)',\n\t\t\t\t\t\t\ttype: 'Promise<ActionResult>',\n\t\t\t\t\t\t\tdescription: isZh ? '向输入框元素填入文本。' : 'Input text into a form element.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'selectOption(index, optionText)',\n\t\t\t\t\t\t\ttype: 'Promise<ActionResult>',\n\t\t\t\t\t\t\tdescription: isZh ? '在下拉框中选择选项。' : 'Select option in a dropdown element.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'scroll(options)',\n\t\t\t\t\t\t\ttype: 'Promise<ActionResult>',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '垂直滚动页面或指定元素。'\n\t\t\t\t\t\t\t\t: 'Scroll page or specific element vertically.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'scrollHorizontally(options)',\n\t\t\t\t\t\t\ttype: 'Promise<ActionResult>',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '水平滚动页面或指定元素。'\n\t\t\t\t\t\t\t\t: 'Scroll page or specific element horizontally.',\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\n\t\t\t\t<h3 className=\"text-lg font-semibold mt-6 mb-3\">{isZh ? '遮罩控制' : 'Mask Control'}</h3>\n\t\t\t\t<APIReference\n\t\t\t\t\tvariant=\"methods\"\n\t\t\t\t\tproperties={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'showMask()',\n\t\t\t\t\t\t\ttype: 'Promise<void>',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '显示视觉遮罩。需要 enableMask: true。'\n\t\t\t\t\t\t\t\t: 'Show visual mask overlay. Requires enableMask: true.',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'hideMask()',\n\t\t\t\t\t\t\ttype: 'Promise<void>',\n\t\t\t\t\t\t\tdescription: isZh ? '隐藏视觉遮罩。' : 'Hide visual mask overlay.',\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\n\t\t\t\t<h3 className=\"text-lg font-semibold mt-6 mb-3\">{isZh ? '生命周期' : 'Lifecycle'}</h3>\n\t\t\t\t<APIReference\n\t\t\t\t\tvariant=\"methods\"\n\t\t\t\t\tproperties={[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: 'dispose()',\n\t\t\t\t\t\t\ttype: 'void',\n\t\t\t\t\t\t\tdescription: isZh\n\t\t\t\t\t\t\t\t? '清理所有资源（DOM 高亮、遮罩等）。Agent 销毁时自动调用。'\n\t\t\t\t\t\t\t\t: 'Clean up all resources (DOM highlights, mask, etc.). Called automatically when agent disposes.',\n\t\t\t\t\t\t},\n\t\t\t\t\t]}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t<APIDivider title={isZh ? '类型定义' : 'Type Definitions'} />\n\n\t\t\t{/* BrowserState */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"browser-state\">BrowserState</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? 'getBrowserState() 返回的结构化浏览器状态，直接用于构建 LLM prompt。'\n\t\t\t\t\t\t: 'Structured browser state returned by getBrowserState(), used directly to build LLM prompts.'}\n\t\t\t\t</p>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`interface BrowserState {\n  url: string\n  title: string\n  header: string   // page info + scroll position\n  content: string  // simplified HTML of interactive elements\n  footer: string   // scroll hint\n}`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t{/* ActionResult */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"action-result\">ActionResult</Heading>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`interface ActionResult {\n  success: boolean\n  message: string\n}`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t{/* Custom PageController */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"custom-implementation\">\n\t\t\t\t\t{isZh ? '自定义实现' : 'Custom Implementation'}\n\t\t\t\t</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '在非浏览器环境（如 Puppeteer、Playwright），你可以实现自定义 PageController。需要实现 Agent 使用的核心方法：'\n\t\t\t\t\t\t: 'In non-browser environments (e.g. Puppeteer, Playwright), you can implement a custom PageController. Implement the core methods used by the agent:'}\n\t\t\t\t</p>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\tcode={`import { PageAgentCore } from '@page-agent/core'\nimport type { PageController } from '@page-agent/page-controller'\n\nclass PuppeteerPageController implements PageController {\n  async getBrowserState() { /* ... */ }\n  async clickElement(index: number) { /* ... */ }\n  async inputText(index: number, text: string) { /* ... */ }\n  async scroll(options: { down: boolean; numPages: number }) { /* ... */ }\n  // ... other methods\n}\n\nconst agent = new PageAgentCore({\n  pageController: new PuppeteerPageController(),\n  baseURL: 'https://api.openai.com/v1',\n  apiKey: 'your-api-key',\n  model: 'gpt-5.2',\n})`}\n\t\t\t\t/>\n\t\t\t</section>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/advanced/security-permissions/page.tsx",
    "content": "import BetaNotice from '@/components/BetaNotice'\nimport { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\nexport default function SecurityPermissions() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<div>\n\t\t\t<BetaNotice />\n\n\t\t\t<h1 className=\"text-4xl font-bold mb-6\">{isZh ? '安全与权限' : 'Security & Permissions'}</h1>\n\n\t\t\t<p className=\"text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed\">\n\t\t\t\t{isZh\n\t\t\t\t\t? 'page-agent 提供多种安全机制，确保 AI 操作在可控范围内进行。'\n\t\t\t\t\t: 'page-agent provides multiple security mechanisms to ensure AI operations stay within controlled boundaries.'}\n\t\t\t</p>\n\n\t\t\t<div className=\"space-y-6\">\n\t\t\t\t<section>\n\t\t\t\t\t<Heading id=\"element-interaction-allowlist-blocklist\" className=\"text-2xl font-bold mb-3\">\n\t\t\t\t\t\t{isZh ? '元素操作黑白名单' : 'Element Interaction Allowlist/Blocklist'}\n\t\t\t\t\t</Heading>\n\t\t\t\t\t<div className=\"space-y-3\">\n\t\t\t\t\t\t<div className=\"p-4 bg-red-50 dark:bg-red-900/20 rounded-lg\">\n\t\t\t\t\t\t\t<h3 className=\"text-lg font-semibold text-red-900 dark:text-red-300\">\n\t\t\t\t\t\t\t\t🚫 {isZh ? '操作黑名单' : 'Blocklist'}\n\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t? '禁止 AI 操作敏感元素，如删除按钮、支付按钮等。'\n\t\t\t\t\t\t\t\t\t: 'Prevent AI from interacting with sensitive elements like delete buttons, payment buttons, etc.'}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"p-4 bg-green-50 dark:bg-green-900/20 rounded-lg\">\n\t\t\t\t\t\t\t<h3 className=\"text-lg font-semibold text-green-900 dark:text-green-300\">\n\t\t\t\t\t\t\t\t✅ {isZh ? '操作白名单' : 'Allowlist'}\n\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t? '明确定义 AI 可以操作的元素范围。'\n\t\t\t\t\t\t\t\t\t: 'Explicitly define which elements AI can interact with.'}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\n\t\t\t\t<section>\n\t\t\t\t\t<Heading id=\"instruction-safety-constraints\" className=\"text-2xl font-bold mb-3\">\n\t\t\t\t\t\t{isZh ? 'Instruction 安全约束' : 'Instruction Safety Constraints'}\n\t\t\t\t\t</Heading>\n\t\t\t\t\t<div className=\"p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg\">\n\t\t\t\t\t\t<h3 className=\"text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300\">\n\t\t\t\t\t\t\t⚠️ {isZh ? '高危操作控制' : 'High-Risk Operation Control'}\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-3\">\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '在 AI 指令中明确列举高危操作，通过两种策略进行控制：'\n\t\t\t\t\t\t\t\t: 'Define high-risk operations in AI instructions and control them through two strategies:'}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<div className=\"space-y-2\">\n\t\t\t\t\t\t\t<div className=\"pl-3 border-l-2 border-red-400\">\n\t\t\t\t\t\t\t\t<p className=\"font-medium text-red-700 dark:text-red-300\">\n\t\t\t\t\t\t\t\t\t{isZh ? '完全禁止操作' : 'Completely Forbidden'}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t<p className=\"text-sm text-gray-500 dark:text-gray-400\">\n\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t? '对极高风险操作明确禁止执行'\n\t\t\t\t\t\t\t\t\t\t: 'Explicitly prohibit execution of extremely high-risk operations'}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"pl-3 border-l-2 border-orange-400\">\n\t\t\t\t\t\t\t\t<p className=\"font-medium text-orange-700 dark:text-orange-300\">\n\t\t\t\t\t\t\t\t\t{isZh ? '需用户确认操作' : 'Requires User Confirmation'}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t<p className=\"text-sm text-gray-500 dark:text-gray-400\">\n\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t? '对中等风险操作要求用户明确同意'\n\t\t\t\t\t\t\t\t\t\t: 'Require explicit user consent for medium-risk operations'}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/features/chrome-extension/page.tsx",
    "content": "import { siChromewebstore, siGithub } from 'simple-icons'\n\nimport CodeEditor from '@/components/CodeEditor'\nimport { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\nexport default function ChromeExtension() {\n\tconst { isZh } = useLanguage()\n\tconst chromeWebStoreUrl =\n\t\t'https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj'\n\tconst githubReleasesUrl = 'https://github.com/alibaba/page-agent/releases'\n\n\treturn (\n\t\t<div>\n\t\t\t<h1 className=\"text-4xl font-bold mb-6\">{isZh ? 'Chrome 扩展' : 'Chrome Extension'}</h1>\n\n\t\t\t<p className=\"text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed\">\n\t\t\t\t{isZh\n\t\t\t\t\t? '可选的 Chrome 扩展。PageAgent.js 继续负责页面内自动化；扩展 API 额外提供多页面任务、浏览器级控制，以及从浏览器外部发起任务的能力。'\n\t\t\t\t\t: 'An optional Chrome extension. PageAgent.js keeps handling in-page automation, while the extension API adds multi-page tasks, browser-level control, and tasks initiated from outside the browser.'}\n\t\t\t</p>\n\n\t\t\t<div className=\"space-y-8 mt-8\">\n\t\t\t\t{/* Features */}\n\t\t\t\t<section>\n\t\t\t\t\t<Heading id=\"key-features\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t\t\t{isZh ? '核心特性' : 'Key Features'}\n\t\t\t\t\t</Heading>\n\t\t\t\t\t<div className=\"grid md:grid-cols-3 gap-4\">\n\t\t\t\t\t\t<div className=\"p-4 bg-gray-50 dark:bg-gray-800 rounded-lg\">\n\t\t\t\t\t\t\t<h3 className=\"font-semibold mb-2\">🔓 {isZh ? '多页任务' : 'Multi-Page Tasks'}</h3>\n\t\t\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 text-sm\">\n\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t? '跨多个页面和标签页连续执行任务，不再受限于单页上下文。'\n\t\t\t\t\t\t\t\t\t: 'Run tasks across multiple pages and tabs without being limited to a single page context.'}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"p-4 bg-gray-50 dark:bg-gray-800 rounded-lg\">\n\t\t\t\t\t\t\t<h3 className=\"font-semibold mb-2\">\n\t\t\t\t\t\t\t\t🧭 {isZh ? '浏览器级控制' : 'Browser-Level Control'}\n\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 text-sm\">\n\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t? '支持跨标签导航、页面切换和更完整的浏览器自动化能力。'\n\t\t\t\t\t\t\t\t\t: 'Enable richer browser automation, including cross-tab navigation and page switching.'}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"p-4 bg-gray-50 dark:bg-gray-800 rounded-lg\">\n\t\t\t\t\t\t\t<h3 className=\"font-semibold mb-2\">\n\t\t\t\t\t\t\t\t🔌 {isZh ? '开放集成接口' : 'Open Integration API'}\n\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 text-sm\">\n\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t? '用户主动授权后，页面 JS、本地 Agent 或云端 Agent 可通过扩展发起多页面任务。'\n\t\t\t\t\t\t\t\t\t: 'With explicit user authorization, page JS, local agents, or cloud agents can trigger multi-page tasks through the extension.'}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\n\t\t\t\t{/* Install */}\n\t\t\t\t<section>\n\t\t\t\t\t<Heading id=\"get-the-extension\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t\t\t{isZh ? '获取扩展' : 'Get the Extension'}\n\t\t\t\t\t</Heading>\n\t\t\t\t\t<div className=\"flex flex-wrap gap-3\">\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\thref={chromeWebStoreUrl}\n\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\tclassName=\"inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white! font-medium rounded-lg transition-colors\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<svg className=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n\t\t\t\t\t\t\t\t<path d={siChromewebstore.path} />\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t{isZh ? '从 Chrome 应用商店安装' : 'Install from Chrome Web Store'}\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\thref={githubReleasesUrl}\n\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\tclassName=\"inline-flex items-center gap-2 px-6 py-3 bg-gray-900 hover:bg-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600 text-white! font-medium rounded-lg transition-colors\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<svg className=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n\t\t\t\t\t\t\t\t<path d={siGithub.path} />\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t{isZh ? 'GitHub Releases（更新版本）' : 'GitHub Releases (faster updates)'}\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\n\t\t\t\t{/* Relationship with PageAgent.js */}\n\t\t\t\t<section>\n\t\t\t\t\t<Heading id=\"how-it-relates-to-page-agent-js\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t\t\t{isZh ? '与 PageAgent.js 的关系' : 'How It Relates to PageAgent.js'}\n\t\t\t\t\t</Heading>\n\t\t\t\t\t<div className=\"p-5 bg-gray-50 dark:bg-gray-800 rounded-lg space-y-3 text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? 'PageAgent.js 本身即可在页面内完成自动化。Chrome 扩展是可选的能力扩展。'\n\t\t\t\t\t\t\t\t: 'PageAgent.js already works for in-page automation. The Chrome extension is optional, not a dependency.'}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '通过扩展，你可以执行多页面任务、控制浏览器，以及从浏览器外部（本地服务或云端服务）发起任务。'\n\t\t\t\t\t\t\t\t: 'With the extension, you can perform multi-page tasks, browser-level control, and tasks triggered outside the browser (local or cloud services).'}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t</section>\n\n\t\t\t\t{/* Third-party Integration */}\n\t\t\t\t<section>\n\t\t\t\t\t<Heading id=\"third-party-integration\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t\t\t{isZh ? '第三方接入' : 'Third-Party Integration'}\n\t\t\t\t\t</Heading>\n\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-4\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '通过页面 JavaScript 调用 `window.PAGE_AGENT_EXT`，你的应用可以发起跨页面任务并控制浏览器行为。'\n\t\t\t\t\t\t\t: 'By calling `window.PAGE_AGENT_EXT` from page JavaScript, your app can trigger multi-page tasks and control browser behavior.'}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<h3 className=\"text-xl font-semibold mb-3\">\n\t\t\t\t\t\t{isZh ? '授权与安全' : 'Authorization and Security'}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-4\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '扩展权限范围较广（例如页面访问、导航、多标签控制）。若被滥用，可能危害用户隐私。为此，调用能力由 Token 保护，用户必须主动将 Token 提供给其信任的应用。'\n\t\t\t\t\t\t\t: 'The extension has broad permissions (such as page access, navigation, and multi-tab control). If abused, it can harm user privacy. That is why access is protected by a token, and users must actively share the token only with applications they trust.'}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tcode={\n\t\t\t\t\t\t\tisZh\n\t\t\t\t\t\t\t\t? `// 1) 用户在扩展侧边栏获取 auth token\n// 2) 仅在可信应用中设置该 token\n// 3) token 匹配后，扩展会暴露 window.PAGE_AGENT_EXT\n\n// ⚠️ 不要把 token 提供给不可信页面或脚本\nlocalStorage.setItem('PageAgentExtUserAuthToken', '<从扩展中获取的-token>')`\n\t\t\t\t\t\t\t\t: `// 1) Get auth token from the extension side panel\n// 2) Set it only in trusted applications\n// 3) After token match, extension exposes window.PAGE_AGENT_EXT\n\n// ⚠️ Never provide the token to untrusted pages or scripts\nlocalStorage.setItem('PageAgentExtUserAuthToken', '<your-token-from-extension>')`\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlanguage=\"javascript\"\n\t\t\t\t\t/>\n\t\t\t\t</section>\n\n\t\t\t\t{/* API Reference */}\n\t\t\t\t<section>\n\t\t\t\t\t<Heading id=\"api-reference\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t\t\t{isZh ? 'API 参考' : 'API Reference'}\n\t\t\t\t\t</Heading>\n\n\t\t\t\t\t{/* AI Assistant Instructions */}\n\t\t\t\t\t<section className=\"p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg\">\n\t\t\t\t\t\t<h3 className=\"text-lg font-semibold text-purple-900 dark:text-purple-300 mb-2\">\n\t\t\t\t\t\t\t🤖 {isZh ? '给 AI 编程助手的文档' : 'Instructions for Your AI Assistant'}\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-3 text-sm\">\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '如果你在使用 AI 编程助手（如 Cursor、GitHub Copilot），可以将以下文档链接提供给它，让它更好地理解和使用 Page Agent 扩展 API：'\n\t\t\t\t\t\t\t\t: 'If you are using an AI coding assistant (like Cursor, GitHub Copilot), share these documentation links with it for better understanding of Page Agent Extension API:'}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<div className=\"space-y-2\">\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/blob/main/packages/extension/docs/extension_api.md\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\tclassName=\"block text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:underline\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t📄 {isZh ? 'API 文档' : 'API Documentation'}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</section>\n\n\t\t\t\t\t{/* TypeScript Declaration */}\n\t\t\t\t\t<Heading id=\"typescript-declaration\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t\t\t{isZh ? 'TypeScript 类型声明' : 'TypeScript Declaration'}\n\t\t\t\t\t</Heading>\n\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-4\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '推荐把 `execute` 的类型声明加入你的项目，获得完整类型提示。'\n\t\t\t\t\t\t\t: 'Add this `execute` declaration to your project for full type support.'}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tcode={`import type {\n\tAgentActivity,\n\tAgentStatus,\n\tExecutionResult,\n\tHistoricalEvent\n} from '@page-agent/core'\n\ninterface ExecuteConfig {\n\tbaseURL: string   // LLM API endpoint\n\tmodel: string     // Model name\n\tapiKey?: string   // LLM AK\n\n\tincludeInitialTab?: boolean\n\tonStatusChange?: (status: AgentStatus) => void\n\tonActivity?: (activity: AgentActivity) => void\n\tonHistoryUpdate?: (history: HistoricalEvent[]) => void\n}\n\ntype Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>\n\ndeclare global {\n\tinterface Window {\n\t\tPAGE_AGENT_EXT_VERSION?: string\n\t\tPAGE_AGENT_EXT?: {\n\t\t\tversion: string\n\t\t\texecute: Execute\n\t\t\tstop: () => void\n\t\t}\n\t}\n}`}\n\t\t\t\t\t\tlanguage=\"typescript\"\n\t\t\t\t\t/>\n\n\t\t\t\t\t<h3 className=\"text-xl font-semibold mt-6 mb-3\">PAGE_AGENT_EXT.execute(task, config)</h3>\n\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tcode={\n\t\t\t\t\t\t\tisZh\n\t\t\t\t\t\t\t\t? `// 使用配置执行任务\nconst result = await window.PAGE_AGENT_EXT.execute(\n\t'在 GitHub 上搜索 \"page-agent\" 并打开第一个结果',\n\t{\n\t\tbaseURL: 'https://api.openai.com/v1',\n\t\tapiKey: 'your-api-key',\n\t\tmodel: 'gpt-5.2',\n\t\t// includeInitialTab: false, // 设为 false 排除初始标签页\n\t\tonStatusChange: status => console.log('状态变化:', status),\n\t\tonActivity: activity => console.log('活动:', activity),\n\t\tonHistoryUpdate: history => console.log('历史更新:', history)\n\t}\n)\n\nconsole.log(result) // 任务执行结果`\n\t\t\t\t\t\t\t\t: `// Execute a task with configuration\nconst result = await window.PAGE_AGENT_EXT.execute(\n\t'Search for \"page-agent\" on GitHub and open the first result',\n\t{\n\t\tbaseURL: 'https://api.openai.com/v1',\n\t\tapiKey: 'your-api-key',\n\t\tmodel: 'gpt-5.2',\n\t\t// includeInitialTab: false, // Set to false to exclude initial tab\n\t\tonStatusChange: status => console.log('Status change:', status),\n\t\tonActivity: activity => console.log('Activity:', activity),\n\t\tonHistoryUpdate: history => console.log('History update:', history)\n\t}\n)\n\nconsole.log(result) // Task execution result`\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlanguage=\"javascript\"\n\t\t\t\t\t/>\n\n\t\t\t\t\t<h3 className=\"text-xl font-semibold mt-6 mb-3\">PAGE_AGENT_EXT.stop()</h3>\n\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-4\">\n\t\t\t\t\t\t{isZh ? '停止当前正在运行的任务。' : 'Stop the current running task.'}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tcode={\n\t\t\t\t\t\t\tisZh\n\t\t\t\t\t\t\t\t? `// 停止当前任务\nwindow.PAGE_AGENT_EXT.stop()`\n\t\t\t\t\t\t\t\t: `// Stop current task execution\nwindow.PAGE_AGENT_EXT.stop()`\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlanguage=\"javascript\"\n\t\t\t\t\t/>\n\t\t\t\t</section>\n\n\t\t\t\t{/* Integration Guide */}\n\t\t\t\t<section>\n\t\t\t\t\t<Heading\n\t\t\t\t\t\tid=\"integrate-multipageagent-into-your-extension\"\n\t\t\t\t\t\tclassName=\"text-2xl font-bold mb-4\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '将 MultiPageAgent 集成你自己的插件'\n\t\t\t\t\t\t\t: 'Integrate MultiPageAgent into Your Extension'}\n\t\t\t\t\t</Heading>\n\t\t\t\t\t<p>@TODO</p>\n\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-4\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '建议先阅读扩展 API 文档，再参考 background entry implementation。'\n\t\t\t\t\t\t\t: 'Start with the extension API docs, then use the background entry implementation as a reference.'}\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/blob/main/packages/extension/src/entrypoints/background.ts\"\n\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\tclassName=\"inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<svg className=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n\t\t\t\t\t\t\t\t<path d={siGithub.path} />\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\tpackages/extension/src/entrypoints/background.ts\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</p>\n\t\t\t\t</section>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/features/custom-instructions/page.tsx",
    "content": "import CodeEditor from '@/components/CodeEditor'\nimport { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\nexport default function Instructions() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<div>\n\t\t\t<h1 className=\"text-4xl font-bold mb-6\">{isZh ? '知识注入' : 'Instructions'}</h1>\n\n\t\t\t<p className=\"text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed\">\n\t\t\t\t{isZh\n\t\t\t\t\t? '通过 instructions 配置，为 AI 注入系统级指导和页面级上下文，让它更好地理解你的业务场景。'\n\t\t\t\t\t: 'Use the instructions config to inject system-level directives and page-specific context, helping the AI better understand your application.'}\n\t\t\t</p>\n\n\t\t\t{/* System Instructions */}\n\t\t\t<section className=\"mb-12\">\n\t\t\t\t<Heading id=\"system-instructions\" className=\"text-3xl font-bold mb-6\">\n\t\t\t\t\t{isZh ? '系统级指导 (System Instructions)' : 'System Instructions'}\n\t\t\t\t</Heading>\n\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-6\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '全局提示词，应用于所有任务。定义 AI 的角色、工作风格和行为边界。'\n\t\t\t\t\t\t: \"Global directives applied to all tasks. Define the AI's role, working style, and behavioral boundaries.\"}\n\t\t\t\t</p>\n\n\t\t\t\t<CodeEditor\n\t\t\t\t\tclassName=\"mb-6\"\n\t\t\t\t\tcode={`const agent = new PageAgent({\n  // ...other config\n  instructions: {\n    system: \\`\nYou are a professional e-commerce assistant.\n\nGuidelines:\n- Always confirm before submitting orders\n- Double-check prices and quantities\n- Report errors immediately instead of retrying blindly\n\\`\n  }\n})`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t{/* Page Instructions */}\n\t\t\t<section className=\"mb-12\">\n\t\t\t\t<Heading id=\"page-instructions\" className=\"text-3xl font-bold mb-6\">\n\t\t\t\t\t{isZh ? '页面级指导 (Page Instructions)' : 'Page Instructions'}\n\t\t\t\t</Heading>\n\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-6\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '动态回调函数，在每个 step 执行前调用，根据当前页面 URL 返回特定提示词。适用于为不同页面提供针对性的操作引导。'\n\t\t\t\t\t\t: 'A dynamic callback invoked before each step. Returns page-specific instructions based on the current URL. Useful for providing targeted guidance on different pages.'}\n\t\t\t\t</p>\n\n\t\t\t\t<CodeEditor\n\t\t\t\t\tclassName=\"mb-6\"\n\t\t\t\t\tcode={`const agent = new PageAgent({\n  // ...other config\n  instructions: {\n    system: 'You are an order management assistant.',\n\n    getPageInstructions: (url) => {\n      if (url.includes('/checkout')) {\n        return \\`\nThis is the checkout page.\n- Verify shipping address before proceeding\n- Check if any discounts are applied\n- Confirm the total amount with the user\n\\`\n      }\n\n      if (url.includes('/products')) {\n        return \\`\nThis is the product listing page.\n- Use filters to narrow down search results\n- Check stock availability before adding to cart\n\\`\n      }\n\n      return undefined // No special instructions for other pages\n    }\n  }\n})`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t{/* How It Works */}\n\t\t\t<section className=\"mb-12\">\n\t\t\t\t<Heading id=\"how-it-works\" className=\"text-3xl font-bold mb-6\">\n\t\t\t\t\t{isZh ? '工作原理' : 'How It Works'}\n\t\t\t\t</Heading>\n\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '在每个执行步骤之前，page-agent 会将 instructions 拼接到用户提示词中：'\n\t\t\t\t\t\t: 'Before each execution step, page-agent prepends the instructions to the user prompt:'}\n\t\t\t\t</p>\n\n\t\t\t\t<CodeEditor\n\t\t\t\t\tlanguage=\"xml\"\n\t\t\t\t\tclassName=\"mb-6\"\n\t\t\t\t\tcode={`<instructions>\n<system_instructions>\nYou are a professional e-commerce assistant.\n...\n</system_instructions>\n<page_instructions>\nThis is the checkout page.\n...\n</page_instructions>\n</instructions>\n\n<!-- followed by agent state, history, and browser state -->`}\n\t\t\t\t/>\n\n\t\t\t\t<ul className=\"list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400\">\n\t\t\t\t\t<li>\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '如果 system 为空，则不输出 <system_instructions> 标签'\n\t\t\t\t\t\t\t: 'If system is empty, the <system_instructions> tag is omitted'}\n\t\t\t\t\t</li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '如果 getPageInstructions 返回空值，则不输出 <page_instructions> 标签'\n\t\t\t\t\t\t\t: 'If getPageInstructions returns empty, the <page_instructions> tag is omitted'}\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/features/custom-tools/page.tsx",
    "content": "import CodeEditor from '@/components/CodeEditor'\nimport { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\nexport default function CustomTools() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<div>\n\t\t\t<h1 className=\"text-4xl font-bold mb-6\">{isZh ? '自定义工具' : 'Custom Tools'}</h1>\n\n\t\t\t<p className=\"text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed\">\n\t\t\t\t{isZh\n\t\t\t\t\t? '通过注册自定义工具，扩展 AI Agent 的能力边界。使用 Zod 定义输入接口，让 AI 安全调用你的业务逻辑。'\n\t\t\t\t\t: 'Extend AI Agent capabilities by registering custom tools. Define input schemas with Zod for safe business logic invocation.'}\n\t\t\t</p>\n\n\t\t\t<div className=\"space-y-8\">\n\t\t\t\t<section>\n\t\t\t\t\t<Heading id=\"zod-version\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t\t\t{isZh ? 'Zod 版本' : 'Zod Version'}\n\t\t\t\t\t</Heading>\n\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-4\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? 'Page Agent 使用 Zod 定义工具的输入 schema。支持 Zod 3 (>=3.25.0) 和 Zod 4，请从 zod/v4 子路径导入。不支持 Zod Mini。'\n\t\t\t\t\t\t\t: 'Page Agent uses Zod for tool input schemas. Both Zod 3 (>=3.25.0) and Zod 4 are supported. Always import from the zod/v4 subpath. Zod Mini is not supported.'}\n\t\t\t\t\t</p>\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tcode={`// Zod 3 (>=3.25.0) or Zod 4\nimport { z } from 'zod/v4'`}\n\t\t\t\t\t\tlanguage=\"javascript\"\n\t\t\t\t\t/>\n\t\t\t\t</section>\n\n\t\t\t\t<section>\n\t\t\t\t\t<Heading id=\"define-tools\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t\t\t{isZh ? '定义工具' : 'Define Tools'}\n\t\t\t\t\t</Heading>\n\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-4\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '使用 tool() 辅助函数定义自定义工具，每个工具包含 description、inputSchema 和 execute 三个属性。'\n\t\t\t\t\t\t\t: 'Use the tool() helper to define custom tools with description, inputSchema, and execute.'}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tcode={`import { z } from 'zod/v4'\nimport { PageAgent, tool } from 'page-agent'\n\nconst pageAgent = new PageAgent({\n  customTools: {\n  \n\t// \n    add_to_cart: tool({\n      description: 'Add a product to the shopping cart by its product ID.',\n      inputSchema: z.object({\n        productId: z.string(),\n        quantity: z.number().min(1).default(1),\n      }),\n      execute: async function (input) {\n        await fetch('/api/cart', {\n          method: 'POST',\n          body: JSON.stringify(input),\n        })\n        return \\`Added \\${input.quantity}x \\${input.productId} to cart.\\`\n      },\n    }),\n\n\t// \n    search_knowledge_base: tool({\n      description: 'Search the internal knowledge base and return relevant articles.',\n      inputSchema: z.object({\n        query: z.string(),\n        limit: z.number().max(10).default(3),\n      }),\n      execute: async function (input) {\n        const res = await fetch(\n          \\`/api/kb?q=\\${encodeURIComponent(input.query)}&limit=\\${input.limit}\\`\n        )\n        const articles = await res.json()\n        return JSON.stringify(articles)\n      },\n    }),\n  },\n})`}\n\t\t\t\t\t\tlanguage=\"javascript\"\n\t\t\t\t\t/>\n\t\t\t\t</section>\n\n\t\t\t\t<section>\n\t\t\t\t\t<Heading id=\"override-remove\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t\t\t{isZh ? '覆盖与移除内置工具' : 'Override & Remove Built-in Tools'}\n\t\t\t\t\t</Heading>\n\t\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-4\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '使用相同的名称可以覆盖内置工具的行为，设置为 null 则完全移除该工具。'\n\t\t\t\t\t\t\t: 'Use the same name to override a built-in tool, or set it to null to remove it entirely.'}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tcode={`const pageAgent = new PageAgent({\n  customTools: {\n    scroll: null, // remove scroll tool\n    execute_javascript: null, // remove script execution\n  },\n})`}\n\t\t\t\t\t\tlanguage=\"javascript\"\n\t\t\t\t\t/>\n\t\t\t\t</section>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/features/data-masking/page.tsx",
    "content": "import CodeEditor from '@/components/CodeEditor'\nimport { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\nexport default function DataMasking() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<div>\n\t\t\t<h1 className=\"text-4xl font-bold mb-6\">{isZh ? '数据脱敏' : 'Data Masking'}</h1>\n\n\t\t\t<p className=\"text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed\">\n\t\t\t\t{isZh\n\t\t\t\t\t? '使用 transformPageContent 钩子在页面内容发送给 LLM 之前进行处理，可用于检查清洗效果、修改页面信息、隐藏敏感数据等。'\n\t\t\t\t\t: 'Use the transformPageContent hook to process page content before sending to LLM. Useful for inspecting extraction results, modifying page info, and masking sensitive data.'}\n\t\t\t</p>\n\n\t\t\t<section className=\"mb-12\">\n\t\t\t\t<Heading id=\"api-definition\" className=\"text-3xl font-bold mb-6\">\n\t\t\t\t\t{isZh ? '接口定义' : 'API Definition'}\n\t\t\t\t</Heading>\n\n\t\t\t\t<CodeEditor\n\t\t\t\t\tclassName=\"mb-6\"\n\t\t\t\t\tcode={`interface PageAgentConfig {\n  /**\n   * Transform page content before sending to LLM.\n   * Called after DOM extraction and simplification.\n   */\n  transformPageContent?: (content: string) => Promise<string> | string\n}`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t<section className=\"mb-12\">\n\t\t\t\t<Heading id=\"common-masking-patterns\" className=\"text-3xl font-bold mb-6\">\n\t\t\t\t\t{isZh ? '常用脱敏规则' : 'Common Masking Patterns'}\n\t\t\t\t</Heading>\n\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-6\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '以下示例展示了如何脱敏常见的敏感信息：'\n\t\t\t\t\t\t: 'The following example shows how to mask common sensitive data:'}\n\t\t\t\t</p>\n\n\t\t\t\t<CodeEditor\n\t\t\t\t\tcode={`const agent = new PageAgent({\n  transformPageContent: async (content) => {\n    // China phone number (11 digits starting with 1)\n    content = content.replace(/\\\\b(1[3-9]\\\\d)(\\\\d{4})(\\\\d{4})\\\\b/g, '$1****$3')\n\n    // Email address\n    content = content.replace(\n      /\\\\b([a-zA-Z0-9._%+-])[^@]*(@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,})\\\\b/g,\n      '$1***$2'\n    )\n\n    // China ID card number (18 digits)\n    content = content.replace(\n      /\\\\b(\\\\d{6})(19|20\\\\d{2})(0[1-9]|1[0-2])(0[1-9]|[12]\\\\d|3[01])(\\\\d{3}[\\\\dXx])\\\\b/g,\n      '$1********$5'\n    )\n\n    // Bank card number (16-19 digits)\n    content = content.replace(/\\\\b(\\\\d{4})\\\\d{8,11}(\\\\d{4})\\\\b/g, '$1********$2')\n\n    return content\n  }\n})`}\n\t\t\t\t/>\n\t\t\t</section>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/features/models/page.tsx",
    "content": "import { Fragment } from 'react'\n\nimport CodeEditor from '@/components/CodeEditor'\nimport { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\nconst BASELINE = new Set([\n\t'gpt-5.1',\n\t'claude-haiku-4.5',\n\t'gemini-3-flash',\n\t'deepseek-3.2',\n\t'qwen3.5-plus',\n\t'qwen3.5-flash',\n])\n\n// Models grouped by brand, newest first\nconst MODEL_GROUPS: Record<string, string[]> = {\n\tQwen: [\n\t\t'qwen3.5-plus',\n\t\t'qwen3.5-flash',\n\t\t'qwen3-coder-next',\n\t\t'qwen-3-max',\n\t\t'qwen-3-plus',\n\t\t'qwen3:14b (ollama)',\n\t],\n\tOpenAI: ['gpt-5.4', 'gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-4.1', 'gpt-4.1-mini'],\n\tDeepSeek: ['deepseek-3.2'],\n\tGoogle: ['gemini-3-pro', 'gemini-3-flash', 'gemini-2.5'],\n\tAnthropic: [\n\t\t'claude-opus-4.6',\n\t\t'claude-opus-4.5',\n\t\t'claude-sonnet-4.5',\n\t\t'claude-haiku-4.5',\n\t\t'claude-sonnet-3.5',\n\t],\n\txAI: ['grok-4.1-fast', 'grok-4', 'grok-code-fast'],\n\tMiniMax: ['MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed'],\n\tMoonshotAI: ['kimi-k2.5'],\n\t'Z.AI': ['glm-5', 'glm-4.7'],\n}\n\nconst ModelBadge = ({ model, baseline }: { model: string; baseline?: boolean }) => (\n\t<div\n\t\tclassName={`px-3 py-1.5 rounded-md text-xs font-medium font-mono transition-colors ${\n\t\t\tbaseline\n\t\t\t\t? 'bg-emerald-500 text-white shadow-sm'\n\t\t\t\t: 'bg-white/80 dark:bg-gray-800/80 text-gray-800 dark:text-gray-200 border border-gray-300 dark:border-gray-600'\n\t\t}`}\n\t>\n\t\t{model}\n\t\t{baseline && <span className=\"ml-1\">⭐</span>}\n\t</div>\n)\n\nexport default function Models() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<div className=\"max-w-4xl\">\n\t\t\t<h1 className=\"text-4xl font-bold mb-4\">{isZh ? '模型' : 'Models'}</h1>\n\t\t\t<p className=\"text-lg text-gray-600 dark:text-gray-400 mb-8\">\n\t\t\t\t{isZh\n\t\t\t\t\t? '当前支持符合 OpenAI 接口规范且支持 tool call 的模型,包括公有云服务和私有部署方案。'\n\t\t\t\t\t: 'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.'}\n\t\t\t</p>\n\n\t\t\t{/* Models Section */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"tested-models\" className=\"text-2xl font-semibold mb-3\">\n\t\t\t\t\t{isZh ? '已测试模型' : 'Tested Models'}\n\t\t\t\t</Heading>\n\t\t\t\t<div className=\"bg-linear-to-br from-emerald-50 to-cyan-50 dark:from-emerald-950/30 dark:to-cyan-950/30 rounded-xl p-6 border border-emerald-200/50 dark:border-emerald-800/50\">\n\t\t\t\t\t<div className=\"grid grid-cols-[5rem_1fr] gap-x-3 gap-y-3 items-start\">\n\t\t\t\t\t\t{Object.entries(MODEL_GROUPS).map(([brand, models]) => (\n\t\t\t\t\t\t\t<Fragment key={brand}>\n\t\t\t\t\t\t\t\t<span className=\"text-xs font-semibold text-gray-500 dark:text-gray-400 pt-2\">\n\t\t\t\t\t\t\t\t\t{brand}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<div className=\"flex flex-wrap gap-2\">\n\t\t\t\t\t\t\t\t\t{models.map((model) => (\n\t\t\t\t\t\t\t\t\t\t<ModelBadge key={model} model={model} baseline={BASELINE.has(model)} />\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</Fragment>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t{/* Tips Section */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<h2 className=\"text-2xl font-semibold mb-4\">{isZh ? '提示' : 'Tips'}</h2>\n\t\t\t\t<div className=\"p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800\">\n\t\t\t\t\t<ul className=\"text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5\">\n\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '⭐ 推荐使用 ToolCall 能力强的轻量级模型'\n\t\t\t\t\t\t\t\t: '⭐ Recommended: Fast, lightweight models with strong ToolCall capabilities'}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? 'ToolCall 能力较弱的模型可能返回错误的格式，常见错误能够自动恢复，建议设置较高的 temperature'\n\t\t\t\t\t\t\t\t: 'Models with weaker ToolCall capabilities may return incorrect formats. Common errors usually auto-recover. Higher temperature recommended'}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '小模型或者无法适应复杂 Tool 定义的模型，通常效果不佳'\n\t\t\t\t\t\t\t\t: 'Small models or those unable to handle complex tool definitions typically perform poorly'}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t{/* Configuration Section */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"configuration\">{isZh ? '配置方式' : 'Configuration'}</Heading>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tcode={`// OpenAI-compatible services (e.g., Alibaba Bailian)\nconst pageAgent = new PageAgent({\n  baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n  apiKey: 'your-api-key',\n  model: 'qwen3.5-plus'\n});\n\n// MiniMax\nconst pageAgent = new PageAgent({\n  baseURL: 'https://api.minimax.io/v1',\n  apiKey: 'your-minimax-api-key',\n  model: 'MiniMax-M2.7'\n});\n\n// Self-hosted models (e.g., Ollama) — no apiKey needed\nconst pageAgent = new PageAgent({\n  baseURL: 'http://localhost:11434/v1',\n  model: 'qwen3:14b'\n});\n\n`}\n\t\t\t\t/>\n\t\t\t</section>\n\n\t\t\t{/* Free Testing API Section */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"free-testing-api\">{isZh ? '免费测试接口' : 'Free Testing API'}</Heading>\n\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '以下免费测试接口仅供 PageAgent.js 和 PageAgent Extension 的技术评估和测试使用。'\n\t\t\t\t\t\t: 'The following free testing endpoint is provided for testing and technical evaluation.'}\n\t\t\t\t</p>\n\t\t\t\t<div className=\"my-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800\">\n\t\t\t\t\t<p className=\"text-xs text-gray-600 dark:text-gray-400\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '⚠️ 仅供技术评估和研发用途，禁止用于生产环境。数据通过中国大陆服务器处理。请勿输入任何个人身份信息或敏感数据。使用即表示您同意'\n\t\t\t\t\t\t\t: '⚠️ Strictly for technical evaluation and R&D only. Data is processed via servers in Mainland China. Do not input any PII or sensitive data. By using this API you agree to the'}{' '}\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use\"\n\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\tclassName=\"text-blue-500 hover:underline\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{isZh ? '使用条款' : 'Terms of Use'}\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"bg-gray-50 dark:bg-gray-900/30 rounded-lg p-5 border border-gray-200 dark:border-gray-800\">\n\t\t\t\t\t<h3 className=\"font-semibold text-gray-900 dark:text-gray-100 mb-2\">\n\t\t\t\t\t\tQwen (Alibaba Cloud China)\n\t\t\t\t\t</h3>\n\t\t\t\t\t<p className=\"text-xs text-gray-500 dark:text-gray-400 mb-3\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '通过阿里云函数计算（中国大陆）转发至百炼 Qwen 模型'\n\t\t\t\t\t\t\t: 'Proxied via Alibaba Cloud FC (Mainland China) to BaiLian Qwen models'}\n\t\t\t\t\t\t{' · '}\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use\"\n\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\tclassName=\"text-blue-500 hover:underline\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{isZh ? '使用条款' : 'Terms of Use'}\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</p>\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tcode={`# qwen3.5-plus (default for demos) or qwen3.5-flash (lighter)\nLLM_BASE_URL=\"https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run\"\nLLM_MODEL_NAME=\"qwen3.5-plus\"\nLLM_API_KEY=\"NA\"`}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t{/* Ollama Section */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"ollama\">Ollama</Heading>\n\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-400 mb-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '已在 Ollama 0.15 + qwen3:14b (RTX3090 24GB) 上测试通过。'\n\t\t\t\t\t\t: 'Tested on Ollama 0.15 with qwen3:14b (RTX3090 24GB).'}\n\t\t\t\t</p>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tcode={`LLM_BASE_URL=\"http://localhost:11434/v1\"\nLLM_API_KEY=\"NA\"\nLLM_MODEL_NAME=\"qwen3:14b\"`}\n\t\t\t\t/>\n\t\t\t\t<div className=\"mt-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800\">\n\t\t\t\t\t<h3 className=\"font-semibold text-amber-900 dark:text-amber-200 mb-2\">\n\t\t\t\t\t\t{isZh ? '⚠️ 注意事项' : '⚠️ Important Notes'}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<ul className=\"text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5\">\n\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '确保 OLLAMA_ORIGINS 设置为 * 以避免 403 错误'\n\t\t\t\t\t\t\t\t: 'Add * to OLLAMA_ORIGINS to avoid 403 errors'}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '小于 10B 参数的模型通常效果不佳'\n\t\t\t\t\t\t\t\t: 'Models smaller than 10B are unlikely to be strong enough'}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t\t<li>{isZh ? '需要支持 tool_call 的模型' : 'Requires tool_call capable models'}</li>\n\t\t\t\t\t\t<li>\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '确保上下文长度大于输入 token 数，否则 Ollama 会静默截断 prompt。普通页面约需 15k token，随步骤增加。默认 4k 上下文长度无法正常工作'\n\t\t\t\t\t\t\t\t: 'Ensure context length exceeds input tokens, or Ollama will silently truncate prompts. ~15k tokens for a typical page, increases with steps. Default 4k context length will NOT work'}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"mt-4\">\n\t\t\t\t\t<h3 className=\"font-semibold text-gray-900 dark:text-gray-100 mb-3\">\n\t\t\t\t\t\t{isZh ? '建议启动参数' : 'Recommended Startup'}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-400 mb-3\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '启动 Ollama 时建议配置以下环境变量：扩大上下文窗口、允许跨域访问、监听所有网络接口。'\n\t\t\t\t\t\t\t: 'Start Ollama with these environment variables: larger context window, allow cross-origin access, and listen on all interfaces.'}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<div className=\"space-y-2\">\n\t\t\t\t\t\t<p className=\"text-xs font-medium text-gray-500 dark:text-gray-400\">macOS / Linux</p>\n\t\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\t\tcode={`OLLAMA_CONTEXT_LENGTH=64000 OLLAMA_HOST=0.0.0.0:11434 OLLAMA_ORIGINS=\"*\" ollama serve`}\n\t\t\t\t\t\t/>\n\n\t\t\t\t\t\t<p className=\"text-xs font-medium text-gray-500 dark:text-gray-400 pt-2\">\n\t\t\t\t\t\t\tWindows (PowerShell)\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\t\tcode={`$env:OLLAMA_CONTEXT_LENGTH=64000; $env:OLLAMA_HOST=\"0.0.0.0:11434\"; $env:OLLAMA_ORIGINS=\"*\"; ollama serve`}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</section>\n\n\t\t\t{/* Production Authentication */}\n\t\t\t<section className=\"mb-10\">\n\t\t\t\t<Heading id=\"production-authentication\" className=\"text-2xl font-semibold mb-4\">\n\t\t\t\t\t{isZh ? '🔐 生产环境鉴权' : '🔐 Production Authentication'}\n\t\t\t\t</Heading>\n\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-400 mb-3\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '如果你只是将它用作个人助手，可以直接连接你的 LLM 服务。'\n\t\t\t\t\t\t: 'If you only use it as a personal assistant, you can connect to your LLM service directly.'}\n\t\t\t\t</p>\n\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-400 mb-3\">\n\t\t\t\t\t{isZh ? (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t如果你计划将它集成到你的 Web 应用中，建议搭建一个后端代理来转发 LLM 请求，并使用{' '}\n\t\t\t\t\t\t\t<code>customFetch</code> 携带 Cookie 或其他鉴权信息：\n\t\t\t\t\t\t</>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\tIf you plan to integrate it into your web app, it's better to have a backend proxy for\n\t\t\t\t\t\t\tthe LLM and use <code>customFetch</code> to authenticate the request with cookies or\n\t\t\t\t\t\t\tother methods:\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\t\t\t\t</p>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tcode={`const agent = new PageAgent({\n  baseURL: '/api/llm-proxy',\n  model: 'gpt-5.1',\n  customFetch: (url, init) =>\n    fetch(url, { ...init, credentials: 'include' }),\n});`}\n\t\t\t\t/>\n\t\t\t\t<div className=\"mt-4 bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-4 rounded-r-lg\">\n\t\t\t\t\t<p className=\"text-sm font-semibold text-yellow-900 dark:text-yellow-200\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '⚠️ 永远不要把真实的 LLM API Key 提交到前端代码中'\n\t\t\t\t\t\t\t: '⚠️ NEVER commit real LLM API keys to your frontend code'}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t</section>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/features/third-party-agent/page.tsx",
    "content": "import CodeEditor from '@/components/CodeEditor'\nimport { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\nexport default function ThirdPartyAgentPage() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<div>\n\t\t\t<h1 className=\"text-4xl font-bold mb-6\">\n\t\t\t\t{isZh ? '接入第三方 Agent' : 'Third-party Agent Integration'}\n\t\t\t</h1>\n\t\t\t<p className=\"mb-6 leading-relaxed text-gray-600 dark:text-gray-300\">\n\t\t\t\t{isZh\n\t\t\t\t\t? '将 pageAgent 作为工具接入你的答疑助手或 Agent 系统，成为你 Agent 的眼和手。'\n\t\t\t\t\t: 'Integrate pageAgent as a tool in your support assistant or Agent system, becoming the eyes and hands of your Agent.'}\n\t\t\t</p>\n\n\t\t\t<Heading id=\"integration-method\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t{isZh ? '集成方式' : 'Integration Method'}\n\t\t\t</Heading>\n\n\t\t\t<div className=\"space-y-4 mb-6\">\n\t\t\t\t<div className=\"p-4 bg-green-50 dark:bg-green-900/20 rounded-lg\">\n\t\t\t\t\t<h3 className=\"text-lg font-semibold mb-2 text-green-900 dark:text-green-300\">\n\t\t\t\t\t\t1. Function Calling\n\t\t\t\t\t</h3>\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tcode={`// ${isZh ? '定义工具' : 'Define tool'}\nconst pageAgentTool = {\n  name: \"page_agent\",\n  description: \"${isZh ? '执行网页操作' : 'Execute web page operations'}\",\n  parameters: {\n    type: \"object\",\n    properties: {\n      instruction: { type: \"string\", description: \"${isZh ? '操作指令' : 'Operation instruction'}\" }\n    },\n    required: [\"instruction\"]\n  },\n  execute: async (params) => {\n    const result = await pageAgent.execute(params.instruction)\n    return { success: result.success, message: result.data }\n  }\n}\n\n// ${isZh ? '注册到你的 agent 中' : 'Register to your agent'}`}\n\t\t\t\t\t\tlanguage=\"javascript\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<Heading id=\"use-cases\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t{isZh ? '应用场景' : 'Use Cases'}\n\t\t\t</Heading>\n\t\t\t<div className=\"grid md:grid-cols-2 gap-4 mb-6\">\n\t\t\t\t<div className=\"bg-linear-to-br from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg\">\n\t\t\t\t\t<h4 className=\"font-semibold mb-2 text-gray-900 dark:text-white\">\n\t\t\t\t\t\t{isZh ? '🤖 智能客服系统' : '🤖 Smart Customer Service'}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '客服机器人帮用户直接操作系统，如\"帮我提交工单\"'\n\t\t\t\t\t\t\t: 'Support bots directly operate systems for users, e.g., \"Help me submit a ticket\"'}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"bg-linear-to-br from-green-50 to-blue-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg\">\n\t\t\t\t\t<h4 className=\"font-semibold mb-2 text-gray-900 dark:text-white\">\n\t\t\t\t\t\t{isZh ? '📋 业务流程助手' : '📋 Business Process Assistant'}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '引导新员工完成复杂流程，如\"完成客户入职\"'\n\t\t\t\t\t\t\t: 'Guide new employees through complex processes, e.g., \"Complete customer onboarding\"'}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"bg-linear-to-br from-purple-50 to-pink-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg\">\n\t\t\t\t\t<h4 className=\"font-semibold mb-2 text-gray-900 dark:text-white\">\n\t\t\t\t\t\t{isZh ? '🎯 个人效率助手' : '🎯 Personal Productivity Assistant'}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '跨网站帮你完成任务，如\"预订会议室\"'\n\t\t\t\t\t\t\t: 'Complete tasks across websites, e.g., \"Book a meeting room\"'}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"bg-linear-to-br from-orange-50 to-red-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg\">\n\t\t\t\t\t<h4 className=\"font-semibold mb-2 text-gray-900 dark:text-white\">\n\t\t\t\t\t\t{isZh ? '🔧 运维自动化' : '🔧 DevOps Automation'}\n\t\t\t\t\t</h4>\n\t\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '通过自然语言操作管理后台，如\"重启服务器\"'\n\t\t\t\t\t\t\t: 'Operate admin panels via natural language, e.g., \"Restart server\"'}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/index.tsx",
    "content": "import { Suspense } from 'react'\nimport { Route, Switch } from 'wouter'\n\nimport DocsLayout from './Layout'\nimport CustomUIDocs from './advanced/custom-ui/page'\nimport PageAgentCoreDocs from './advanced/page-agent-core/page'\n// Advanced\nimport PageAgentDocs from './advanced/page-agent/page'\nimport PageControllerDocs from './advanced/page-controller/page'\nimport SecurityPermissions from './advanced/security-permissions/page'\n// Features\nimport ChromeExtension from './features/chrome-extension/page'\nimport Instructions from './features/custom-instructions/page'\nimport CustomTools from './features/custom-tools/page'\nimport DataMasking from './features/data-masking/page'\nimport Models from './features/models/page'\nimport ThirdPartyAgent from './features/third-party-agent/page'\nimport Limitations from './introduction/limitations/page'\n// Introduction\nimport Overview from './introduction/overview/page'\nimport QuickStart from './introduction/quick-start/page'\nimport Troubleshooting from './introduction/troubleshooting/page'\n\nfunction DocsPage({ children }: { children: React.ReactNode }) {\n\treturn (\n\t\t<DocsLayout>\n\t\t\t<Suspense>{children}</Suspense>\n\t\t</DocsLayout>\n\t)\n}\n\nexport default function DocsRouter() {\n\treturn (\n\t\t<Switch>\n\t\t\t{/* Introduction */}\n\t\t\t<Route path=\"/introduction/overview\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<Overview />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\t\t\t<Route path=\"/introduction/quick-start\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<QuickStart />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\t\t\t<Route path=\"/introduction/limitations\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<Limitations />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\t\t\t<Route path=\"/introduction/troubleshooting\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<Troubleshooting />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\n\t\t\t{/* Features */}\n\t\t\t<Route path=\"/features/custom-tools\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<CustomTools />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\t\t\t<Route path=\"/features/data-masking\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<DataMasking />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\t\t\t<Route path=\"/features/custom-instructions\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<Instructions />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\t\t\t<Route path=\"/features/models\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<Models />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\t\t\t<Route path=\"/features/chrome-extension\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<ChromeExtension />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\t\t\t<Route path=\"/features/third-party-agent\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<ThirdPartyAgent />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\n\t\t\t{/* Advanced */}\n\t\t\t<Route path=\"/advanced/page-agent\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<PageAgentDocs />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\t\t\t<Route path=\"/advanced/page-agent-core\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<PageAgentCoreDocs />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\t\t\t<Route path=\"/advanced/page-controller\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<PageControllerDocs />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\t\t\t<Route path=\"/advanced/custom-ui\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<CustomUIDocs />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\t\t\t<Route path=\"/advanced/security-permissions\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<SecurityPermissions />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\n\t\t\t{/* Default redirect or 404 */}\n\t\t\t<Route path=\"/docs\">\n\t\t\t\t<DocsPage>\n\t\t\t\t\t<Overview />\n\t\t\t\t</DocsPage>\n\t\t\t</Route>\n\t\t</Switch>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/introduction/limitations/page.tsx",
    "content": "import { Link } from 'wouter'\n\nimport { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\nexport default function LimitationsPage() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<div className=\"max-w-4xl mx-auto\">\n\t\t\t<div className=\"mb-8\">\n\t\t\t\t<h1 className=\"text-4xl font-bold mb-4 text-gray-900 dark:text-white\">\n\t\t\t\t\t{isZh ? '使用限制' : 'Limitations'}\n\t\t\t\t</h1>\n\t\t\t\t<p className=\"text-xl text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? 'Page Agent 基于 DOM 理解网页并执行操作。这决定了它的能力边界。'\n\t\t\t\t\t\t: 'Page Agent understands web pages via DOM and performs actions accordingly. This defines its capability boundary.'}\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div className=\"prose prose-lg dark:prose-invert max-w-none\">\n\t\t\t\t{/* PageAgent.js vs PageAgentExt */}\n\t\t\t\t<Heading id=\"pageagent-js-vs-pageagentext\" className=\"text-2xl font-bold mb-3\">\n\t\t\t\t\t{isZh ? 'PageAgent.js vs PageAgentExt' : 'PageAgent.js vs PageAgentExt'}\n\t\t\t\t</Heading>\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-4\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? 'PageAgent.js 是核心库，运行在页面内。PageAgentExt 是可选的浏览器扩展，提供额外的浏览器级控制能力。'\n\t\t\t\t\t\t: 'PageAgent.js is the core library running inside a page. PageAgentExt is an optional browser extension that adds browser-level control.'}\n\t\t\t\t</p>\n\t\t\t\t<div className=\"overflow-x-auto mb-6\">\n\t\t\t\t\t<table className=\"w-full text-sm border-collapse\">\n\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t<tr className=\"border-b border-gray-200 dark:border-gray-700\">\n\t\t\t\t\t\t\t\t<th className=\"text-left py-3 pr-4\"></th>\n\t\t\t\t\t\t\t\t<th className=\"text-left py-3 px-4 font-semibold\">PageAgent.js</th>\n\t\t\t\t\t\t\t\t<th className=\"text-left py-3 pl-4 font-semibold\">\n\t\t\t\t\t\t\t\t\tPageAgentExt{' '}\n\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\thref=\"/features/chrome-extension\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"text-xs font-normal text-blue-600 dark:text-blue-400 hover:underline\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{isZh ? '了解更多' : 'learn more'}\n\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t<tbody className=\"text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t\t<tr className=\"border-b border-gray-100 dark:border-gray-800\">\n\t\t\t\t\t\t\t\t<td className=\"py-3 pr-4 font-medium text-gray-900 dark:text-white\">\n\t\t\t\t\t\t\t\t\t{isZh ? '接入方式' : 'Integration'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"py-3 px-4\">\n\t\t\t\t\t\t\t\t\t{isZh ? '网站开发者主动集成' : 'Site developer integrates the library'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"py-3 pl-4\">\n\t\t\t\t\t\t\t\t\t{isZh ? '用户安装浏览器扩展' : 'User installs a browser extension'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t<tr className=\"border-b border-gray-100 dark:border-gray-800\">\n\t\t\t\t\t\t\t\t<td className=\"py-3 pr-4 font-medium text-gray-900 dark:text-white\">\n\t\t\t\t\t\t\t\t\t{isZh ? '可操作范围' : 'Scope'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"py-3 px-4\">\n\t\t\t\t\t\t\t\t\t{isZh ? '当前页面（为 SPA 设计）' : 'Current page (designed for SPAs)'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"py-3 pl-4\">\n\t\t\t\t\t\t\t\t\t{isZh ? '任意网页、多标签页' : 'Any web page, multi-tab'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td className=\"py-3 pr-4 font-medium text-gray-900 dark:text-white\">\n\t\t\t\t\t\t\t\t\t{isZh ? '额外能力' : 'Extra capabilities'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"py-3 px-4\">—</td>\n\t\t\t\t\t\t\t\t<td className=\"py-3 pl-4\">\n\t\t\t\t\t\t\t\t\t{isZh ? '新建/切换/关闭标签页' : 'Open / switch / close tabs'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</tbody>\n\t\t\t\t\t</table>\n\t\t\t\t</div>\n\n\t\t\t\t{/* Interaction Limitations */}\n\t\t\t\t<Heading id=\"interaction-capabilities\" className=\"text-2xl font-bold mb-3 mt-6\">\n\t\t\t\t\t{isZh ? '交互能力' : 'Interaction Capabilities'}\n\t\t\t\t</Heading>\n\t\t\t\t<div className=\"bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6\">\n\t\t\t\t\t<div className=\"grid md:grid-cols-2 gap-6\">\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<h3 className=\"font-semibold mb-3 text-green-700 dark:text-green-400\">\n\t\t\t\t\t\t\t\t{isZh ? '支持' : 'Supported'}\n\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t<ul className=\"space-y-1.5 text-sm\">\n\t\t\t\t\t\t\t\t{[\n\t\t\t\t\t\t\t\t\tisZh ? '点击、文本输入、选择' : 'Click, text input, select',\n\t\t\t\t\t\t\t\t\tisZh ? '页面滚动（垂直 / 水平）' : 'Scroll (vertical / horizontal)',\n\t\t\t\t\t\t\t\t\tisZh ? '表单提交、焦点切换' : 'Form submit, focus',\n\t\t\t\t\t\t\t\t\tisZh ? '同源 iframe（仅单层）' : 'Same-origin iframe (single level only)',\n\t\t\t\t\t\t\t\t\tisZh ? '执行 JavaScript（可选）' : 'Execute JavaScript (opt-in)',\n\t\t\t\t\t\t\t\t].map((text) => (\n\t\t\t\t\t\t\t\t\t<li key={text} className=\"flex items-center text-gray-700 dark:text-gray-300\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"mr-2 text-green-600 dark:text-green-400\">✓</span>\n\t\t\t\t\t\t\t\t\t\t{text}\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<h3 className=\"font-semibold mb-3 text-red-700 dark:text-red-400\">\n\t\t\t\t\t\t\t\t{isZh ? '不支持' : 'Not supported'}\n\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t<ul className=\"space-y-1.5 text-sm\">\n\t\t\t\t\t\t\t\t{[\n\t\t\t\t\t\t\t\t\tisZh ? '悬停、拖拽、右键菜单' : 'Hover, drag & drop, right-click',\n\t\t\t\t\t\t\t\t\tisZh ? '键盘快捷键' : 'Keyboard shortcuts',\n\t\t\t\t\t\t\t\t\tisZh ? '坐标定位操作' : 'Position-based control',\n\t\t\t\t\t\t\t\t\tisZh ? '嵌套 iframe、跨域 iframe' : 'Nested iframes, cross-origin iframes',\n\t\t\t\t\t\t\t\t\tisZh ? '绘图操作' : 'Drawing',\n\t\t\t\t\t\t\t\t\tisZh\n\t\t\t\t\t\t\t\t\t\t? 'Monaco、CodeMirror 等需要通过 JS 实例控制的编辑器'\n\t\t\t\t\t\t\t\t\t\t: 'Monaco, CodeMirror and other editors that require JS instance access',\n\t\t\t\t\t\t\t\t].map((text) => (\n\t\t\t\t\t\t\t\t\t<li key={text} className=\"flex items-center text-gray-700 dark:text-gray-300\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"mr-2 text-red-600 dark:text-red-400\">✗</span>\n\t\t\t\t\t\t\t\t\t\t{text}\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t{/* Understanding Limitations */}\n\t\t\t\t<Heading id=\"text-based-approach\" className=\"text-2xl font-bold mb-3 mt-6\">\n\t\t\t\t\t{isZh ? '基于文本的方案' : 'Text-Based Approach'}\n\t\t\t\t</Heading>\n\n\t\t\t\t<p className=\"mb-2 font-medium\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? 'Page Agent 不使用多模态模型，不截图，没有视觉能力。仅通过 DOM 结构理解页面。'\n\t\t\t\t\t\t: 'Page Agent does not use multimodal models, does not take screenshots, and has no visual capability. It reads pages through DOM structure only.'}\n\t\t\t\t</p>\n\t\t\t\t<p className=\"mb-2 font-medium\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '图片、Canvas、WebGL、SVG 等视觉内容无法被识别。页面的语义化程度和可访问性直接影响 AI 的理解准确性。'\n\t\t\t\t\t\t: 'Images, Canvas, WebGL, SVG and other visual content cannot be recognized. Page semantic quality and accessibility directly affect AI accuracy.'}\n\t\t\t\t</p>\n\t\t\t\t<p className=\"mb-2 font-medium\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '反常识的交互逻辑、纯视觉的操作提示、快速出现消失的元素等都会降低自动化成功率。语义化的 HTML 和良好的可访问性会显著提升效果。'\n\t\t\t\t\t\t: 'Counter-intuitive interactions, visual-only cues, and rapidly appearing/disappearing elements reduce automation success. Semantic HTML and good accessibility significantly improve results.'}\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/introduction/overview/page.tsx",
    "content": "import { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\nexport default function Overview() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<article>\n\t\t\t<div className=\"mb-8\">\n\t\t\t\t<h1 className=\"text-4xl font-bold mb-4\">Overview</h1>\n\t\t\t\t<p className=\"text-xl text-gray-600 dark:text-gray-300 mb-4 leading-relaxed\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? 'page-agent 是一个完全基于 Web 技术的 GUI Agent，简单几步，让你的网站拥有 AI 操作员。'\n\t\t\t\t\t\t: 'page-agent is a purely web-based GUI Agent. Gives your website an AI operator in simple steps.'}\n\t\t\t\t</p>\n\n\t\t\t\t{/* Status Badges */}\n\t\t\t\t<div className=\"flex flex-wrap gap-2 items-center\">\n\t\t\t\t\t<a href=\"https://opensource.org/licenses/MIT\" target=\"_blank\" rel=\"noopener noreferrer\">\n\t\t\t\t\t\t<img src=\"https://img.shields.io/badge/License-MIT-yellow.svg\" alt=\"MIT License\" />\n\t\t\t\t\t</a>\n\t\t\t\t\t<a href=\"http://www.typescriptlang.org/\" target=\"_blank\" rel=\"noopener noreferrer\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tsrc=\"https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg\"\n\t\t\t\t\t\t\talt=\"TypeScript\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</a>\n\t\t\t\t\t<a\n\t\t\t\t\t\thref=\"https://www.npmjs.com/package/page-agent\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<img src=\"https://img.shields.io/npm/dt/page-agent.svg\" alt=\"Downloads\" />\n\t\t\t\t\t</a>\n\t\t\t\t\t<a\n\t\t\t\t\t\thref=\"https://bundlephobia.com/package/page-agent\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<img src=\"https://img.shields.io/bundlephobia/minzip/page-agent\" alt=\"Bundle Size\" />\n\t\t\t\t\t</a>\n\t\t\t\t\t<a href=\"https://github.com/alibaba/page-agent\" target=\"_blank\" rel=\"noopener noreferrer\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tsrc=\"https://img.shields.io/github/stars/alibaba/page-agent.svg\"\n\t\t\t\t\t\t\talt=\"GitHub stars\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</a>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<section>\n\t\t\t\t<Heading id=\"what-is-page-agent\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t\t{isZh ? '什么是 page-agent？' : 'What is page-agent?'}\n\t\t\t\t</Heading>\n\n\t\t\t\t<p className=\"text-gray-600 dark:text-gray-300 mb-8 leading-relaxed \">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? 'page-agent 是一个页面内嵌式 GUI Agent。与传统的浏览器自动化工具不同，page-agent 面向网站开发者，而非爬虫或Agent客户端开发者；将 Agent 集成到你的网站中，让用户可以通过自然语言与页面进行交互。'\n\t\t\t\t\t\t: 'page-agent is an embedded GUI Agent. Unlike traditional browser automation tools, page-agent is built for web developers and web applications first. Integrate it into your site to let users interact with pages through natural language.'}\n\t\t\t\t</p>\n\t\t\t</section>\n\n\t\t\t<section>\n\t\t\t\t<Heading id=\"core-features\" className=\"text-2xl font-bold mb-3\">\n\t\t\t\t\t{isZh ? '核心特性' : 'Core Features'}\n\t\t\t\t</Heading>\n\n\t\t\t\t<div className=\"grid md:grid-cols-2 gap-4 mb-8\" role=\"list\">\n\t\t\t\t\t<div className=\"p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg\">\n\t\t\t\t\t\t<h3 className=\"text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300\">\n\t\t\t\t\t\t\t{isZh ? '🧠 智能 DOM 理解' : '🧠 Smart DOM Analysis'}\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<p className=\"text-gray-700 dark:text-gray-300\">\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '基于 DOM 分析，高强度脱水。无需视觉识别，纯文本实现精准操作。'\n\t\t\t\t\t\t\t\t: 'DOM-based analysis with high-intensity dehydration. No visual recognition needed. Pure text for fast and precise operations.'}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div className=\"p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg\">\n\t\t\t\t\t\t<h3 className=\"text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300\">\n\t\t\t\t\t\t\t{isZh ? '🔒 安全可控' : '🔒 Secure & Controllable'}\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<p className=\"text-gray-700 dark:text-gray-300\">\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '支持操作黑白名单、数据脱敏保护。注入自定义知识库，让 AI 按你的规则工作。'\n\t\t\t\t\t\t\t\t: 'Supports operation allowlists, data masking protection. Inject custom knowledge to make AI work by your rules.'}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div className=\"p-4 bg-green-50 dark:bg-green-900/20 rounded-lg\">\n\t\t\t\t\t\t<h3 className=\"text-lg font-semibold mb-2 text-green-900 dark:text-green-300\">\n\t\t\t\t\t\t\t{isZh ? '⚡ 零后端部署' : '⚡ Zero Backend'}\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<p className=\"text-gray-700 dark:text-gray-300\">\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? 'CDN 或 NPM 引入，自定义 LLM 接入点。'\n\t\t\t\t\t\t\t\t: 'CDN or NPM import with custom LLM endpoints.'}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div className=\"p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg\">\n\t\t\t\t\t\t<h3 className=\"text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300\">\n\t\t\t\t\t\t\t{isZh ? '♿ 普惠智能' : '♿ Accessible Intelligence'}\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<p className=\"text-gray-700 dark:text-gray-300\">\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '为复杂 B端系统、管理后台提供自然语言入口。让每个用户都能轻松上手。'\n\t\t\t\t\t\t\t\t: 'Provides natural language interface for complex B2B systems and admin panels. Makes software easy for everyone.'}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<Heading id=\"vs-browser-use\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t\t{isZh ? '与 browser-use 的区别' : 'vs. browser-use'}\n\t\t\t\t</Heading>\n\n\t\t\t\t<div className=\"overflow-x-auto mb-8\">\n\t\t\t\t\t<table className=\"w-full border-collapse border border-gray-300 dark:border-gray-600\">\n\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t<tr className=\"bg-gray-50 dark:bg-gray-800\">\n\t\t\t\t\t\t\t\t<th className=\"border border-gray-300 dark:border-gray-600 px-4 py-3 text-left\"></th>\n\t\t\t\t\t\t\t\t<th className=\"border border-gray-300 dark:border-gray-600 px-4 py-3 text-left\">\n\t\t\t\t\t\t\t\t\tpage-agent\n\t\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t\t<th className=\"border border-gray-300 dark:border-gray-600 px-4 py-3 text-left\">\n\t\t\t\t\t\t\t\t\tbrowser-use\n\t\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium\">\n\t\t\t\t\t\t\t\t\t{isZh ? '部署方式' : 'Deployment'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-3\">\n\t\t\t\t\t\t\t\t\t{isZh ? '页面内嵌组件' : 'Embedded component'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-3\">\n\t\t\t\t\t\t\t\t\t{isZh ? '外部工具' : 'External tool'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium\">\n\t\t\t\t\t\t\t\t\t{isZh ? '操作范围' : 'Scope'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-3\">\n\t\t\t\t\t\t\t\t\t{isZh ? '当前页面' : 'Current page'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-3\">\n\t\t\t\t\t\t\t\t\t{isZh ? '整个浏览器' : 'Entire browser'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium\">\n\t\t\t\t\t\t\t\t\t{isZh ? '目标用户' : 'Target Users'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-3\">\n\t\t\t\t\t\t\t\t\t{isZh ? '网站开发者' : 'Web developers'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-3\">\n\t\t\t\t\t\t\t\t\t{isZh ? '爬虫/Agent 开发者' : 'Scraper/Agent developers'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium\">\n\t\t\t\t\t\t\t\t\t{isZh ? '使用场景' : 'Use Case'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-3\">\n\t\t\t\t\t\t\t\t\t{isZh ? '用户体验增强' : 'UX enhancement'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t<td className=\"border border-gray-300 dark:border-gray-600 px-4 py-3\">\n\t\t\t\t\t\t\t\t\t{isZh ? '自动化任务' : 'Automation tasks'}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</tbody>\n\t\t\t\t\t</table>\n\t\t\t\t</div>\n\n\t\t\t\t<Heading id=\"use-cases\" className=\"text-2xl font-bold mb-4\">\n\t\t\t\t\t{isZh ? '应用场景' : 'Use Cases'}\n\t\t\t\t</Heading>\n\n\t\t\t\t<ul className=\"space-y-4 mb-8\">\n\t\t\t\t\t<li className=\"flex items-start space-x-3\">\n\t\t\t\t\t\t<span className=\"w-6 h-6 min-w-6 bg-blue-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0\">\n\t\t\t\t\t\t\t1\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<div className=\"text-gray-700 dark:text-gray-300\">\n\t\t\t\t\t\t\t<strong>{isZh ? '对接答疑机器人：' : 'Connect Support Bots:'}</strong>{' '}\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '把你的答疑助手变成全能Agent。客服机器人不再只说「请先点击设置按钮然后点击...」，而是直接帮用户现场操作。'\n\t\t\t\t\t\t\t\t: \"Turn your support assistant into a full agent. Customer service bots no longer just say 'Please click the settings button then click...'—they operate for users directly.\"}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</li>\n\t\t\t\t\t<li className=\"flex items-start space-x-3\">\n\t\t\t\t\t\t<span className=\"w-6 h-6 min-w-6 bg-green-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0\">\n\t\t\t\t\t\t\t2\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<div className=\"text-gray-700 dark:text-gray-300\">\n\t\t\t\t\t\t\t<strong>{isZh ? '交互升级/智能化改造：' : 'Modernize Legacy Apps:'}</strong>{' '}\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '一行代码，老应用变身Agent，产品专家帮用户操作复杂 B 端软件。降低人工支持成本，提高用户满意度。'\n\t\t\t\t\t\t\t\t: 'One line of code transforms old apps into agents. Product experts help users navigate complex B2B software. Reduce support costs and improve satisfaction.'}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</li>\n\t\t\t\t\t<li className=\"flex items-start space-x-3\">\n\t\t\t\t\t\t<span className=\"w-6 h-6 min-w-6 bg-purple-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0\">\n\t\t\t\t\t\t\t3\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<div className=\"text-gray-700 dark:text-gray-300\">\n\t\t\t\t\t\t\t<strong>{isZh ? '产品教学：' : 'Interactive Training:'}</strong>{' '}\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '向用户演示交互过程，边做边教。例如让AI演示「如何提交报销申请」的完整操作流程。'\n\t\t\t\t\t\t\t\t: \"Demonstrate workflows in real-time. Let AI show the complete process of 'how to submit an expense report.'\"}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</li>\n\t\t\t\t\t<li className=\"flex items-start space-x-3\">\n\t\t\t\t\t\t<span className=\"w-6 h-6 min-w-6 bg-orange-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0\">\n\t\t\t\t\t\t\t4\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<div className=\"text-gray-700 dark:text-gray-300\">\n\t\t\t\t\t\t\t<strong>{isZh ? '无障碍支持：' : 'Accessibility:'}</strong>{' '}\n\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t? '为视障用户、老年用户提供自然语言交互，对接屏幕阅读器或语音助理，让软件人人可用。'\n\t\t\t\t\t\t\t\t: 'Provide natural language interaction for visually impaired and elderly users. Connect screen readers or voice assistants to make software accessible to everyone.'}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\t\t</article>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/introduction/quick-start/page.tsx",
    "content": "import CodeEditor from '@/components/CodeEditor'\nimport { Heading } from '@/components/Heading'\nimport { CDN_DEMO_CN_URL, CDN_DEMO_URL } from '@/constants'\nimport { useLanguage } from '@/i18n/context'\n\nexport default function QuickStart() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<div>\n\t\t\t<h1 className=\"text-4xl font-bold mb-6\">Quick Start</h1>\n\n\t\t\t<p className=\" mb-6 leading-relaxed\">\n\t\t\t\t{isZh ? '几分钟内完成 page-agent 的集成。' : 'Integrate page-agent in minutes.'}\n\t\t\t</p>\n\n\t\t\t<Heading id=\"installation-steps\" className=\"text-2xl font-bold mb-3\">\n\t\t\t\t{isZh ? '安装步骤' : 'Installation Steps'}\n\t\t\t</Heading>\n\n\t\t\t<div className=\"space-y-4 mb-6\">\n\t\t\t\t{/* Demo CDN - One Line */}\n\t\t\t\t<div className=\"p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg\">\n\t\t\t\t\t<h3 className=\"text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300\">\n\t\t\t\t\t\t{isZh ? '🚀 快速体验（Demo CDN）' : '🚀 Quick Try (Demo CDN)'}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<div className=\"bg-yellow-50 dark:bg-yellow-900/20 p-2 rounded mb-3 text-sm\">\n\t\t\t\t\t\t<span className=\"text-yellow-800 dark:text-yellow-200\">\n\t\t\t\t\t\t\t⚠️{' '}\n\t\t\t\t\t\t\t{isZh ? (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t该 Demo CDN 使用了免费的测试 LLM API，使用即表示您同意其\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use\"\n\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"underline\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t使用条款\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\tThis demo CDN uses our free testing LLM API. By using it you agree to the{' '}\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use\"\n\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\t\t\tclassName=\"underline\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tTerms of Use\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tcode={`<script src=\"DEMO_CDN_URL\" crossorigin=\"true\"></script>`}\n\t\t\t\t\t\tlanguage=\"html\"\n\t\t\t\t\t/>\n\t\t\t\t\t<table className=\"w-full border-collapse text-sm\">\n\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t<tr className=\"border-b border-gray-200 dark:border-gray-700\">\n\t\t\t\t\t\t\t\t<th className=\"text-left py-2 px-3 font-semibold w-28\">\n\t\t\t\t\t\t\t\t\t{isZh ? '镜像' : 'Mirrors'}\n\t\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t\t<th className=\"text-left py-2 px-3 font-semibold\">URL</th>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t<tr className=\"border-b border-gray-100 dark:border-gray-800\">\n\t\t\t\t\t\t\t\t<td className=\"py-2 px-3\">{isZh ? '全球' : 'Global'}</td>\n\t\t\t\t\t\t\t\t<td className=\"py-2 px-3 font-mono text-xs break-all\">{CDN_DEMO_URL}</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td className=\"py-2 px-3\">{isZh ? '中国' : 'China'}</td>\n\t\t\t\t\t\t\t\t<td className=\"py-2 px-3 font-mono text-xs break-all\">{CDN_DEMO_CN_URL}</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</tbody>\n\t\t\t\t\t</table>\n\t\t\t\t</div>\n\n\t\t\t\t{/* NPM - Recommended */}\n\t\t\t\t<div className=\"p-4 bg-green-50 dark:bg-green-900/20 rounded-lg\">\n\t\t\t\t\t<h3 className=\"text-lg font-semibold mb-2 text-green-900 dark:text-green-300\">\n\t\t\t\t\t\t{isZh ? '📦 NPM 安装（推荐）' : '📦 NPM Install (Recommended)'}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tcode={`// npm install page-agent\n\nimport { PageAgent } from 'page-agent'`}\n\t\t\t\t\t\tlanguage=\"bash\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg\">\n\t\t\t\t\t<h3 className=\"text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300\">\n\t\t\t\t\t\t{isZh ? '2. 初始化配置' : '2. Initialize Configuration'}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tcode={`const agent = new PageAgent({\n  model: 'qwen3.5-plus',\n  baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n  apiKey: 'YOUR_API_KEY',\n  language: '${isZh ? 'zh-CN' : 'en-US'}'\n})`}\n\t\t\t\t\t\tlanguage=\"javascript\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg\">\n\t\t\t\t\t<h3 className=\"text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300\">\n\t\t\t\t\t\t{isZh ? '3. 开始使用' : '3. Start Using'}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<CodeEditor\n\t\t\t\t\t\tcode={`// ${isZh ? '程序化执行自然语言指令' : 'Execute natural language instructions programmatically'}\nawait agent.execute('${isZh ? '点击提交按钮，然后填写用户名为张三' : 'Click submit button, then fill username as John'}');\n\n// ${isZh ? '或者' : 'Or:'}\n// ${isZh ? '显示对话框让用户输入指令' : 'Show panel for user to input instructions'}\nagent.panel.show()\n`}\n\t\t\t\t\t\tlanguage=\"javascript\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/docs/introduction/troubleshooting/page.tsx",
    "content": "import { useEffect, useRef, useState } from 'react'\nimport { Link } from 'wouter'\n\nimport CodeEditor from '@/components/CodeEditor'\nimport { Heading } from '@/components/Heading'\nimport { useLanguage } from '@/i18n/context'\n\n// ---------------------------------------------------------------------------\n// Data: each section is a typed object for easy extension\n// ---------------------------------------------------------------------------\n\ninterface TroubleshootingSection {\n\tid: string\n\ttitle: { en: string; zh: string }\n\tsymptom: { en: string; zh: string }\n\tcolor: 'red' | 'amber' | 'orange' | 'violet'\n\tcontent: (isZh: boolean) => React.ReactNode\n}\n\nconst SECTIONS: TroubleshootingSection[] = [\n\t{\n\t\tid: 'format-errors',\n\t\ttitle: { en: 'Model Response Format Errors', zh: '模型返回格式错误' },\n\t\tsymptom: {\n\t\t\ten: 'The model returns malformed tool calls, plain text, or unexpected JSON instead of structured actions.',\n\t\t\tzh: '模型返回了格式错误的 tool call、纯文本或非预期的 JSON，而非结构化的操作指令。',\n\t\t},\n\t\tcolor: 'amber',\n\t\tcontent: FormatErrorsContent,\n\t},\n\t{\n\t\tid: 'low-success-rate',\n\t\ttitle: { en: 'Low Task Success Rate', zh: '任务成功率低' },\n\t\tsymptom: {\n\t\t\ten: 'The agent appears to understand the task but frequently fails to complete it, or produces incorrect results.',\n\t\t\tzh: 'Agent 似乎理解了任务，但频繁执行失败或产生不正确的结果。',\n\t\t},\n\t\tcolor: 'amber',\n\t\tcontent: LowSuccessRateContent,\n\t},\n\t{\n\t\tid: 'wrong-element',\n\t\ttitle: { en: \"Can't Hit Target Elements\", zh: '无法点击目标元素' },\n\t\tsymptom: {\n\t\t\ten: 'The agent repeatedly retries but keeps interacting with the wrong element, or fails to locate the correct one.',\n\t\t\tzh: 'Agent 反复重试，但始终点击在错误的元素上，或无法定位到正确的目标元素。',\n\t\t},\n\t\tcolor: 'amber',\n\t\tcontent: WrongElementContent,\n\t},\n\t{\n\t\tid: 'api-errors',\n\t\ttitle: { en: 'API Request Errors', zh: 'API 请求错误' },\n\t\tsymptom: {\n\t\t\ten: 'HTTP 400 Bad Request or similar errors when calling the LLM API.',\n\t\t\tzh: '调用 LLM API 时出现 HTTP 400 Bad Request 或类似的参数错误。',\n\t\t},\n\t\tcolor: 'amber',\n\t\tcontent: ApiErrorsContent,\n\t},\n]\n\n// ---------------------------------------------------------------------------\n// Section content components\n// ---------------------------------------------------------------------------\n\nfunction FormatErrorsContent(isZh: boolean) {\n\treturn (\n\t\t<ol className=\"list-decimal pl-5 space-y-4 text-gray-700 dark:text-gray-300\">\n\t\t\t<li>\n\t\t\t\t<strong>{isZh ? '确认模型是否支持' : 'Verify model compatibility'}</strong>\n\t\t\t\t<p className=\"mt-1\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '并非所有模型都能正确处理 page-agent 的 tool 定义。请查看'\n\t\t\t\t\t\t: 'Not all models can handle page-agent tool definitions correctly. Check the '}\n\t\t\t\t\t<Link\n\t\t\t\t\t\thref=\"/features/models\"\n\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 underline underline-offset-2\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{isZh ? '已测试模型列表' : 'tested models list'}\n\t\t\t\t\t</Link>\n\t\t\t\t\t{isZh ? '。' : '.'}\n\t\t\t\t</p>\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\t<strong>\n\t\t\t\t\t{isZh ? '检查代理/网关的参数转发' : 'Check proxy/gateway parameter forwarding'}\n\t\t\t\t</strong>\n\t\t\t\t<p className=\"mt-1\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '如果使用了 API 代理或网关，请确保请求中的 '\n\t\t\t\t\t\t: 'If using an API proxy or gateway, make sure the '}\n\t\t\t\t\t<code>tools</code>\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? ' 字段被完整、无修改地转发给模型供应商。部分代理可能会剥离或修改此字段。'\n\t\t\t\t\t\t: ' parameter is forwarded to the model provider intact. Some proxies may strip or alter this field.'}\n\t\t\t\t</p>\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\t<strong>{isZh ? '寻求社区帮助' : 'Get community help'}</strong>\n\t\t\t\t<p className=\"mt-1\">\n\t\t\t\t\t{isZh ? (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t如果以上步骤无法解决问题，欢迎在{' '}\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/discussions\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 underline underline-offset-2\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tGitHub Discussions\n\t\t\t\t\t\t\t</a>{' '}\n\t\t\t\t\t\t\t中反馈，附上模型名称和错误信息。\n\t\t\t\t\t\t</>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\tIf the above steps don't help, join the{' '}\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/discussions\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 underline underline-offset-2\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tGitHub Discussions\n\t\t\t\t\t\t\t</a>{' '}\n\t\t\t\t\t\t\twith your model name and error details.\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\t\t\t\t</p>\n\t\t\t</li>\n\t\t</ol>\n\t)\n}\n\nfunction LowSuccessRateContent(isZh: boolean) {\n\treturn (\n\t\t<>\n\t\t\t<p className=\"text-sm text-gray-500 dark:text-gray-400 mb-4 italic\">\n\t\t\t\t{isZh\n\t\t\t\t\t? '按以下顺序逐步排查，从最简单的情况开始：'\n\t\t\t\t\t: 'Follow this diagnostic funnel from simplest to most advanced:'}\n\t\t\t</p>\n\t\t\t<ol className=\"list-decimal pl-5 space-y-4 text-gray-700 dark:text-gray-300\">\n\t\t\t\t<li>\n\t\t\t\t\t<strong>{isZh ? '先从简单指令开始' : 'Start with a simple instruction'}</strong>\n\t\t\t\t\t<p className=\"mt-1\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '给一个具体的、单步的简单指令（如\"点击登录按钮\"），看 Agent 能否完成。如果连简单操作都失败了，问题可能不在模型能力上。'\n\t\t\t\t\t\t\t: 'Give a concrete, single-step instruction (e.g. \"click the login button\"). If even simple actions fail, the issue is likely not model capability.'}\n\t\t\t\t\t</p>\n\t\t\t\t</li>\n\t\t\t\t<li>\n\t\t\t\t\t<strong>{isZh ? '尝试最强模型' : 'Try the strongest model available'}</strong>\n\t\t\t\t\t<p className=\"mt-1\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '切换到你能获取到的最先进、最大的模型，以排除是否是模型智能水平不足导致的问题。'\n\t\t\t\t\t\t\t: \"Switch to the most capable model you have access to, to isolate whether it's a model intelligence issue.\"}\n\t\t\t\t\t</p>\n\t\t\t\t</li>\n\t\t\t\t<li>\n\t\t\t\t\t<strong>{isZh ? '优化指令质量' : 'Improve instruction quality'}</strong>\n\t\t\t\t\t<p className=\"mt-1\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '给出尽可能具体的指令。对于复杂任务，建议使用另一个 LLM 来预先拆分和细化用户的需求，然后逐步执行。'\n\t\t\t\t\t\t\t: \"Be as specific as possible. For complex tasks, consider using another LLM to decompose and refine the user's request before execution.\"}\n\t\t\t\t\t</p>\n\t\t\t\t</li>\n\t\t\t\t<li>\n\t\t\t\t\t<strong>{isZh ? '提供充足的上下文' : 'Provide sufficient context'}</strong>\n\t\t\t\t\t<p className=\"mt-1\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '通过 instructions 配置注入网站背景描述、关键术语解释等上下文信息，帮助 Agent 更好地理解页面。'\n\t\t\t\t\t\t\t: 'Use the instructions config to inject website descriptions, key terminology, and background context to help the agent understand the page.'}\n\t\t\t\t\t</p>\n\t\t\t\t</li>\n\t\t\t\t<li>\n\t\t\t\t\t<strong>{isZh ? '检查 HTML 清洗结果' : 'Check HTML sanitization output'}</strong>\n\t\t\t\t\t<p className=\"mt-1\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '使用开发者工具检查清洗后的 HTML，确认关键信息、文本和可操作元素是否被正确保留。'\n\t\t\t\t\t\t\t: 'Inspect the sanitized HTML in dev tools to confirm that key information, text, and interactive elements are preserved correctly.'}\n\t\t\t\t\t</p>\n\t\t\t\t</li>\n\t\t\t</ol>\n\t\t</>\n\t)\n}\n\nfunction WrongElementContent(isZh: boolean) {\n\treturn (\n\t\t<ol className=\"list-decimal pl-5 space-y-4 text-gray-700 dark:text-gray-300\">\n\t\t\t<li>\n\t\t\t\t<strong>{isZh ? '了解现实局限' : 'Understand the reality'}</strong>\n\t\t\t\t<p className=\"mt-1\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '并非所有网站都提供了完善的语义化 HTML 和 accessibility 标签。对于此类网站，DOM 清洗可能无法产出足够好的结果。'\n\t\t\t\t\t\t: 'Not all websites provide proper semantic HTML and accessibility labels. For such sites, DOM sanitization may not produce good enough results.'}\n\t\t\t\t</p>\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\t<strong>{isZh ? '检查目标元素类型' : 'Check target element type'}</strong>\n\t\t\t\t<p className=\"mt-1\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '确认目标元素是否为图片、Canvas、或需要复杂交互（如拖拽、基于坐标的点击）的元素。这些本身就超出了当前的能力范围。'\n\t\t\t\t\t\t: 'Verify if the target is an image, Canvas, or requires complex interactions (drag-and-drop, coordinate-based clicking). These are beyond current capabilities.'}\n\t\t\t\t</p>\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\t<strong>{isZh ? '检查清洗后的 HTML' : 'Inspect sanitized HTML'}</strong>\n\t\t\t\t<p className=\"mt-1\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '检查清洗结果中是否存在关键信息丢失、可操作元素未被编号等问题。'\n\t\t\t\t\t\t: 'Look for missing key information or unnumbered interactive elements in the sanitized output.'}\n\t\t\t\t</p>\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\t<strong>{isZh ? '注入 accessibility 增强' : 'Inject accessibility improvements'}</strong>\n\t\t\t\t<p className=\"mt-1\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '通过注入脚本为网站添加 aria-label、语义化标签等 accessibility 属性，改善 DOM 清洗质量。'\n\t\t\t\t\t\t: 'Inject scripts to add aria-labels, semantic attributes, and other a11y improvements to enhance DOM sanitization quality.'}\n\t\t\t\t</p>\n\t\t\t</li>\n\t\t\t<li>\n\t\t\t\t<strong>{isZh ? '开发专用 Tool' : 'Build a custom Tool'}</strong>\n\t\t\t\t<p className=\"mt-1\">\n\t\t\t\t\t{isZh ? (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t对于特定的、持续难以操作的元素，考虑开发{' '}\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\thref=\"/features/custom-tools\"\n\t\t\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 underline underline-offset-2\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t自定义 Tool\n\t\t\t\t\t\t\t</Link>{' '}\n\t\t\t\t\t\t\t来直接操作这些元素。\n\t\t\t\t\t\t</>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\tFor consistently difficult elements, consider building a{' '}\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\thref=\"/features/custom-tools\"\n\t\t\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 underline underline-offset-2\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tcustom Tool\n\t\t\t\t\t\t\t</Link>{' '}\n\t\t\t\t\t\t\tto interact with them directly.\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\t\t\t\t</p>\n\t\t\t</li>\n\t\t</ol>\n\t)\n}\n\nfunction ApiErrorsContent(isZh: boolean) {\n\treturn (\n\t\t<div className=\"space-y-4 text-gray-700 dark:text-gray-300\">\n\t\t\t<p>\n\t\t\t\t{isZh\n\t\t\t\t\t? '一些 LLM 供应商使用了与 OpenAI 不完全兼容的参数格式，导致请求参数校验失败。'\n\t\t\t\t\t: 'Some LLM providers use parameter formats that are not fully compatible with the OpenAI spec, causing request validation failures.'}\n\t\t\t</p>\n\t\t\t<div className=\"bg-gray-50 dark:bg-gray-800/50 rounded-lg p-4\">\n\t\t\t\t<p className=\"font-medium mb-2\">\n\t\t\t\t\t{isZh ? '解决方案：使用 customFetch' : 'Solution: use customFetch'}\n\t\t\t\t</p>\n\t\t\t\t<p className=\"text-sm mb-3\">\n\t\t\t\t\t{isZh\n\t\t\t\t\t\t? '通过 customFetch 配置拦截请求，在发送前调整参数格式以适配目标供应商的要求。'\n\t\t\t\t\t\t: 'Use the customFetch config to intercept requests and adapt parameters before sending them to the target provider.'}\n\t\t\t\t</p>\n\t\t\t\t<CodeEditor\n\t\t\t\t\tcode={`const agent = new PageAgent({\n  // ...\n  customFetch: async (url, init) => {\n    const body = JSON.parse(init.body)\n    // Adapt parameters for your provider\n    delete body.stream_options\n    return fetch(url, { ...init, body: JSON.stringify(body) })\n  },\n})`}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t<p className=\"text-sm\">\n\t\t\t\t{isZh ? '参见 ' : 'See '}\n\t\t\t\t<Link\n\t\t\t\t\thref=\"/advanced/page-agent-core\"\n\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 underline underline-offset-2\"\n\t\t\t\t>\n\t\t\t\t\tPageAgentCore API\n\t\t\t\t</Link>\n\t\t\t\t{isZh ? ' 了解 customFetch 的完整用法。' : ' for full customFetch documentation.'}\n\t\t\t</p>\n\t\t</div>\n\t)\n}\n\n// ---------------------------------------------------------------------------\n// Color mapping for symptom callouts\n// ---------------------------------------------------------------------------\n\nconst SYMPTOM_COLORS = {\n\tred: 'border-red-400 bg-red-50 dark:bg-red-900/15 text-red-800 dark:text-red-200',\n\tamber: 'border-amber-400 bg-amber-50 dark:bg-amber-900/15 text-amber-800 dark:text-amber-200',\n\torange:\n\t\t'border-orange-400 bg-orange-50 dark:bg-orange-900/15 text-orange-800 dark:text-orange-200',\n\tviolet:\n\t\t'border-violet-400 bg-violet-50 dark:bg-violet-900/15 text-violet-800 dark:text-violet-200',\n} as const\n\n// ---------------------------------------------------------------------------\n// Right-side TOC with IntersectionObserver\n// ---------------------------------------------------------------------------\n\nfunction useActiveSection(ids: string[]) {\n\tconst [activeId, setActiveId] = useState(ids[0])\n\tconst observerRef = useRef<IntersectionObserver | null>(null)\n\n\tuseEffect(() => {\n\t\tobserverRef.current?.disconnect()\n\n\t\tconst visibleEntries = new Map<string, number>()\n\n\t\tobserverRef.current = new IntersectionObserver(\n\t\t\t(entries) => {\n\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\tif (entry.isIntersecting) {\n\t\t\t\t\t\tvisibleEntries.set(entry.target.id, entry.intersectionRatio)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvisibleEntries.delete(entry.target.id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Pick the first visible section in document order\n\t\t\t\tconst firstVisible = ids.find((id) => visibleEntries.has(id))\n\t\t\t\tif (firstVisible) setActiveId(firstVisible)\n\t\t\t},\n\t\t\t{ rootMargin: '-80px 0px -60% 0px', threshold: [0, 0.25] }\n\t\t)\n\n\t\tfor (const id of ids) {\n\t\t\tconst el = document.getElementById(id)\n\t\t\tif (el) observerRef.current.observe(el)\n\t\t}\n\n\t\treturn () => observerRef.current?.disconnect()\n\t}, [ids])\n\n\treturn activeId\n}\n\n// ---------------------------------------------------------------------------\n// Page component\n// ---------------------------------------------------------------------------\n\nexport default function TroubleshootingPage() {\n\tconst { isZh } = useLanguage()\n\tconst sectionIds = SECTIONS.map((s) => s.id)\n\tconst activeId = useActiveSection(sectionIds)\n\n\treturn (\n\t\t<div className=\"max-w-5xl mx-auto\">\n\t\t\t{/* Header */}\n\t\t\t<div className=\"mb-10\">\n\t\t\t\t<h1 className=\"text-4xl font-bold mb-4 text-gray-900 dark:text-white\">Troubleshooting</h1>\n\t\t\t</div>\n\n\t\t\t{/* Two-column: content + TOC */}\n\t\t\t<div className=\"flex gap-8\">\n\t\t\t\t{/* Main content */}\n\t\t\t\t<div className=\"flex-1 min-w-0 space-y-12\">\n\t\t\t\t\t{SECTIONS.map((section) => (\n\t\t\t\t\t\t<section key={section.id} className=\"scroll-mt-24\">\n\t\t\t\t\t\t\t<Heading\n\t\t\t\t\t\t\t\tid={section.id}\n\t\t\t\t\t\t\t\tclassName=\"text-2xl font-bold mb-4 text-gray-900 dark:text-white\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{isZh ? section.title.zh : section.title.en}\n\t\t\t\t\t\t\t</Heading>\n\n\t\t\t\t\t\t\t{/* Symptom callout */}\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclassName={`border-l-4 px-4 py-3 rounded-r-lg mb-6 ${SYMPTOM_COLORS[section.color]}`}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<span className=\"text-xs font-semibold uppercase tracking-wider opacity-70\">\n\t\t\t\t\t\t\t\t\t{isZh ? '症状' : 'Symptom'}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<p className=\"mt-1 text-sm\">{isZh ? section.symptom.zh : section.symptom.en}</p>\n\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t{/* Diagnostic steps */}\n\t\t\t\t\t\t\t<div className=\"prose-sm\">{section.content(isZh)}</div>\n\t\t\t\t\t\t</section>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\n\t\t\t\t{/* Right TOC */}\n\t\t\t\t<aside className=\"hidden lg:block w-48 shrink-0\">\n\t\t\t\t\t<div className=\"sticky top-24\">\n\t\t\t\t\t\t<h4 className=\"text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-3\">\n\t\t\t\t\t\t\t{isZh ? '目录' : 'On this page'}\n\t\t\t\t\t\t</h4>\n\t\t\t\t\t\t<nav className=\"space-y-1\">\n\t\t\t\t\t\t\t{SECTIONS.map((section) => (\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\tkey={section.id}\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tonClick={() =>\n\t\t\t\t\t\t\t\t\t\tdocument\n\t\t\t\t\t\t\t\t\t\t\t.getElementById(section.id)\n\t\t\t\t\t\t\t\t\t\t\t?.scrollIntoView({ behavior: 'smooth', block: 'start' })\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tclassName={`block cursor-pointer py-1 text-left text-sm transition-colors ${\n\t\t\t\t\t\t\t\t\t\tactiveId === section.id\n\t\t\t\t\t\t\t\t\t\t\t? 'text-blue-600 dark:text-blue-400 font-medium'\n\t\t\t\t\t\t\t\t\t\t\t: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'\n\t\t\t\t\t\t\t\t\t}`}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{isZh ? section.title.zh : section.title.en}\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</nav>\n\t\t\t\t\t</div>\n\t\t\t\t</aside>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/home/FeaturesSection.tsx",
    "content": "import { Bot, Box, MessageSquare, Shield, Sparkles, Users } from 'lucide-react'\n\nimport { BlurFade } from '../../components/ui/blur-fade'\nimport { Highlighter } from '../../components/ui/highlighter'\nimport { MagicCard } from '../../components/ui/magic-card'\nimport { Particles } from '../../components/ui/particles'\nimport { useLanguage } from '../../i18n/context'\n\n// Word-cloud style: each item has a position (%), size, opacity, and color for a scattered look\nconst LLM_CLOUD: {\n\tname: string\n\tcolor: string\n\tx: number\n\ty: number\n\tsize: number\n\topacity: number\n}[] = [\n\t{ name: 'OpenAI', color: '#10b981', x: 18, y: 22, size: 1.5, opacity: 1 },\n\t{ name: 'Claude', color: '#f97316', x: 62, y: 15, size: 1.35, opacity: 0.95 },\n\t{ name: 'Qwen', color: '#8b5cf6', x: 38, y: 50, size: 1.8, opacity: 0.9 },\n\t{ name: 'Gemini', color: '#3b82f6', x: 68, y: 48, size: 1.2, opacity: 0.85 },\n\t{ name: 'DeepSeek', color: '#06b6d4', x: 10, y: 65, size: 1.1, opacity: 0.8 },\n\t{ name: 'Grok', color: '#f43f5e', x: 52, y: 78, size: 1.0, opacity: 0.75 },\n\t{ name: 'Ollama', color: '#9ca3af', x: 82, y: 25, size: 1.1, opacity: 0.8 },\n\t{ name: 'Kimi', color: '#14b8a6', x: 30, y: 82, size: 0.85, opacity: 0.6 },\n\t{ name: 'GLM', color: '#f59e0b', x: 70, y: 72, size: 0.85, opacity: 0.55 },\n\t{ name: 'LLaMA', color: '#60a5fa', x: 88, y: 70, size: 0.8, opacity: 0.45 },\n]\n\nconst CARD_HEIGHT = 'h-72'\n\nexport default function FeaturesSection() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<section className=\"px-6 py-14\" aria-labelledby=\"features-heading\">\n\t\t\t<div className=\"max-w-6xl mx-auto\">\n\t\t\t\t<div className=\"grid grid-cols-1 md:grid-cols-3 gap-4 auto-rows-[18rem]\">\n\t\t\t\t\t{/* Row 1: Zero Infrastructure (2col) + Privacy (1col) */}\n\t\t\t\t\t<BlurFade inView className=\"col-span-1 md:col-span-2\">\n\t\t\t\t\t\t<MagicCard\n\t\t\t\t\t\t\tclassName=\"h-full rounded-2xl\"\n\t\t\t\t\t\t\tgradientFrom=\"#3b82f6\"\n\t\t\t\t\t\t\tgradientTo=\"#06b6d4\"\n\t\t\t\t\t\t\tgradientColor=\"#3b82f6\"\n\t\t\t\t\t\t\tgradientOpacity={0.15}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div className={`flex ${CARD_HEIGHT} flex-col`}>\n\t\t\t\t\t\t\t\t<div className=\"flex-1 p-7 flex flex-col justify-center\">\n\t\t\t\t\t\t\t\t\t<div className=\"space-y-2.5 mb-5\">\n\t\t\t\t\t\t\t\t\t\t{[\n\t\t\t\t\t\t\t\t\t\t\t'pip install browser-use playwright',\n\t\t\t\t\t\t\t\t\t\t\t'docker run -p 3000:3000 playwright-mcp',\n\t\t\t\t\t\t\t\t\t\t\t'const browser = await chromium.launch()',\n\t\t\t\t\t\t\t\t\t\t].map((cmd) => (\n\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\tkey={cmd}\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"font-mono text-sm text-white-400 dark:text-gray-300 truncate\"\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<Highlighter\n\t\t\t\t\t\t\t\t\t\t\t\t\taction=\"strike-through\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tcolor=\"#ef4444aa\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tstrokeWidth={1.5}\n\t\t\t\t\t\t\t\t\t\t\t\t\t// multiline={false}\n\t\t\t\t\t\t\t\t\t\t\t\t\t// isView\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{cmd}\n\t\t\t\t\t\t\t\t\t\t\t\t</Highlighter>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200/60 dark:border-emerald-700/40 rounded-xl px-5 py-3 font-mono text-sm text-emerald-700 dark:text-emerald-400 flex items-center gap-2.5\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-emerald-500 text-xs shrink-0\">✓</span>\n\t\t\t\t\t\t\t\t\t\t{'<script src=\"page-agent.js\"></script>'}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div className=\"px-7 pb-5\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2.5 mb-1\">\n\t\t\t\t\t\t\t\t\t\t<Box className=\"w-5 h-5 text-blue-500\" />\n\t\t\t\t\t\t\t\t\t\t<h3 className=\"font-semibold text-lg text-gray-900 dark:text-white\">\n\t\t\t\t\t\t\t\t\t\t\t{isZh ? '零基建集成' : 'Zero Infrastructure'}\n\t\t\t\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-gray-500 dark:text-gray-300 leading-relaxed\">\n\t\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t\t? '无需 Python、无头浏览器、服务端部署。一行 script 标签搞定。'\n\t\t\t\t\t\t\t\t\t\t\t: \"No Python. No headless browser. No server. One script tag — that's it.\"}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</MagicCard>\n\t\t\t\t\t</BlurFade>\n\n\t\t\t\t\t<BlurFade inView delay={0.1} className=\"col-span-1\">\n\t\t\t\t\t\t<MagicCard\n\t\t\t\t\t\t\tclassName=\"h-full rounded-2xl\"\n\t\t\t\t\t\t\tgradientFrom=\"#8b5cf6\"\n\t\t\t\t\t\t\tgradientTo=\"#a855f7\"\n\t\t\t\t\t\t\tgradientColor=\"#8b5cf6\"\n\t\t\t\t\t\t\tgradientOpacity={0.12}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div className={`flex ${CARD_HEIGHT} flex-col`}>\n\t\t\t\t\t\t\t\t<div className=\"flex-1 relative overflow-hidden\">\n\t\t\t\t\t\t\t\t\t<Particles\n\t\t\t\t\t\t\t\t\t\tclassName=\"absolute inset-0\"\n\t\t\t\t\t\t\t\t\t\tquantity={40}\n\t\t\t\t\t\t\t\t\t\tstaticity={50}\n\t\t\t\t\t\t\t\t\t\tease={80}\n\t\t\t\t\t\t\t\t\t\tcolor=\"#8b5cf6\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<div className=\"absolute inset-0 flex items-center justify-center\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"w-16 h-16 rounded-2xl bg-purple-500/10 dark:bg-purple-500/20 backdrop-blur-sm flex items-center justify-center ring-1 ring-purple-500/20\">\n\t\t\t\t\t\t\t\t\t\t\t<Shield className=\"w-8 h-8 text-purple-500\" strokeWidth={1.5} />\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div className=\"px-6 pb-5\">\n\t\t\t\t\t\t\t\t\t<h3 className=\"font-semibold text-lg text-gray-900 dark:text-white mb-1\">\n\t\t\t\t\t\t\t\t\t\t{isZh ? '隐私优先' : 'Privacy by Default'}\n\t\t\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-gray-500 dark:text-gray-300 leading-relaxed\">\n\t\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t\t? '浏览器内运行，数据完全由你掌控。'\n\t\t\t\t\t\t\t\t\t\t\t: 'Runs in the browser. You control your data, always.'}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</MagicCard>\n\t\t\t\t\t</BlurFade>\n\n\t\t\t\t\t{/* Row 2: Human-in-the-Loop (1col) + LLM (2col) */}\n\t\t\t\t\t<BlurFade inView delay={0.15} className=\"col-span-1\">\n\t\t\t\t\t\t<MagicCard\n\t\t\t\t\t\t\tclassName=\"h-full rounded-2xl\"\n\t\t\t\t\t\t\tgradientFrom=\"#3b82f6\"\n\t\t\t\t\t\t\tgradientTo=\"#8b5cf6\"\n\t\t\t\t\t\t\tgradientColor=\"#6366f1\"\n\t\t\t\t\t\t\tgradientOpacity={0.12}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div className={`flex ${CARD_HEIGHT} flex-col`}>\n\t\t\t\t\t\t\t\t<div className=\"flex-1 p-5 flex flex-col justify-center max-w-xs mx-auto w-full\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex gap-2 mb-2.5\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"shrink-0 w-6 h-6 rounded-full bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center\">\n\t\t\t\t\t\t\t\t\t\t\t<Bot className=\"w-3.5 h-3.5 text-purple-600 dark:text-purple-400\" />\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div className=\"bg-gray-100 dark:bg-white/10 rounded-2xl rounded-tl-md px-3.5 py-2 text-sm text-gray-700 dark:text-gray-200\">\n\t\t\t\t\t\t\t\t\t\t\t{isZh ? '找到 3 条匹配记录。选择哪一条？' : 'Found 3 matches. Which one?'}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"flex gap-2 justify-end mb-2.5\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"bg-blue-500 rounded-2xl rounded-tr-md px-3.5 py-2 text-sm text-white\">\n\t\t\t\t\t\t\t\t\t\t\t{isZh ? '第二条' : 'The second one.'}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div className=\"shrink-0 w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center\">\n\t\t\t\t\t\t\t\t\t\t\t<Users className=\"w-3.5 h-3.5 text-blue-600 dark:text-blue-400\" />\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"shrink-0 w-6 h-6 rounded-full bg-emerald-100 dark:bg-emerald-900/50 flex items-center justify-center text-emerald-600 dark:text-emerald-400 text-xs font-bold\">\n\t\t\t\t\t\t\t\t\t\t\t✓\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div className=\"bg-gray-100 dark:bg-white/10 rounded-2xl rounded-tl-md px-3.5 py-2 text-sm text-gray-700 dark:text-gray-200\">\n\t\t\t\t\t\t\t\t\t\t\t{isZh ? '已选择并提交！' : 'Done! Selected and submitted.'}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div className=\"px-5 pb-5\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2.5 mb-1\">\n\t\t\t\t\t\t\t\t\t\t<MessageSquare className=\"w-5 h-5 text-blue-500\" />\n\t\t\t\t\t\t\t\t\t\t<h3 className=\"font-semibold text-lg text-gray-900 dark:text-white\">\n\t\t\t\t\t\t\t\t\t\t\t{isZh ? '人机协同' : 'Human-in-the-Loop'}\n\t\t\t\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-gray-500 dark:text-gray-300 leading-relaxed\">\n\t\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t\t? '内置协作面板，AI 操作前先确认——不是盲目自动化。'\n\t\t\t\t\t\t\t\t\t\t\t: 'Built-in collaborative panel. Agent asks before acting — not blind automation.'}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</MagicCard>\n\t\t\t\t\t</BlurFade>\n\n\t\t\t\t\t<BlurFade inView delay={0.2} className=\"col-span-1 md:col-span-2\">\n\t\t\t\t\t\t<MagicCard\n\t\t\t\t\t\t\tclassName=\"h-full rounded-2xl\"\n\t\t\t\t\t\t\tgradientFrom=\"#f59e0b\"\n\t\t\t\t\t\t\tgradientTo=\"#f97316\"\n\t\t\t\t\t\t\tgradientColor=\"#f59e0b\"\n\t\t\t\t\t\t\tgradientOpacity={0.12}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div className={`flex ${CARD_HEIGHT} flex-col`}>\n\t\t\t\t\t\t\t\t<div className=\"flex-1 overflow-hidden relative\">\n\t\t\t\t\t\t\t\t\t{LLM_CLOUD.map((item) => (\n\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\tkey={item.name}\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"absolute font-semibold whitespace-nowrap select-none\"\n\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\tleft: `${item.x}%`,\n\t\t\t\t\t\t\t\t\t\t\t\ttop: `${item.y}%`,\n\t\t\t\t\t\t\t\t\t\t\t\tfontSize: `${item.size}rem`,\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: item.color,\n\t\t\t\t\t\t\t\t\t\t\t\topacity: item.opacity,\n\t\t\t\t\t\t\t\t\t\t\t\ttransform: 'translate(-50%, -50%)',\n\t\t\t\t\t\t\t\t\t\t\t\ttextShadow: `0 0 80px ${item.color}99`,\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{item.name}\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div className=\"px-7 pb-5\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2.5 mb-1\">\n\t\t\t\t\t\t\t\t\t\t<Sparkles className=\"w-5 h-5 text-amber-500\" />\n\t\t\t\t\t\t\t\t\t\t<h3 className=\"font-semibold text-lg text-gray-900 dark:text-white\">\n\t\t\t\t\t\t\t\t\t\t\t{isZh ? '兼容多种 LLM' : 'Bring Your Own LLMs'}\n\t\t\t\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-gray-500 dark:text-gray-300 leading-relaxed\">\n\t\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t\t? 'OpenAI、Claude、DeepSeek、Qwen 等，或通过 Ollama 完全离线。'\n\t\t\t\t\t\t\t\t\t\t\t: 'OpenAI, Claude, DeepSeek, Qwen, and more — or fully offline via Ollama.'}\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</MagicCard>\n\t\t\t\t\t</BlurFade>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</section>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/home/HeroSection.tsx",
    "content": "/* eslint-disable react-dom/no-dangerously-set-innerhtml */\nimport type { PageAgent as PageAgentType } from 'page-agent'\nimport { useEffect, useState } from 'react'\nimport { Link, useSearchParams } from 'wouter'\n\nimport { AnimatedGradientText } from '../../components/ui/animated-gradient-text'\nimport { Highlighter } from '../../components/ui/highlighter'\nimport { NeonGradientCard } from '../../components/ui/neon-gradient-card'\nimport { Particles } from '../../components/ui/particles'\nimport {\n\tCDN_DEMO_CN_URL,\n\tCDN_DEMO_URL,\n\t// DEMO_API_KEY,\n\tDEMO_BASE_URL,\n\tDEMO_MODEL,\n} from '../../constants'\nimport { useLanguage } from '../../i18n/context'\n\nlet pageAgentModule: Promise<typeof import('page-agent')> | null = null\n\nfunction getInjection(useCN?: boolean) {\n\tconst cdn = useCN ? CDN_DEMO_CN_URL : CDN_DEMO_URL\n\n\tconst injection = encodeURI(\n\t\t`javascript:(function(){var s=document.createElement('script');s.src=\\`${cdn}?t=\\${Math.random()}\\`;s.setAttribute('crossorigin', true);s.type=\"text/javascript\";s.onload=()=>console.log('PageAgent script loaded!');document.body.appendChild(s);})();`\n\t)\n\n\treturn `\n\t<a\n\t\thref=${injection}\n\t\tclass=\"inline-flex items-center text-xs px-3 py-2 bg-blue-500 text-white font-medium rounded-lg hover:shadow-md transform hover:scale-105 transition-all duration-200 cursor-move border-2 border-dashed border-green-300\"\n\t\tdraggable=\"true\"\n\t\tonclick=\"return false;\"\n\t\ttitle=\"Drag me to your bookmarks bar!\"\n\t>\n\t\t✨PageAgent\n\t</a>\n\t`\n}\n\nexport default function HeroSection() {\n\tconst { language, isZh } = useLanguage()\n\n\tconst defaultTask = isZh\n\t\t? '从导航栏中进入文档页，打开\"快速开始\"相关的文档，帮我总结成 markdown'\n\t\t: 'Goto docs in navigation bar, find Quick-Start section, and summarize in markdown'\n\n\tconst [task, setTask] = useState(() => defaultTask)\n\n\tuseEffect(() => {\n\t\tsetTask(defaultTask)\n\t}, [defaultTask])\n\n\tconst [params] = useSearchParams()\n\tconst isOther = params.has('try_other')\n\n\tconst [activeTab, setActiveTab] = useState<'try' | 'other'>(isOther ? 'other' : 'try')\n\tconst [cdnSource, setCdnSource] = useState<'international' | 'china'>('international')\n\n\tconst [ready, setReady] = useState(false)\n\tuseEffect(() => {\n\t\tpageAgentModule ??= import('page-agent')\n\t\tpageAgentModule.then(() => setReady(true))\n\t}, [])\n\n\tconst handleExecute = async () => {\n\t\tif (!task.trim() || !ready || !pageAgentModule) return\n\n\t\tconst { PageAgent } = await pageAgentModule\n\t\tconst win = window as any\n\n\t\tif (!win.pageAgent || win.pageAgent.disposed) {\n\t\t\twin.pageAgent = new (PageAgent as typeof PageAgentType)({\n\t\t\t\tinteractiveBlacklist: [document.getElementById('root')!],\n\t\t\t\tlanguage: language,\n\n\t\t\t\tinstructions: {\n\t\t\t\t\tsystem: 'You are a helpful assistant on PageAgent website.',\n\t\t\t\t\tgetPageInstructions: (url: string) => {\n\t\t\t\t\t\tconst hint = url.includes('page-agent') ? 'This is PageAgent demo page.' : undefined\n\t\t\t\t\t\tconsole.log('[instructions] getPageInstructions:', url, '->', hint)\n\t\t\t\t\t\treturn hint\n\t\t\t\t\t},\n\t\t\t\t},\n\n\t\t\t\tmodel:\n\t\t\t\t\timport.meta.env.DEV && import.meta.env.LLM_MODEL_NAME\n\t\t\t\t\t\t? import.meta.env.LLM_MODEL_NAME\n\t\t\t\t\t\t: DEMO_MODEL,\n\t\t\t\tbaseURL:\n\t\t\t\t\timport.meta.env.DEV && import.meta.env.LLM_BASE_URL\n\t\t\t\t\t\t? import.meta.env.LLM_BASE_URL\n\t\t\t\t\t\t: DEMO_BASE_URL,\n\t\t\t\tapiKey:\n\t\t\t\t\timport.meta.env.DEV && import.meta.env.LLM_API_KEY\n\t\t\t\t\t\t? import.meta.env.LLM_API_KEY\n\t\t\t\t\t\t: undefined,\n\t\t\t})\n\t\t}\n\n\t\tconst result = await win.pageAgent.execute(task)\n\t\tconsole.log(result)\n\t}\n\n\treturn (\n\t\t<section className=\"relative px-6 pt-18 pb-14 lg:pb-20 lg:pt-24\" aria-labelledby=\"hero-heading\">\n\t\t\t<div className=\"max-w-7xl mx-auto text-center\">\n\t\t\t\t{/* Background Pattern + Particles */}\n\t\t\t\t<div className=\"absolute inset-0 opacity-30\" aria-hidden=\"true\">\n\t\t\t\t\t<div className=\"absolute inset-0 bg-linear-to-r from-blue-400/20 to-purple-400/20 rounded-3xl transform rotate-1\"></div>\n\t\t\t\t\t<div className=\"absolute inset-0 bg-linear-to-l from-purple-400/20 to-blue-400/20 rounded-3xl transform -rotate-1\"></div>\n\t\t\t\t</div>\n\t\t\t\t<Particles\n\t\t\t\t\tclassName=\"absolute inset-0\"\n\t\t\t\t\tquantity={80}\n\t\t\t\t\tstaticity={30}\n\t\t\t\t\tease={80}\n\t\t\t\t\tcolor=\"#6366f1\"\n\t\t\t\t/>\n\n\t\t\t\t<div className=\"relative z-10\">\n\t\t\t\t\t<div className=\"inline-flex items-center px-4 py-2 mb-4 text-sm font-medium bg-white/90 dark:bg-gray-800/90 rounded-full shadow-lg border border-gray-200 dark:border-gray-700\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName=\"w-2 h-2 bg-blue-500 rounded-full mr-2 animate-pulse\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t></span>\n\t\t\t\t\t\t<AnimatedGradientText colorFrom=\"#3b82f6\" colorTo=\"#8b5cf6\">\n\t\t\t\t\t\t\tAI Agent In Your Webpage\n\t\t\t\t\t\t</AnimatedGradientText>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<h1\n\t\t\t\t\t\tid=\"hero-heading\"\n\t\t\t\t\t\tclassName=\"text-5xl lg:text-7xl font-bold mb-10 mt-8 bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent pb-1\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{isZh ? (\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t<span className=\"text-6xl lg:text-7xl\">你网站里的 AI 操作员</span>\n\t\t\t\t\t\t\t\t<span className=\"block text-xl lg:text-2xl mt-5 font-medium bg-linear-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent\">\n\t\t\t\t\t\t\t\t\tThe AI Operator Living in Your Web Page\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\tThe AI Operator\n\t\t\t\t\t\t\t\t<br />\n\t\t\t\t\t\t\t\tLiving in Your Web Page\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</h1>\n\n\t\t\t\t\t<p className=\"text-xl lg:text-2xl text-gray-600 dark:text-gray-300 mb-12 max-w-4xl mx-auto leading-relaxed\">\n\t\t\t\t\t\t<Highlighter action=\"underline\" color=\"#8b5cf6\" strokeWidth={2}>\n\t\t\t\t\t\t\t<span className=\"bg-linear-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent font-bold\">\n\t\t\t\t\t\t\t\t{isZh ? '🪄一行代码' : '🪄One line of code'}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</Highlighter>\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '，让你的网站变身 AI 原生应用。'\n\t\t\t\t\t\t\t: ', turns your website into an AI-native app.'}\n\t\t\t\t\t\t<br />\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '用户/答疑机器人给出文字指示，AI 帮你操作页面。'\n\t\t\t\t\t\t\t: 'Users give natural language commands, AI handles the rest.'}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t{/* Try It Now Section - Tab Card */}\n\t\t\t\t\t<div className=\"mb-12\">\n\t\t\t\t\t\t<div className=\"max-w-3xl mx-auto\">\n\t\t\t\t\t\t\t<NeonGradientCard\n\t\t\t\t\t\t\t\tborderSize={2}\n\t\t\t\t\t\t\t\tborderRadius={20}\n\t\t\t\t\t\t\t\tneonColors={{ firstColor: '#ff00aa', secondColor: '#00FFF1' }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{/* Tab Headers */}\n\t\t\t\t\t\t\t\t<div className=\"flex border-b border-gray-200 dark:border-gray-700\">\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\tonClick={() => setActiveTab('try')}\n\t\t\t\t\t\t\t\t\t\tclassName={`cursor-pointer flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 rounded-tl-2xl ${\n\t\t\t\t\t\t\t\t\t\t\tactiveTab === 'try'\n\t\t\t\t\t\t\t\t\t\t\t\t? 'bg-linear-to-r from-blue-50 to-purple-50 dark:from-blue-900/30 dark:to-purple-900/30 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'\n\t\t\t\t\t\t\t\t\t\t\t\t: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'\n\t\t\t\t\t\t\t\t\t\t}`}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{isZh ? '🚀 立即尝试' : '🚀 Try It Now'}\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\tonClick={() => setActiveTab('other')}\n\t\t\t\t\t\t\t\t\t\tclassName={`cursor-pointer flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 rounded-tr-2xl ${\n\t\t\t\t\t\t\t\t\t\t\tactiveTab === 'other'\n\t\t\t\t\t\t\t\t\t\t\t\t? 'bg-linear-to-r from-green-50 to-blue-50 dark:from-green-900/30 dark:to-blue-900/30 text-green-700 dark:text-green-300 border-b-2 border-green-500'\n\t\t\t\t\t\t\t\t\t\t\t\t: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'\n\t\t\t\t\t\t\t\t\t\t}`}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{isZh ? '🌐 其他网页尝试' : '🌐 Try on Other Sites'}\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t{/* Tab Content */}\n\t\t\t\t\t\t\t\t<div className=\"p-4\">\n\t\t\t\t\t\t\t\t\t{activeTab === 'try' && (\n\t\t\t\t\t\t\t\t\t\t<div className=\"space-y-4\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"relative\">\n\t\t\t\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\t\t\t\tvalue={task}\n\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={(e) => setTask(e.target.value)}\n\t\t\t\t\t\t\t\t\t\t\t\t\tplaceholder={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tisZh\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? '输入您想要 AI 执行的任务...'\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: 'Describe what you want AI to do...'\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"w-full px-4 py-3 pr-20 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-sm mb-0\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-page-agent-not-interactive\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={handleExecute}\n\t\t\t\t\t\t\t\t\t\t\t\t\tdisabled={!ready}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"absolute right-2 top-2 px-5 py-1.5 bg-linear-to-r from-blue-600 to-purple-600 text-white font-medium rounded-md hover:shadow-md transform hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none text-sm\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tdata-page-agent-not-interactive\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{ready ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tisZh ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'执行'\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'Run'\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"animate-pulse\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isZh ? '准备中...' : 'Preparing...'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-xs text-gray-500 dark:text-gray-400 text-left\">\n\t\t\t\t\t\t\t\t\t\t\t\t{isZh ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t使用免费测试 LLM API，点击执行即表示您同意\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"underline\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t使用条款\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tPowered by free testing LLM API. By clicking Run you agree to the{' '}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"underline\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tTerms of Use\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t\t\t{activeTab === 'other' && (\n\t\t\t\t\t\t\t\t\t\t<div className=\"grid md:grid-cols-2 gap-6\">\n\t\t\t\t\t\t\t\t\t\t\t{/* 左侧：操作步骤 */}\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"space-y-4\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"bg-blue-50 dark:bg-gray-700 p-4 rounded-lg\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-gray-700 dark:text-gray-300 text-sm mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"font-semibold\">{isZh ? '步骤 1:' : 'Step 1:'}</span>{' '}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isZh ? '显示收藏夹栏' : 'Show your bookmarks bar'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex items-center justify-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<kbd className=\"px-2 py-1 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded text-xs font-mono\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tCtrl + Shift + B\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</kbd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-gray-500 dark:text-gray-400\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isZh ? '或' : 'or'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<kbd className=\"px-2 py-1 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded text-xs font-mono\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t⌘ + Shift + B\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</kbd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"bg-green-50 dark:bg-gray-700 p-4 rounded-lg\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-gray-700 dark:text-gray-300 text-sm mb-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"font-semibold\">{isZh ? '步骤 2:' : 'Step 2:'}</span>{' '}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isZh ? '拖拽下面按钮到收藏夹栏' : 'Drag this button to your bookmarks'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex items-center justify-center gap-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvalue={cdnSource}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={(e) =>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetCdnSource(e.target.value as 'international' | 'china')\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"px-2 py-1.5 text-xs border border-gray-300 dark:border-gray-500 rounded bg-white dark:bg-gray-600 text-gray-700 dark:text-gray-200\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"international\">jsdelivr CDN</option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option value=\"china\">npmmirror CDN</option>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdangerouslySetInnerHTML={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t__html: getInjection(cdnSource === 'china'),\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t></div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"bg-purple-50 dark:bg-gray-700 p-4 rounded-lg\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-gray-700 dark:text-gray-300 text-sm\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"font-semibold\">{isZh ? '步骤 3:' : 'Step 3:'}</span>{' '}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? '在其他网站点击收藏夹中的按钮即可使用'\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: 'Click the bookmark on any site to activate'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t\t\t{/* 右侧：注意事项 */}\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"bg-yellow-50 dark:bg-gray-700 p-4 rounded-lg\">\n\t\t\t\t\t\t\t\t\t\t\t\t<h4 className=\"font-semibold text-gray-900 dark:text-white mb-3 text-sm\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{isZh ? '⚠️ 注意' : '⚠️ Heads Up'}\n\t\t\t\t\t\t\t\t\t\t\t\t</h4>\n\t\t\t\t\t\t\t\t\t\t\t\t<ul className=\"space-y-2 text-sm text-gray-700 dark:text-gray-300\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<li className=\"flex items-start text-left\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 \"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isZh ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t使用免费测试 LLM API，使用即表示同意\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-yellow-700 dark:text-yellow-300 underline\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t使用条款\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tUses free testing LLM API. By using you agree to the{' '}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-yellow-700 dark:text-yellow-300 underline\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tTerms of Use\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<li className=\"flex items-start text-left\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 \"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? '数据通过中国大陆服务器处理'\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: 'Data processed via servers in Mainland China'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<li className=\"flex items-start text-left\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 \"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? '部分网站屏蔽了链接嵌入，将无反应'\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: 'Some sites block script injection (CSP policies)'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<li className=\"flex items-start text-left\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 \"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isZh ? '支持单页应用' : 'Works on single-page apps'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<li className=\"flex items-start text-left\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 \"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? '仅识别文本，不识别图像，不支持拖拽等复杂交互'\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: 'Text-only understanding—no image recognition or drag-and-drop'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<li className=\"flex items-start text-left\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 \"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isZh ? '详细使用限制参照' : 'Full limitations in'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thref=\"/docs/introduction/limitations\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-blue-600 dark:text-blue-400 hover:underline pl-1\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isZh ? '《文档》' : 'Docs'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</NeonGradientCard>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<ul\n\t\t\t\t\t\tclassName=\"flex flex-wrap justify-center gap-6 text-sm text-gray-500 dark:text-gray-400\"\n\t\t\t\t\t\trole=\"list\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<li className=\"flex items-center\">\n\t\t\t\t\t\t\t<span className=\"w-2 h-2 bg-green-500 rounded-full mr-2\" aria-hidden=\"true\"></span>\n\t\t\t\t\t\t\t{isZh ? '纯前端方案' : 'Pure Front-end Solution'}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t\t<li className=\"flex items-center\">\n\t\t\t\t\t\t\t<span className=\"w-2 h-2 bg-green-500 rounded-full mr-2\" aria-hidden=\"true\"></span>\n\t\t\t\t\t\t\t{isZh ? '支持私有模型' : 'Your Own Models'}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t\t<li className=\"flex items-center\">\n\t\t\t\t\t\t\t<span className=\"w-2 h-2 bg-green-500 rounded-full mr-2\" aria-hidden=\"true\"></span>\n\t\t\t\t\t\t\t{isZh ? '无痛脱敏' : 'Built-in Privacy'}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t\t<li className=\"flex items-center\">\n\t\t\t\t\t\t\t<span className=\"w-2 h-2 bg-green-500 rounded-full mr-2\" aria-hidden=\"true\"></span>\n\t\t\t\t\t\t\t{isZh ? 'MIT 开源' : 'MIT Open Source'}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</section>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/home/OneMoreThingSection.tsx",
    "content": "import { ExternalLink } from 'lucide-react'\nimport { siGooglechrome } from 'simple-icons'\nimport { Link } from 'wouter'\n\nimport { BlurFade } from '../../components/ui/blur-fade'\nimport { MagicCard } from '../../components/ui/magic-card'\nimport { useLanguage } from '../../i18n/context'\n\nexport default function OneMoreThingSection() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<section className=\"px-6 py-14\" aria-labelledby=\"one-more-thing-heading\">\n\t\t\t<div className=\"max-w-4xl mx-auto text-center\">\n\t\t\t\t<BlurFade inView>\n\t\t\t\t\t<h2\n\t\t\t\t\t\tid=\"one-more-thing-heading\"\n\t\t\t\t\t\tclassName=\"text-4xl lg:text-5xl font-bold mb-6 bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent\"\n\t\t\t\t\t>\n\t\t\t\t\t\tOne More Thing\n\t\t\t\t\t</h2>\n\t\t\t\t\t<p className=\"text-xl text-gray-600 dark:text-gray-300 mb-4 max-w-2xl mx-auto\">\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? '想要多页面控制？试试可选的浏览器扩展。'\n\t\t\t\t\t\t\t: 'Need multi-page control? Try the optional browser extension.'}\n\t\t\t\t\t</p>\n\t\t\t\t\t<p className=\"text-sm text-gray-500 dark:text-gray-400 mb-12 max-w-2xl mx-auto\">\n\t\t\t\t\t\t{'* '}\n\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t? 'PageAgent.js 本身无需任何扩展即可工作，扩展是额外的能力增强。'\n\t\t\t\t\t\t\t: 'PageAgent.js works without any extension — this is a power-up, not a dependency.'}\n\t\t\t\t\t</p>\n\t\t\t\t</BlurFade>\n\n\t\t\t\t<div className=\"flex flex-col sm:flex-row items-center justify-center gap-4 mb-12\">\n\t\t\t\t\t<a\n\t\t\t\t\t\thref=\"https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t\tclassName=\"group inline-flex items-center gap-3 px-8 py-4 bg-linear-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-medium rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tsrc=\"https://img.alicdn.com/imgextra/i3/O1CN01JpW0Vo1sR3FpiZKFM_!!6000000005762-55-tps-192-192.svg\"\n\t\t\t\t\t\t\talt=\"Chrome Web Store\"\n\t\t\t\t\t\t\tclassName=\"w-7 h-7\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<span>{isZh ? '从 Chrome 应用商店安装' : 'Install from Chrome Web Store'}</span>\n\t\t\t\t\t\t<ExternalLink className=\"w-4 h-4 opacity-50 group-hover:opacity-100 transition-opacity\" />\n\t\t\t\t\t</a>\n\t\t\t\t\t<Link\n\t\t\t\t\t\thref=\"/docs/features/chrome-extension\"\n\t\t\t\t\t\tclassName=\"inline-flex items-center gap-3 px-8 py-4 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-900 dark:text-white font-medium rounded-2xl transition-all duration-300 hover:scale-105\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<svg className=\"w-5 h-5\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t<path d={siGooglechrome.path} fill=\"currentColor\" />\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t<span>{isZh ? '查看文档' : 'Read the Docs'}</span>\n\t\t\t\t\t</Link>\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"grid sm:grid-cols-3 gap-5 text-left max-w-3xl mx-auto\">\n\t\t\t\t\t{[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttitle: isZh ? '多页面任务' : 'Multi-Page Tasks',\n\t\t\t\t\t\t\tdesc: isZh\n\t\t\t\t\t\t\t\t? '跨多个页面和标签页连续执行任务，不再受限于单页上下文'\n\t\t\t\t\t\t\t\t: 'Run tasks across multiple pages and tabs without being limited to a single page context',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttitle: isZh ? '页面内发起控制' : 'Control from Your Page',\n\t\t\t\t\t\t\tdesc: isZh\n\t\t\t\t\t\t\t\t? '在页面 JS 中发起任务，驱动整个浏览器完成跨标签操作'\n\t\t\t\t\t\t\t\t: 'Trigger tasks from page JS to drive the entire browser across tabs',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttitle: isZh ? '外部发起任务' : 'External Triggers',\n\t\t\t\t\t\t\tdesc: isZh\n\t\t\t\t\t\t\t\t? '页面 JS、本地 Agent 或云端 Agent 均可通过扩展发起任务'\n\t\t\t\t\t\t\t\t: 'Page JS, local agents, or cloud agents can trigger tasks through the extension',\n\t\t\t\t\t\t},\n\t\t\t\t\t].map((item) => (\n\t\t\t\t\t\t<MagicCard\n\t\t\t\t\t\t\tkey={item.title}\n\t\t\t\t\t\t\tclassName=\"rounded-xl\"\n\t\t\t\t\t\t\tgradientColor=\"#8b5cf620\"\n\t\t\t\t\t\t\tgradientOpacity={0.15}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div className=\"p-5\">\n\t\t\t\t\t\t\t\t<h3 className=\"font-semibold text-gray-900 dark:text-white mb-1\">{item.title}</h3>\n\t\t\t\t\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-300\">{item.desc}</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</MagicCard>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</section>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/home/ScenariosSection.tsx",
    "content": "import { Bot, Users, Zap } from 'lucide-react'\n\nimport { BlurFade } from '../../components/ui/blur-fade'\nimport { SparklesText } from '../../components/ui/sparkles-text'\nimport { useLanguage } from '../../i18n/context'\n\nexport default function ScenariosSection() {\n\tconst { isZh } = useLanguage()\n\n\treturn (\n\t\t<section\n\t\t\tclassName=\"px-6 py-16 bg-linear-to-b from-blue-100 to-purple-100 dark:from-blue-950/40 dark:to-gray-800\"\n\t\t\taria-labelledby=\"scenarios-heading\"\n\t\t>\n\t\t\t<div className=\"max-w-6xl mx-auto\">\n\t\t\t\t<BlurFade inView>\n\t\t\t\t\t<div className=\"text-center mb-12\">\n\t\t\t\t\t\t<SparklesText\n\t\t\t\t\t\t\tclassName=\"text-4xl lg:text-5xl mb-6\"\n\t\t\t\t\t\t\tcolors={{ first: '#3b82f6', second: '#8b5cf6' }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{isZh ? '应用场景' : 'Built For'}\n\t\t\t\t\t\t</SparklesText>\n\t\t\t\t\t</div>\n\t\t\t\t</BlurFade>\n\n\t\t\t\t<div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n\t\t\t\t\t{/* SaaS AI Copilot */}\n\t\t\t\t\t<BlurFade inView delay={0.05}>\n\t\t\t\t\t\t<div className=\"group relative overflow-hidden rounded-2xl bg-linear-to-b from-blue-50 to-white dark:from-blue-950/40 dark:to-gray-800 border border-blue-200/80 dark:border-blue-800/50 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-500\">\n\t\t\t\t\t\t\t<div className=\"p-6 pb-4\">\n\t\t\t\t\t\t\t\t<div className=\"rounded-xl bg-gray-950 p-4 font-mono text-xs leading-6 text-gray-300 overflow-hidden shadow-inner\">\n\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-purple-400\">import</span> {'{ PageAgent }'}{' '}\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-purple-400\">from</span>{' '}\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-emerald-400\">&apos;page-agent&apos;</span>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"mt-2\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-purple-400\">const</span>{' '}\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-blue-300\">copilot</span> ={' '}\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-purple-400\">new</span>{' '}\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-yellow-300\">PageAgent</span>\n\t\t\t\t\t\t\t\t\t\t{'({'}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"pl-4\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-blue-300\">model</span>:{' '}\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-emerald-400\">&apos;gpt-5.1&apos;</span>,\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"pl-4\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-blue-300\">apiKey</span>:{' '}\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-emerald-400\">process.env.KEY</span>,\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div>{'})'}</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"p-6 pt-2\">\n\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2 mb-2\">\n\t\t\t\t\t\t\t\t\t<Bot className=\"w-5 h-5 text-blue-500\" />\n\t\t\t\t\t\t\t\t\t<h3 className=\"font-semibold text-lg text-gray-900 dark:text-white\">\n\t\t\t\t\t\t\t\t\t\t{isZh ? 'SaaS AI 副驾驶' : 'SaaS AI Copilot'}\n\t\t\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-300 leading-relaxed\">\n\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t? '几小时内为你的产品加上 AI 副驾驶，不需要重写后端。'\n\t\t\t\t\t\t\t\t\t\t: 'Ship an AI copilot in your product in hours, not months. No backend rewrite needed.'}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</BlurFade>\n\n\t\t\t\t\t{/* Smart Form Filling */}\n\t\t\t\t\t<BlurFade inView delay={0.1}>\n\t\t\t\t\t\t<div className=\"group relative overflow-hidden rounded-2xl bg-linear-to-b from-amber-50 to-white dark:from-amber-950/40 dark:to-gray-800 border border-amber-200/80 dark:border-amber-800/50 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-500\">\n\t\t\t\t\t\t\t<div className=\"p-6 pb-4\">\n\t\t\t\t\t\t\t\t<div className=\"rounded-xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 shadow-inner space-y-2.5\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 bg-amber-50 dark:bg-amber-900/30 rounded-lg px-3 py-2 border border-amber-200/50 dark:border-amber-700/40\">\n\t\t\t\t\t\t\t\t\t\t<span>🪄</span>\n\t\t\t\t\t\t\t\t\t\t<span className=\"italic\">\n\t\t\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t\t\t? '\"填写上周五出差的报销单\"'\n\t\t\t\t\t\t\t\t\t\t\t\t: '\"Fill the expense report for Friday\\'s trip\"'}\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t{[\n\t\t\t\t\t\t\t\t\t\t{ label: isZh ? '姓名' : 'Name', value: 'John Smith' },\n\t\t\t\t\t\t\t\t\t\t{ label: isZh ? '金额' : 'Amount', value: '$342.50' },\n\t\t\t\t\t\t\t\t\t\t{ label: isZh ? '类目' : 'Category', value: 'Travel' },\n\t\t\t\t\t\t\t\t\t].map((field) => (\n\t\t\t\t\t\t\t\t\t\t<div key={field.label} className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-xs text-gray-400 dark:text-gray-500 w-12 shrink-0\">\n\t\t\t\t\t\t\t\t\t\t\t\t{field.label}\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex-1 h-7 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-600 px-2 flex items-center text-xs text-gray-600 dark:text-gray-300\">\n\t\t\t\t\t\t\t\t\t\t\t\t{field.value}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-emerald-500 text-xs\">✓</span>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"p-6 pt-2\">\n\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2 mb-2\">\n\t\t\t\t\t\t\t\t\t<Zap className=\"w-5 h-5 text-amber-500\" />\n\t\t\t\t\t\t\t\t\t<h3 className=\"font-semibold text-lg text-gray-900 dark:text-white\">\n\t\t\t\t\t\t\t\t\t\t{isZh ? '智能表单填写' : 'Smart Form Filling'}\n\t\t\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-300 leading-relaxed\">\n\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t? '把 20 次点击变成一句话。ERP、CRM、管理后台的最佳拍档。'\n\t\t\t\t\t\t\t\t\t\t: 'Turn 20-click workflows into one sentence. Perfect for ERP, CRM, and admin systems.'}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</BlurFade>\n\n\t\t\t\t\t{/* Accessibility */}\n\t\t\t\t\t<BlurFade inView delay={0.15}>\n\t\t\t\t\t\t<div className=\"group relative overflow-hidden rounded-2xl bg-linear-to-b from-purple-50 to-white dark:from-purple-950/40 dark:to-gray-800 border border-purple-200/80 dark:border-purple-800/50 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-500\">\n\t\t\t\t\t\t\t<div className=\"p-6 pb-4 flex flex-col items-center justify-center\">\n\t\t\t\t\t\t\t\t<div className=\"w-full rounded-xl bg-purple-50 dark:bg-purple-900/30 p-5 space-y-3\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-3\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"w-8 h-8 rounded-full bg-purple-500/10 dark:bg-purple-500/20 flex items-center justify-center text-base\">\n\t\t\t\t\t\t\t\t\t\t\t🎤\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div className=\"text-sm text-purple-700 dark:text-purple-300 italic\">\n\t\t\t\t\t\t\t\t\t\t\t{isZh ? '\"点击提交按钮\"' : '\"Click the submit button\"'}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-3 pl-11\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-1.5\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"w-1.5 h-1.5 bg-purple-400 rounded-full animate-pulse\"></div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"w-1.5 h-1.5 bg-purple-400 rounded-full animate-pulse [animation-delay:0.2s]\"></div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"w-1.5 h-1.5 bg-purple-400 rounded-full animate-pulse [animation-delay:0.4s]\"></div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-xs text-purple-500 dark:text-purple-400\">\n\t\t\t\t\t\t\t\t\t\t\t{isZh ? 'AI 正在执行...' : 'AI executing...'}\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-3 pl-11 text-sm text-emerald-600 dark:text-emerald-400\">\n\t\t\t\t\t\t\t\t\t\t<span>✓</span> {isZh ? '按钮已点击' : 'Button clicked'}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"p-6 pt-2\">\n\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2 mb-2\">\n\t\t\t\t\t\t\t\t\t<Users className=\"w-5 h-5 text-purple-500\" />\n\t\t\t\t\t\t\t\t\t<h3 className=\"font-semibold text-lg text-gray-900 dark:text-white\">\n\t\t\t\t\t\t\t\t\t\t{isZh ? '无障碍增强' : 'Accessibility'}\n\t\t\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-300 leading-relaxed\">\n\t\t\t\t\t\t\t\t\t{isZh\n\t\t\t\t\t\t\t\t\t\t? '用自然语言让任何网页无障碍。语音指令、屏幕阅读器，零门槛。'\n\t\t\t\t\t\t\t\t\t\t: 'Make any web app accessible through natural language. Voice, screen readers, zero barrier.'}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</BlurFade>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</section>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/pages/home/index.tsx",
    "content": "import { Suspense, lazy } from 'react'\n\nimport { useDocumentTitle } from '@/lib/useDocumentTitle'\n\nimport HeroSection from './HeroSection'\n\nconst FeaturesSection = lazy(() => import('./FeaturesSection'))\nconst ScenariosSection = lazy(() => import('./ScenariosSection'))\nconst OneMoreThingSection = lazy(() => import('./OneMoreThingSection'))\n\nexport default function HomePage() {\n\tuseDocumentTitle()\n\n\treturn (\n\t\t<>\n\t\t\t<HeroSection />\n\t\t\t<Suspense\n\t\t\t\tfallback={\n\t\t\t\t\t<div className=\"flex items-center justify-center gap-3 py-20 text-gray-400\">\n\t\t\t\t\t\t<div className=\"w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin\" />\n\t\t\t\t\t\tLoading...\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t>\n\t\t\t\t<FeaturesSection />\n\t\t\t\t<ScenariosSection />\n\t\t\t\t<OneMoreThingSection />\n\t\t\t</Suspense>\n\t\t</>\n\t)\n}\n"
  },
  {
    "path": "packages/website/src/router.tsx",
    "content": "import { Suspense, lazy, useEffect, useLayoutEffect } from 'react'\nimport { Route, Switch, useLocation } from 'wouter'\n\nimport Footer from './components/Footer'\nimport Header from './components/Header'\nimport HomePage from './pages/home'\n\nconst docsImport = () => import('./pages/docs')\nconst DocsPages = lazy(docsImport)\n\nfunction ScrollToTop() {\n\tconst [pathname] = useLocation()\n\tuseLayoutEffect(() => {\n\t\twindow.scrollTo(0, 0)\n\t}, [pathname])\n\treturn null\n}\n\nexport default function Router() {\n\tuseEffect(() => {\n\t\tconst schedule = globalThis.requestIdleCallback ?? ((cb: () => void) => setTimeout(cb, 1))\n\t\tconst cancel = globalThis.cancelIdleCallback ?? clearTimeout\n\t\tconst id = schedule(() => docsImport())\n\t\treturn () => cancel(id)\n\t}, [])\n\n\treturn (\n\t\t<div className=\"flex min-h-screen flex-col\">\n\t\t\t<Header />\n\t\t\t<Suspense>\n\t\t\t\t<ScrollToTop />\n\t\t\t\t<Switch>\n\t\t\t\t\t<Route path=\"/\">\n\t\t\t\t\t\t<main\n\t\t\t\t\t\t\tid=\"main-content\"\n\t\t\t\t\t\t\tclassName=\"flex-1 bg-linear-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<HomePage />\n\t\t\t\t\t\t</main>\n\t\t\t\t\t</Route>\n\n\t\t\t\t\t<Route path=\"/docs\" nest>\n\t\t\t\t\t\t<div className=\"flex-1 bg-white dark:bg-gray-900\">\n\t\t\t\t\t\t\t<Suspense\n\t\t\t\t\t\t\t\tfallback={\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center justify-center gap-3 py-20 text-gray-400\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin\" />\n\t\t\t\t\t\t\t\t\t\tLoading...\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<DocsPages />\n\t\t\t\t\t\t\t</Suspense>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Route>\n\n\t\t\t\t\t<Route>\n\t\t\t\t\t\t<div className=\"flex-1 bg-white dark:bg-gray-900 flex items-center justify-center\">\n\t\t\t\t\t\t\t<div className=\"text-center\">\n\t\t\t\t\t\t\t\t<h1 className=\"text-4xl font-bold mb-4 text-gray-900 dark:text-white\">404</h1>\n\t\t\t\t\t\t\t\t<p className=\"text-xl text-gray-600 dark:text-gray-300\">Page not found</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Route>\n\t\t\t\t</Switch>\n\t\t\t</Suspense>\n\t\t\t<Footer />\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "packages/website/tailwind.config.js",
    "content": "export default {\n\timportant: '#root',\n}\n"
  },
  {
    "path": "packages/website/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.base.json\",\n    \"compilerOptions\": {\n        \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.tsbuildinfo\",\n        \"noEmit\": false,\n        \"allowImportingTsExtensions\": false,\n        \"baseUrl\": \".\",\n        \"outDir\": \"dist\",\n        \"paths\": {\n            // Self root\n            \"@/*\": [\"src/*\"],\n\n            \"@page-agent/llms\": [\"../llms/src/index.ts\"],\n            \"@page-agent/page-controller\": [\"../page-controller/src/PageController.ts\"],\n            \"@page-agent/core\": [\"../core/src/PageAgentCore.ts\"],\n            \"@page-agent/ui\": [\"../ui/src/index.ts\"],\n\n            \"page-agent\": [\"../page-agent/src/PageAgent.ts\"]\n        }\n    },\n    \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n    \"exclude\": [\"dist\", \"node_modules\"],\n    \"references\": [\n        //\n        { \"path\": \"../llms\" },\n        { \"path\": \"../page-controller\" },\n        { \"path\": \"../core\" },\n        { \"path\": \"../ui\" },\n\n        { \"path\": \"../page-agent\" }\n    ]\n}\n"
  },
  {
    "path": "packages/website/vite.config.js",
    "content": "import tailwindcss from '@tailwindcss/vite'\nimport react from '@vitejs/plugin-react-swc'\nimport { config as dotenvConfig } from 'dotenv'\nimport { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport process from 'node:process'\nimport { dirname, join, resolve } from 'path'\nimport { fileURLToPath } from 'url'\nimport { defineConfig } from 'vite'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\nconst pageAgentPkg = JSON.parse(\n\treadFileSync(resolve(__dirname, '../page-agent/package.json'), 'utf-8')\n)\n\n// Load .env from repo root\ndotenvConfig({ path: resolve(__dirname, '../../.env'), quiet: true })\n\n// All SPA routes that need index.html copies for direct access on static hosts\nconst SPA_ROUTES = [\n\t'docs',\n\t'docs/introduction/overview',\n\t'docs/introduction/quick-start',\n\t'docs/introduction/limitations',\n\t'docs/introduction/troubleshooting',\n\t'docs/features/custom-tools',\n\t'docs/features/data-masking',\n\t'docs/features/custom-instructions',\n\t'docs/features/models',\n\t'docs/features/chrome-extension',\n\t'docs/features/third-party-agent',\n\t'docs/advanced/page-agent',\n\t'docs/advanced/page-agent-core',\n\t'docs/advanced/page-controller',\n\t'docs/advanced/custom-ui',\n\t'docs/advanced/security-permissions',\n]\n\nconst SITE_URL = 'https://alibaba.github.io/page-agent'\n\nfunction spaRoutes() {\n\treturn {\n\t\tname: 'spa-routes',\n\t\tcloseBundle() {\n\t\t\tconst dist = resolve(__dirname, 'dist')\n\t\t\tconst src = join(dist, 'index.html')\n\t\t\tfor (const route of SPA_ROUTES) {\n\t\t\t\tconst dir = join(dist, route)\n\t\t\t\tmkdirSync(dir, { recursive: true })\n\t\t\t\tcopyFileSync(src, join(dir, 'index.html'))\n\t\t\t}\n\t\t\tconsole.log(`  ✓ Copied index.html to ${SPA_ROUTES.length} SPA routes`)\n\n\t\t\tconst today = new Date().toISOString().split('T')[0]\n\t\t\tconst urls = ['', ...SPA_ROUTES]\n\t\t\t\t.map(\n\t\t\t\t\t(route) =>\n\t\t\t\t\t\t`  <url>\\n    <loc>${SITE_URL}/${route}</loc>\\n    <lastmod>${today}</lastmod>\\n  </url>`\n\t\t\t\t)\n\t\t\t\t.join('\\n')\n\t\t\twriteFileSync(\n\t\t\t\tjoin(dist, 'sitemap.xml'),\n\t\t\t\t`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n${urls}\\n</urlset>\\n`\n\t\t\t)\n\t\t\tconsole.log(`  ✓ Generated sitemap.xml with ${SPA_ROUTES.length + 1} URLs`)\n\t\t},\n\t}\n}\n\n// Website Config (React Documentation Site)\nexport default defineConfig(({ mode }) => ({\n\tbase: '/page-agent/',\n\tclearScreen: false,\n\tplugins: [react(), tailwindcss(), spaRoutes()],\n\tbuild: {\n\t\tchunkSizeWarningLimit: 2000,\n\t\tcssCodeSplit: true,\n\t\trollupOptions: {\n\t\t\tonwarn: function (message, handler) {\n\t\t\t\tif (message.code === 'EVAL') return\n\t\t\t\thandler(message)\n\t\t\t},\n\t\t\toutput: {\n\t\t\t\tmanualChunks: {\n\t\t\t\t\tvendor: ['react', 'react-dom', 'wouter'],\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\tresolve: {\n\t\talias: {\n\t\t\t// Self root\n\t\t\t'@': resolve(__dirname, 'src'),\n\n\t\t\t// Monorepo packages (always bundle local code instead of npm versions)\n\t\t\t'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),\n\t\t\t'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),\n\t\t\t'@page-agent/core': resolve(__dirname, '../core/src/PageAgentCore.ts'),\n\t\t\t'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),\n\n\t\t\t'page-agent': resolve(__dirname, '../page-agent/src/PageAgent.ts'),\n\t\t},\n\t},\n\tdefine: {\n\t\t...(mode === 'development' && {\n\t\t\t'import.meta.env.LLM_MODEL_NAME': JSON.stringify(process.env.LLM_MODEL_NAME),\n\t\t\t'import.meta.env.LLM_API_KEY': JSON.stringify(process.env.LLM_API_KEY),\n\t\t\t'import.meta.env.LLM_BASE_URL': JSON.stringify(process.env.LLM_BASE_URL),\n\t\t}),\n\t\t'import.meta.env.VERSION': JSON.stringify(pageAgentPkg.version),\n\t},\n}))\n"
  },
  {
    "path": "scripts/sync-version.js",
    "content": "#!/usr/bin/env node\n/**\n * Sync version from root package.json to all packages\n *\n * Usage:\n *   node scripts/sync-version.js        # Sync current version from root\n *   node scripts/sync-version.js 0.1.0  # Set root version, then sync all packages\n */\nimport chalk from 'chalk'\nimport { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'\nimport { dirname, join } from 'path'\nimport { exit } from 'process'\nimport { fileURLToPath } from 'url'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\nconst rootDir = join(__dirname, '..')\n\nconst versionArg = process.argv[2]\n\n// Read root package.json\nconst rootPkgPath = join(rootDir, 'package.json')\nconst rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf-8'))\nconst oldVersion = rootPkg.version\nconst newVersion = versionArg ?? rootPkg.version\n\nif (!newVersion) {\n\tconsole.log(chalk.yellow('⚠️  No version found in root package.json.\\n'))\n\texit(1)\n}\n\nconsole.log(chalk.cyan.bold('\\n📦 Syncing version\\n'))\n\n// Update root package.json if new version specified\nif (versionArg) {\n\trootPkg.version = newVersion\n\twriteFileSync(rootPkgPath, JSON.stringify(rootPkg, null, '    ') + '\\n')\n\tconsole.log(\n\t\tchalk.green('✓') +\n\t\t\t` ${chalk.bold('root')}: ${chalk.dim(oldVersion)} → ${chalk.yellow(newVersion)}`\n\t)\n} else {\n\tconsole.log(chalk.dim('  root:') + ` ${chalk.yellow(newVersion)} ${chalk.dim('(source)')}`)\n}\n\n// Sync to all packages\nconst packagesDir = join(rootDir, 'packages')\nconst packages = readdirSync(packagesDir, { withFileTypes: true })\n\t.filter((d) => d.isDirectory())\n\t.map((d) => d.name)\n\nlet hasChanges = !!versionArg\n\n/**\n * Check if a dependency name is a page-agent internal package\n */\nfunction isInternalPackage(name) {\n\treturn name === 'page-agent' || name.startsWith('@page-agent/')\n}\n\n/**\n * Update internal package versions in dependencies object\n * @returns {boolean} Whether any changes were made\n */\nfunction updateInternalDeps(deps, newVersion) {\n\tif (!deps) return false\n\tlet changed = false\n\tfor (const [name, version] of Object.entries(deps)) {\n\t\tif (isInternalPackage(name) && version !== newVersion) {\n\t\t\tdeps[name] = newVersion\n\t\t\tchanged = true\n\t\t}\n\t}\n\treturn changed\n}\n\nfor (const pkg of packages) {\n\tconst pkgPath = join(packagesDir, pkg, 'package.json')\n\tif (!existsSync(pkgPath)) continue\n\n\tconst pkgJson = JSON.parse(readFileSync(pkgPath, 'utf-8'))\n\tlet pkgChanged = false\n\n\t// Update package version\n\tif (pkgJson.version !== newVersion) {\n\t\tpkgJson.version = newVersion\n\t\tpkgChanged = true\n\t}\n\n\t// Update internal dependencies (dependencies only, devDeps keep \"*\")\n\tif (updateInternalDeps(pkgJson.dependencies, newVersion)) {\n\t\tpkgChanged = true\n\t}\n\n\tif (!pkgChanged) {\n\t\tconsole.log(chalk.dim(`  ${pkgJson.name}: ${newVersion} (unchanged)`))\n\t\tcontinue\n\t}\n\n\twriteFileSync(pkgPath, JSON.stringify(pkgJson, null, '    ') + '\\n')\n\tconsole.log(\n\t\tchalk.green('✓') +\n\t\t\t` ${chalk.bold(pkgJson.name)}: ${chalk.dim(oldVersion)} → ${chalk.yellow(newVersion)}`\n\t)\n\thasChanges = true\n}\n\n// Update CDN URLs in documentation and source files\nconst CDN_DEMO_URL_OLD = `https://cdn.jsdelivr.net/npm/page-agent@${oldVersion}/dist/iife/page-agent.demo.js`\nconst CDN_DEMO_URL_NEW = `https://cdn.jsdelivr.net/npm/page-agent@${newVersion}/dist/iife/page-agent.demo.js`\nconst CDN_DEMO_CN_URL_OLD = `https://registry.npmmirror.com/page-agent/${oldVersion}/files/dist/iife/page-agent.demo.js`\nconst CDN_DEMO_CN_URL_NEW = `https://registry.npmmirror.com/page-agent/${newVersion}/files/dist/iife/page-agent.demo.js`\n\nconst filesToUpdateCdn = ['README.md', 'docs/README-zh.md', 'packages/website/src/constants.ts']\n\nfor (const relPath of filesToUpdateCdn) {\n\tconst filePath = join(rootDir, relPath)\n\tif (!existsSync(filePath)) continue\n\n\tlet content = readFileSync(filePath, 'utf-8')\n\tconst original = content\n\n\tcontent = content.replaceAll(CDN_DEMO_URL_OLD, CDN_DEMO_URL_NEW)\n\tcontent = content.replaceAll(CDN_DEMO_CN_URL_OLD, CDN_DEMO_CN_URL_NEW)\n\n\tif (content !== original) {\n\t\twriteFileSync(filePath, content)\n\t\tconsole.log(chalk.green('✓') + ` ${chalk.bold(relPath)}: CDN URLs updated`)\n\t\thasChanges = true\n\t}\n}\n\nconsole.log(chalk.green.bold(`\\n✓ Version synced: ${newVersion}\\n`))\n\n// Show git commands hint\nif (hasChanges) {\n\tconst tagName = `v${newVersion}`\n\tconsole.log(chalk.cyan.bold('📋 Next steps:\\n'))\n\tconsole.log(chalk.blueBright(`npm i`))\n\tconsole.log(\n\t\tchalk.blueBright(`git add . && git commit -m \"chore(version): bump version to ${newVersion}\"`)\n\t)\n\tconsole.log(chalk.blueBright(`git tag -a ${tagName} -m \"${tagName}\"`))\n\tconsole.log(chalk.blueBright(`git push && git push origin ${tagName}\\n`))\n}\n"
  },
  {
    "path": "tsconfig.base.json",
    "content": "{\n    \"compilerOptions\": {\n        \"composite\": true,\n        \"target\": \"ES2024\",\n        \"useDefineForClassFields\": true,\n        \"lib\": [\"ES2024\", \"DOM\", \"DOM.Iterable\"],\n        \"module\": \"ESNext\",\n        \"skipLibCheck\": true,\n        \"allowJs\": true,\n\n        \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.tsbuildinfo\",\n        // \"baseUrl\": \"src\",\n        \"baseUrl\": \".\",\n        \"outDir\": \"dist\",\n        // \"incremental\": true,\n\n        /* Bundler mode */\n        \"moduleResolution\": \"bundler\",\n        \"verbatimModuleSyntax\": false,\n        \"noEmit\": true,\n        \"jsx\": \"react-jsx\",\n        \"allowImportingTsExtensions\": true,\n\n        /* Linting */\n        \"strict\": true,\n        \"noUnusedLocals\": false,\n        \"noUnusedParameters\": false,\n        \"erasableSyntaxOnly\": true,\n        \"noFallthroughCasesInSwitch\": true,\n        \"noUncheckedSideEffectImports\": true\n    }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "// this is only for IDE ts language server to work.\n// do not use this for building or linting.\n{\n    \"extends\": \"./tsconfig.base.json\",\n    \"references\": [\n        { \"path\": \"./packages/page-controller\" },\n        { \"path\": \"./packages/ui\" },\n        { \"path\": \"./packages/llms\" },\n        { \"path\": \"./packages/page-agent\" },\n        { \"path\": \"./packages/website\" }\n    ],\n    \"include\": [\"packages/*/src/**/*.ts\", \"packages/*/src/**/*.tsx\"],\n    \"exclude\": [\"node_modules\", \"dist\", \"packages/*/dist\"]\n}\n"
  }
]