Showing preview only (818K chars total). Download the full file or copy to clipboard to get everything.
Repository: alibaba/page-agent
Branch: main
Commit: 80e96d0b9e1d
Files: 227
Total size: 756.6 KB
Directory structure:
gitextract_5g_s5zem/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── ci.yml
│ ├── deploy-demo.yml
│ └── release.yml
├── .gitignore
├── .husky/
│ ├── commit-msg
│ └── pre-commit
├── .vscode/
│ ├── extensions.json
│ └── settings.json
├── AGENTS.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── docs/
│ ├── CHANGELOG.md
│ ├── CODE_OF_CONDUCT.md
│ ├── README-zh.md
│ └── terms-and-privacy.md
├── eslint.config.js
├── package.json
├── packages/
│ ├── core/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── PageAgentCore.ts
│ │ │ ├── env.d.ts
│ │ │ ├── prompts/
│ │ │ │ ├── .prettierignore
│ │ │ │ └── system_prompt.md
│ │ │ ├── tools/
│ │ │ │ └── index.ts
│ │ │ ├── types.ts
│ │ │ └── utils/
│ │ │ ├── autoFixer.ts
│ │ │ └── index.ts
│ │ ├── tsconfig.dts.json
│ │ ├── tsconfig.json
│ │ └── vite.config.js
│ ├── extension/
│ │ ├── .prettierignore
│ │ ├── PRIVACY.md
│ │ ├── components.json
│ │ ├── docs/
│ │ │ └── extension_api.md
│ │ ├── package.json
│ │ ├── public/
│ │ │ └── _locales/
│ │ │ ├── en/
│ │ │ │ └── messages.json
│ │ │ └── zh_CN/
│ │ │ └── messages.json
│ │ ├── src/
│ │ │ ├── agent/
│ │ │ │ ├── .prettierignore
│ │ │ │ ├── MultiPageAgent.ts
│ │ │ │ ├── RemotePageController.background.ts
│ │ │ │ ├── RemotePageController.content.ts
│ │ │ │ ├── RemotePageController.ts
│ │ │ │ ├── TabsController.background.ts
│ │ │ │ ├── TabsController.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── system_prompt.md
│ │ │ │ ├── tabTools.ts
│ │ │ │ └── useAgent.ts
│ │ │ ├── assets/
│ │ │ │ └── index.css
│ │ │ ├── components/
│ │ │ │ ├── ConfigPanel.tsx
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── HistoryDetail.tsx
│ │ │ │ ├── HistoryList.tsx
│ │ │ │ ├── cards.tsx
│ │ │ │ ├── misc.tsx
│ │ │ │ └── ui/
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── field.tsx
│ │ │ │ ├── hover-card.tsx
│ │ │ │ ├── input-group.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── item.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── sonner.tsx
│ │ │ │ ├── spinner.tsx
│ │ │ │ ├── switch.tsx
│ │ │ │ ├── textarea.tsx
│ │ │ │ └── typing-animation.tsx
│ │ │ ├── entrypoints/
│ │ │ │ ├── background.ts
│ │ │ │ ├── content.ts
│ │ │ │ ├── hub/
│ │ │ │ │ ├── App.tsx
│ │ │ │ │ ├── hub-ws.ts
│ │ │ │ │ ├── index.html
│ │ │ │ │ └── main.tsx
│ │ │ │ ├── main-world.ts
│ │ │ │ └── sidepanel/
│ │ │ │ ├── App.tsx
│ │ │ │ ├── index.html
│ │ │ │ └── main.tsx
│ │ │ ├── lib/
│ │ │ │ ├── db.ts
│ │ │ │ ├── history-export.ts
│ │ │ │ └── utils.ts
│ │ │ └── types/
│ │ │ ├── assets.d.ts
│ │ │ ├── globals.d.ts
│ │ │ └── markdown.d.ts
│ │ ├── tsconfig.json
│ │ └── wxt.config.js
│ ├── llms/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── OpenAIClient.ts
│ │ │ ├── constants.ts
│ │ │ ├── errors.ts
│ │ │ ├── index.ts
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.dts.json
│ │ ├── tsconfig.json
│ │ └── vite.config.js
│ ├── mcp/
│ │ ├── README.md
│ │ ├── package.json
│ │ └── src/
│ │ ├── hub-bridge.js
│ │ ├── index.js
│ │ └── launcher.html
│ ├── page-agent/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── PageAgent.ts
│ │ │ ├── demo.ts
│ │ │ └── env.d.ts
│ │ ├── tsconfig.dts.json
│ │ ├── tsconfig.json
│ │ ├── vite.config.js
│ │ └── vite.iife.config.js
│ ├── page-controller/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── PageController.ts
│ │ │ ├── actions.ts
│ │ │ ├── dom/
│ │ │ │ ├── dom_tree/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── type.ts
│ │ │ │ ├── getPageInfo.ts
│ │ │ │ └── index.ts
│ │ │ ├── env.d.ts
│ │ │ ├── mask/
│ │ │ │ ├── SimulatorMask.module.css
│ │ │ │ ├── SimulatorMask.ts
│ │ │ │ ├── checkDarkMode.ts
│ │ │ │ └── cursor.module.css
│ │ │ ├── patches/
│ │ │ │ ├── antd.ts
│ │ │ │ └── react.ts
│ │ │ └── utils/
│ │ │ └── index.ts
│ │ ├── tsconfig.dts.json
│ │ ├── tsconfig.json
│ │ └── vite.config.js
│ ├── ui/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── env.d.ts
│ │ │ ├── i18n/
│ │ │ │ ├── index.ts
│ │ │ │ └── locales.ts
│ │ │ ├── index.ts
│ │ │ ├── motion-css/
│ │ │ │ ├── createMotion.ts
│ │ │ │ ├── motion.module.css
│ │ │ │ └── readme
│ │ │ ├── panel/
│ │ │ │ ├── Panel.module.css
│ │ │ │ ├── Panel.ts
│ │ │ │ ├── cards.ts
│ │ │ │ └── types.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.dts.json
│ │ ├── tsconfig.json
│ │ └── vite.config.js
│ └── website/
│ ├── AGENTS.md
│ ├── components.json
│ ├── index.html
│ ├── package.json
│ ├── public/
│ │ └── robots.txt
│ ├── src/
│ │ ├── components/
│ │ │ ├── APIReference.tsx
│ │ │ ├── BetaNotice.tsx
│ │ │ ├── CodeEditor.tsx
│ │ │ ├── Footer.tsx
│ │ │ ├── Header.tsx
│ │ │ ├── Heading.tsx
│ │ │ ├── HighlightSyntax.module.css
│ │ │ ├── HighlightSyntax.tsx
│ │ │ ├── JSConsole.module.css
│ │ │ ├── JSConsole.tsx
│ │ │ ├── LanguageSwitcher.tsx
│ │ │ ├── ThemeSwitcher.tsx
│ │ │ └── ui/
│ │ │ ├── alert.tsx
│ │ │ ├── animated-gradient-text.tsx
│ │ │ ├── animated-shiny-text.tsx
│ │ │ ├── aurora-text.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── bento-grid.tsx
│ │ │ ├── blur-fade.tsx
│ │ │ ├── button.tsx
│ │ │ ├── highlighter.tsx
│ │ │ ├── hyper-text.tsx
│ │ │ ├── kbd.tsx
│ │ │ ├── magic-card.tsx
│ │ │ ├── marquee.tsx
│ │ │ ├── neon-gradient-card.tsx
│ │ │ ├── particles.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── sonner.tsx
│ │ │ ├── sparkles-text.tsx
│ │ │ ├── spinner.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── text-animate.tsx
│ │ │ ├── tooltip.tsx
│ │ │ └── typing-animation.tsx
│ │ ├── constants.ts
│ │ ├── env.d.ts
│ │ ├── hooks/
│ │ │ └── useGitHubStars.ts
│ │ ├── i18n/
│ │ │ └── context.tsx
│ │ ├── index.css
│ │ ├── lib/
│ │ │ ├── useDocumentTitle.ts
│ │ │ └── utils.ts
│ │ ├── main.tsx
│ │ ├── pages/
│ │ │ ├── docs/
│ │ │ │ ├── Layout.tsx
│ │ │ │ ├── advanced/
│ │ │ │ │ ├── custom-ui/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── page-agent/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── page-agent-core/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── page-controller/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── security-permissions/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── features/
│ │ │ │ │ ├── chrome-extension/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── custom-instructions/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── custom-tools/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── data-masking/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── models/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── third-party-agent/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── introduction/
│ │ │ │ ├── limitations/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── overview/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── quick-start/
│ │ │ │ │ └── page.tsx
│ │ │ │ └── troubleshooting/
│ │ │ │ └── page.tsx
│ │ │ └── home/
│ │ │ ├── FeaturesSection.tsx
│ │ │ ├── HeroSection.tsx
│ │ │ ├── OneMoreThingSection.tsx
│ │ │ ├── ScenariosSection.tsx
│ │ │ └── index.tsx
│ │ └── router.tsx
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ └── vite.config.js
├── scripts/
│ └── sync-version.js
├── tsconfig.base.json
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: Report a bug
title: '[Bug] '
labels: ['bug']
body:
- type: markdown
attributes:
value: |
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
- type: textarea
id: description
attributes:
label: What happened?
placeholder: Describe the bug and expected behavior
validations:
required: true
- type: textarea
id: code
attributes:
label: Code
render: typescript
placeholder: Minimal reproduction code
validations:
required: false
- type: input
id: browser
attributes:
label: Browser
placeholder: 'Chrome 120, Firefox 119, etc.'
validations:
required: false
- type: input
id: version
attributes:
label: version
placeholder: '0.0.0'
validations:
required: false
- type: checkboxes
id: community
attributes:
label: Community Communication / 社区沟通
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
options:
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
required: true
- label: I will share constructive, actionable suggestions. / 我会提供建设性、可行动的建议。
required: true
- label: I have read the Code of Conduct. / 我已阅读行为准则。
required: true
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Questions & Ideas / 问题与想法(Discussions)
url: https://github.com/alibaba/page-agent/discussions
about: Use Discussions for Q&A and ideation. 使用 Discussions 进行问答与想法交流。
- name: Security Report / 安全问题报告
url: https://github.com/alibaba/page-agent/security/policy
about: Report security vulnerabilities responsibly. 通过安全页面报告漏洞。
- name: Contributing Guide / 贡献指南
url: https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md
about: How to contribute code and ideas. 如何进行贡献与提交代码。
- name: Code of Conduct / 行为准则
url: https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md
about: Community expectations and standards. 社区行为期望与标准。
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature Request
description: Suggest a feature
title: '[Feature] '
labels: ['enhancement']
body:
- type: markdown
attributes:
value: |
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
- type: textarea
id: description
attributes:
label: Feature Description / 功能描述
description: Describe the problem, solution, and any API changes. / 描述问题、解决方案以及 API 变更。
placeholder: |
**Problem**:
What problem does this solve?
**Solution**:
How should this work?
**Proposed API**:
```typescript
// code here
```
validations:
required: true
- type: checkboxes
id: community
attributes:
label: Community Communication / 社区沟通
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
options:
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
required: true
- label: I will share constructive, actionable suggestions. / 我会提供建设性、可行动的建议。
required: true
- label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。
required: true
validations:
required: true
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## What
Brief description of changes.
## Type
- [ ] Bug fix
- [ ] Feature / Improvement
- [ ] Refactor
- [ ] Documentation
- [ ] Website
- [ ] Demo / Testing
- [ ] Breaking change
## Testing
- [ ] Tested in modern browsers
- [ ] No console errors
- [ ] Types/doc added
Closes #(issue)
## Requirements / 要求
- [ ] I have read and follow the [Code of Conduct](../docs/CODE_OF_CONDUCT.md) and [Contributing Guide](../CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
- [ ] This PR is NOT generated by a bot or AI agent acting autonomously. I have authored or meaningfully reviewed every change. / 此 PR 不是由 bot 或 AI 自主生成的,我已亲自编写或充分审查了每一处变更。
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'weekly'
groups:
# 生产依赖 - 小版本更新
production-dependencies:
dependency-type: 'production'
update-types:
- 'minor'
- 'patch'
# 开发依赖 - 小版本更新
development-dependencies:
dependency-type: 'development'
update-types:
- 'minor'
- 'patch'
# Major 更新单独处理(不分组,需要人工审查)
# 安全更新也不分组,Dependabot 会自动优先创建独立 PR
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
groups:
github-actions:
patterns:
- '*'
================================================
FILE: .github/workflows/ci.yml
================================================
permissions:
contents: read
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [24]
steps:
- uses: actions/checkout@v6
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
# test on default version of npm
# - 9.6~10.8 on node@20
# - 11.3~11.6 on node@24
- name: Node and NPM version
run: node --version && npm --version
- name: Install dependencies
run: npm install
- name: Lint
run: npx eslint . && npx prettier --check **/*.ts
- name: Build
run: npm run build
================================================
FILE: .github/workflows/deploy-demo.yml
================================================
name: Deploy Demo
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build demo
run: npm run build:website
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
path: './packages/website/dist'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
tags:
- 'v*'
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
registry-url: 'https://registry.npmjs.org'
# Ensure npm 11.5.1 or later is installed
- name: Update npm
run: npm install -g npm@latest
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build:libs
- name: Publish all public packages
run: |
VERSION=${GITHUB_REF#refs/tags/v}
if [[ "$VERSION" == *"-"* ]]; then
# Prerelease version (e.g., 0.3.0-beta.1) -> extract tag name before the dot
TAG=$(echo "$VERSION" | sed 's/.*-\([a-zA-Z]*\).*/\1/')
npm publish --workspaces --access public --tag "$TAG"
else
npm publish --workspaces --access public
fi
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
# /lib
dist-ssr
*.local
# Editor directories and files
# .vscode/*
# !.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.qoder
# env files
.env
.env.*
# extension
.output
.wxt
# AI
.agent
.claude
.cursor
.gemini
CLAUDE.md
================================================
FILE: .husky/commit-msg
================================================
npx --no -- commitlint --edit $1
================================================
FILE: .husky/pre-commit
================================================
npx lint-staged --allow-empty
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}
================================================
FILE: .vscode/settings.json
================================================
{
"cSpell.words": [
"contenteditable",
"deepseek",
"historychange",
"HITL",
"innerhtml",
"languagedetector",
"llms",
"magicui",
"npmmirror",
"onwarn",
"opensource",
"qwen",
"retryable",
"shadcn",
"sidepanel",
"statuschange",
"wouter"
],
"files.exclude": {
"packages/*/node_modules": true
},
"markdownlint.config": {
// "comment": "Relaxed rules",
"default": true,
"whitespace": false,
"line_length": false,
"ul-indent": false,
"no-inline-html": false,
"no-bare-urls": false,
"fenced-code-language": false,
"first-line-h1": false,
"block-spacing": false,
"blanks-around-lists": false,
"ol-prefix": false,
"no-duplicate-heading": false
}
}
================================================
FILE: AGENTS.md
================================================
# Instructions for Coding Assistants
## Project Overview
This is a **monorepo** with npm workspaces:
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
- **Extension** (`packages/extension/`) - Browser extension (WXT + React) 🚧 WIP
- **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**
Internal packages:
- **Core** (`packages/core/`) - PageAgentCore without UI (npm: `@page-agent/core`)
- **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model
- **Page Controller** (`packages/page-controller/`) - DOM operations and visual feedback (SimulatorMask), independent of LLM
- **UI** (`packages/ui/`) - Panel and i18n. Decoupled from PageAgent
## Development Commands
```bash
npm start # Start website dev server
npm run build # Build all packages
npm run build:libs # Build all libraries
npm run lint # ESLint with TypeScript strict rules
npm run zip -w @page-agent/ext # Zip the extension package
```
## Architecture
### Monorepo Structure
Simple monorepo solution: TypeScript references + Vite aliases. Update tsconfig and vite config when adding/removing packages.
```
packages/
├── core/ # npm: "@page-agent/core" ⭐ Core agent logic (headless)
├── page-agent/ # npm: "page-agent" entry class (with UI + controller + demo builds)
├── website/ # @page-agent/website (private)
├── llms/ # @page-agent/llms
├── extension/ # Browser extension (WXT + React)
├── page-controller/ # @page-agent/page-controller
└── ui/ # @page-agent/ui
```
`workspaces` in `package.json` must be in topological order.
### Module Boundaries
- **Page Agent**: Main entry with UI. Extends PageAgentCore and adds Panel. Imports from `@page-agent/core`, `@page-agent/ui`
- **Core**: PageAgentCore without UI. Imports from `@page-agent/llms`, `@page-agent/page-controller`
- **LLMs**: LLM client with MacroToolInput contract. No dependency on page-agent
- **UI**: Panel and i18n. Decoupled from PageAgent via PanelAgentAdapter interface
- **Page Controller**: DOM operations with optional visual feedback (SimulatorMask). No LLM dependency. Enable mask via `enableMask: true` config
### PageController ↔ PageAgent Communication
All communication is async and isolated:
```typescript
// PageAgent delegates DOM operations to PageController
await this.pageController.updateTree()
await this.pageController.clickElement(index)
await this.pageController.inputText(index, text)
await this.pageController.scroll({ down: true, numPages: 1 })
// PageController exposes state via async methods
const simplifiedHTML = await this.pageController.getSimplifiedHTML()
const pageInfo = await this.pageController.getPageInfo()
```
### DOM Pipeline
1. **DOM Extraction**: Live DOM → `FlatDomTree` via `page-controller/src/dom/dom_tree/`
2. **Dehydration**: DOM tree → simplified text for LLM
3. **LLM Processing**: AI returns action plans (page-agent)
4. **Indexed Operations**: PageAgent calls PageController by element index
## Key Files Reference
### Page Agent (`packages/page-agent/`)
| File | Description |
| ------------------ | -------------------------------------------- |
| `src/PageAgent.ts` | ⭐ Main class with UI, extends PageAgentCore |
| `src/demo.ts` | IIFE demo entry (auto-init with demo API) |
### Core (`packages/core/`)
| File | Description |
| ---------------------- | --------------------------------------- |
| `src/PageAgentCore.ts` | ⭐ Core agent class without UI |
| `src/tools/` | Tool definitions calling PageController |
| `src/config/` | Configuration types and constants |
| `src/prompts/` | System prompt templates |
### LLMs (`packages/llms/`)
| File | Description |
| --------------------- | ------------------------------------- |
| `src/index.ts` | ⭐ LLM class with retry logic |
| `src/types.ts` | MacroToolInput, AgentBrain, LLMConfig |
| `src/OpenAIClient.ts` | OpenAI-compatible client |
### Page Controller (`packages/page-controller/`)
| File | Description |
| --------------------------- | ---------------------------------------------------------- |
| `src/PageController.ts` | ⭐ Main controller class with optional mask support |
| `src/SimulatorMask.ts` | Visual overlay blocking user interaction during automation |
| `src/actions.ts` | Element interactions (click, input, scroll) |
| `src/dom/dom_tree/index.js` | Core DOM extraction engine |
## Adding New Features
### New Agent Tool
1. Implement in `packages/core/src/tools/index.ts`
2. If tool needs DOM ops, add method to PageController first
3. Tool calls `this.pageController.methodName()` for DOM interactions
### New PageController Action
1. Add implementation in `packages/page-controller/src/actions.ts`
2. Expose via async method in `PageController.ts`
3. Export from `packages/page-controller/src/index.ts`
## Code Standards
- Explicit typing for exported/public APIs
- ESLint relaxes some unsafe rules for rapid iteration
- Every change you make should not only implement the desired functionality but also improve the quality of the codebase
- All code and comments must be in English.
- Do not try to hide errors or risks. They are valuable feedbacks for developers and users. Make them visible and actionable.
- Traceability and predictability is more important than success rate.
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to PageAgent
♥️ We welcome contributions from everyone.
## 🚀 Quick Start
### Development Setup
1. **Prerequisites**
- `macOS` / `Linux` / `WSL`
- `node.js >= 20` with `npm >= 10`
- An editor that supports `ts/eslint/prettier`
- Make sure `eslint`, `prettier` and `commitlint` work well. Un-linted code won't pass the CI.
2. **Setup**
```bash
npm i
npm start # Start demo and documentation site
npm run build # Build libs and website
```
### Project Structure
This is a **monorepo** with npm workspaces containing **4 main packages**:
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
- **Core** (`packages/core/`) - Core agent logic without UI (npm: `@page-agent/core`)
- **Extension** (`packages/extension/`) - Chrome extension for multi-page tasks and browser-level automation
- **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
> We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
>
> - When developing. Use alias so that we don't have to pre-build.
> - When bundling. Use external and disable ts `paths` alias.
> - When bundling `IIFE` and `Website`. Bundle everything together.
## 🤝 How to Contribute
### Reporting Issues
- Use the GitHub issue tracker to report bugs or request features
- Search existing issues before creating new ones
- Provide clear reproduction steps for bugs
- Include browser version and environment details
### Code Contributions
1. **Fork and Clone**
```bash
git clone https://github.com/your-username/page-agent.git
cd page-agent
```
2. **Create Feature Branch**
```bash
git checkout -b feat/your-feature-name
```
3. **Make Changes**
- Follow existing code style and patterns
- Add tests for new functionality
- Update documentation as needed
4. **Test Your Changes**
- Build and lint everything.
- Test in our demo website
- Test it on other websites if applicable
- `@TODO: test suite`
5. **Commit and Push**
```bash
git add .
git commit -m "feat: add awesome feature"
git push origin feat/your-feature-name
```
6. **Create Pull Request**
- Provide clear description of changes
- Link related issues
- Include screenshots for UI changes
## 📝 Code Style
### General Guidelines
- Use TypeScript for type safety
- Follow existing naming conventions
- Write meaningful commit messages
- Keep functions small and focused
- Add JSDoc for public APIs
### Vibe Coding with AI
> [Vibe coding](https://en.wikipedia.org/wiki/Vibe_coding)
- Vibe coding is **RECOMMENDED** when maintaining **the demo, the website, the UI and tests**.
- We have a [website/AGENTS.md](packages/website/AGENTS.md) for that.
- Vibe coding is **NOT** allowed for the core lib!!!
- NEVER try to vibe coding the MV3 extension!!! It is HELL.
- Review anything AI wrote before make a commit. You are the author of anything you commit. NOT AI.
If your AI assistant does not support [AGENTS.md](https://agents.md/). Add an alias for it:
- claude-code (`CLAUDE.md`)
```markdown
@AGENTS.md
```
- antigravity (`.agent/rules/alias.md`)
```markdown
---
trigger: always_on
---
@../../AGENTS.md
```
## 🔧 Development Workflows
### Test With Your Own LLM API
- Create a `.env` file in the repo root with your LLM API config
```env
LLM_MODEL_NAME=gpt-5.2
LLM_API_KEY=your-api-key
LLM_BASE_URL=https://api.your-llm-provider.com/v1
```
- **Ollama example** (tested on 0.15 + qwen3:14b, RTX3090 24GB):
```env
LLM_BASE_URL="http://localhost:11434/v1"
LLM_API_KEY="NA"
LLM_MODEL_NAME="qwen3:14b"
```
> @see https://alibaba.github.io/page-agent/docs/features/models#ollama for configuration
- **Restart the dev server** to load new env vars
- 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).
### Extension Development
```bash
# make sure you ran `npm run build:libs` first
# and every time you changed the core libs
npm run dev -w @page-agent/ext
npm run zip -w @page-agent/ext
```
- Update `packages/extension/docs/extension_api.md` for API integration details
### Testing on Other Websites
- Start and serve a local `iife` script
```bash
npm run dev:demo # Serving IIFE with auto rebuild at http://localhost:5174/page-agent.demo.js
```
- Add a new bookmark
```javascript
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);})();
```
- Click the bookmark on any page to load Page-Agent
> Warning: AK in your local `.env` will be inlined in the iife script. Be very careful when you distribute the script.
### Adding Documentation
Ask an AI to help you add documentation to the `website/` package. Follow the existing style.
> Our AGENTS.md file and guardrails are designed for this purpose. But please be careful and review anything AI generated.
## 🚫 What We Don't Accept
- Breaking changes and large PRs without prior discussion
- Heavy dependencies to core libs
- Contributions without proper testing
- Code that doesn't follow project conventions
- Dependencies or code with licenses incompatible with MIT
- Bot or AI-generated pull requests without meaningful human involvement
## 📄 Legal
By contributing to this project, you agree that your contributions will be licensed under the MIT License.
> CLA is optional.
## 💬 Questions?
- Open a GitHub issue for technical questions
- Check existing documentation and issues first
- Be respectful and constructive in discussions
Thank you for helping make PageAgent better! 🎉
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2026 SimonLuvRamen
Copyright (c) 2026 Alibaba Group Holding Limited
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Page Agent
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
</picture>
[](https://opensource.org/licenses/MIT) [](http://www.typescriptlang.org/) [](https://bundlephobia.com/package/page-agent) [](https://www.npmjs.com/package/page-agent) [](https://github.com/alibaba/page-agent)
The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
🌐 **English** | [中文](./docs/README-zh.md)
<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>
<video id="demo-video" src="https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2" controls crossorigin muted></video>
---
## ✨ Features
- **🎯 Easy integration**
- No need for `browser extension` / `python` / `headless browser`.
- Just in-page javascript. Everything happens in your web page.
- **📖 Text-based DOM manipulation**
- No screenshots. No multi-modal LLMs or special permissions needed.
- **🧠 Bring your own LLMs**
- **🎨 Pretty UI with human-in-the-loop**
- **🐙 Optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension) for multi-page tasks.**
## 💡 Use Cases
- **SaaS AI Copilot** — Ship an AI copilot in your product in lines of code. No backend rewrite.
- **Smart Form Filling** — Turn 20-click workflows into one sentence. Perfect for ERP, CRM, and admin systems.
- **Accessibility** — Make any web app accessible through natural language. Voice commands, screen readers, zero barrier.
- **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).
## 🚀 Quick Start
### One-line integration
Fastest way to try PageAgent with our free Demo LLM:
```html
<script src="{URL}" crossorigin="true"></script>
```
> **⚠️ 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).
| Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.6.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.6.0/files/dist/iife/page-agent.demo.js |
### NPM Installation
```bash
npm install page-agent
```
```javascript
import { PageAgent } from 'page-agent'
const agent = new PageAgent({
model: 'qwen3.5-plus',
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: 'YOUR_API_KEY',
language: 'en-US',
})
await agent.execute('Click the login button')
```
For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/docs/introduction/overview).
## 🤝 Contributing
We welcome contributions from the community! Follow our instructions in [CONTRIBUTING.md](CONTRIBUTING.md) for setup and guidelines.
Please read [Code of Conduct](docs/CODE_OF_CONDUCT.md) before contributing.
Contributions generated entirely by bots or agents without substantial human involvement will not be accepted, and bot accounts may be blocked.
## 👏 Acknowledgments
This project builds upon the excellent work of **[`browser-use`](https://github.com/browser-use/browser-use)**.
`PageAgent` is designed for **client-side web enhancement**, not server-side automation.
```
DOM processing components and prompt are derived from browser-use:
Browser Use <https://github.com/browser-use/browser-use>
Copyright (c) 2024 Gregor Zunic
Licensed under the MIT License
We gratefully acknowledge the browser-use project and its contributors for their
excellent work on web automation and DOM interaction patterns that helped make
this project possible.
Third-party dependencies and their licenses can be found in the package.json
file and in the node_modules directory after installation.
```
## 📄 License
[MIT License](LICENSE)
---
**⭐ Star this repo if you find PageAgent helpful!**
<a href="https://www.star-history.com/?repos=alibaba%2Fpage-agent&type=date">
<picture>
<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" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
</picture>
</a>
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Supported Versions
We provide security fixes on a best-effort basis for:
| Version | Supported |
| --------------------------------------------------------- | --------- |
| `main` | Yes |
| Latest npm release of `page-agent` and workspace packages | Yes |
| Older releases | No |
Please upgrade to the latest release before reporting an issue against an older build.
## Reporting a Vulnerability
Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.
Use GitHub's private vulnerability reporting flow:
- Open https://github.com/alibaba/page-agent/security/policy
- Click `Report a vulnerability`
If private reporting is unavailable, open a minimal public issue only to request a private contact channel. Do not include exploit details.
## What to Include
- Affected package or feature
- Exact version, commit, or build
- Browser, OS, and runtime environment
- Reproduction steps or a proof of concept
- Expected impact
## Scope
We prioritize reports that show a real security boundary failure, such as:
- Unauthorized access to data, tokens, or extension capabilities
- Bypassing explicit safety constraints
- Sensitive data exposure caused by default behavior
The following usually do not qualify by themselves:
- Unsafe custom integrations that ignore documented safeguards
- Intentionally embedding secrets into client-side builds
- Reports against unsupported older versions
## Disclosure
Please avoid public disclosure until maintainers have had a reasonable chance to investigate and ship a fix.
================================================
FILE: docs/CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.6.0] - 2026-03-21
### Features
- **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
- **Better iframe handling** - Same-origin iframe elements are handled more reliably during DOM extraction and actions
- **Extension history workflows** - Users can rerun past tasks, export history sessions as JSON, and approve MCP-triggered tasks before execution
### Improvements
- **Unified versioning across packages** - The extension now follows the root workspace version. Changelog entries are no longer split into a separate extension version section
- **Configurable `stepDelay`** - Agent pacing between steps is now configurable via `stepDelay`
- **Optional API key** - `apiKey` can now be omitted for compatible deployments that do not require one
- **Optional named tool choice** - Tool invocation can disable named tool choice for providers that behave better without it
- **Better rich-text input support** - Improved `contenteditable` handling with better event dispatching and `execCommand` fallback for more editors
- **More flexible DOM extraction** - `includeAttributes` now supports wildcards, `contenteditable` is included by default, and heuristically interactive elements expose more useful attributes
- **MiniMax model support** - Added MiniMax compatibility, with the default recommendation updated to `MiniMax-M2.7`
### Bug Fixes
- Fixed Safari issues when `requestIdleCallback` is unavailable
- Avoid throwing when `webgl2` initialization fails
- Improved OpenAI-compatible request patches for GPT-5.4 chat tools and MiniMax temperature/tool-call compatibility
- Fixed several UI polish issues in the extension and website, including cursor and layout regressions
## [1.5.1] - 2026-03-05
### Breaking Changes
- **`data-browser-use-ignore` → `data-page-agent-ignore`** - DOM ignore attribute renamed to match the project identity
- **Config types restructured** - `PageAgentConfig` split into `AgentConfig` + `PageAgentCoreConfig`; config definitions moved from `config/index.ts` to `types.ts`
- **Zod v3/v4 dual support** - Libraries now accept both `zod@^3.25` and `zod@^4.0` as peer dependencies
### Features
- **Experimental `llms.txt` support** - Agent can fetch and include a site's `llms.txt` in context. Enable via `experimentalLlmsTxt: true`
### Improvements
- Default `maxSteps` changed from 20 to 40 for better for complex tasks out of the box
- Added 400ms wait between agent steps for page reactions
- Increased click wait time (100ms → 200ms) for more reliable interactions
- Removed debug `console.log` statements from scroll actions
- Reset observations on new task start
- Improved logging across packages
### Extension v0.1.9
> PageAgent 1.5.1
- **Advanced config panel** - New collapsible section exposing Max Steps, System Instruction, and experimental `llms.txt` toggle
- Streamlined User Auth Token description
- Moved testing API notice below auth token section
---
## [1.4.0] - 2026-02-27
### Features
- Update Terms of Use and Privacy Policy
- **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
- **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
- **Qwen model updates** - Added `qwen3.5-plus` as the default free testing model; disabled `enable_thinking` for Qwen models to avoid incompatible responses
- **Updated default LLM endpoint** - Migrated demo and extension to a new testing endpoint with legacy endpoint auto-migration
### Improvements
- Unified zod imports (`* as z`) across all packages for consistency
- Better Zod error formatting with `z.prettifyError()` in LLM client
- Exported `InvokeError` and `InvokeErrorType` as values (not just types) from `@page-agent/llms`
- Exported `SupportedLanguage` type from `@page-agent/core`
### Extension v0.1.8
- **Language setting** - Added language selector (System / English / 中文) in config panel
- **UI makeover** - New empty state with breathing glow and typing animation; ai-motion glow overlay while running; refined focus styles
- **Testing endpoint notice** - Shows terms of use notice when using the free testing API
- **Legacy endpoint migration** - Auto-migrates old Supabase testing endpoint to new endpoint on startup
---
## [1.3.0] - 2026-02-13
### Breaking Changes
- **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`.
### Features
- **Panel action button** - The panel button now morphs between Stop (■) and Close (X) based on agent status
- **Error history** - Errors and max-step failures are now recorded in `history` as `AgentErrorEvent`, making post-task analysis more complete
### Bug Fixes
- **AbortError handling** - `AbortError` is no longer retried by the LLM client, and shows a clean "Task stopped" message instead of a raw error stack
---
## [1.2.0] - 2026-02-11
### Features
- **Observe Phase** - Agent now observes the page before each action, improving decision accuracy on dynamic pages
- **Better Abort Handling** - Improved `abortSignal` support for cleaner task cancellation
### Improvements
- Pruned system prompts for lower token usage and faster responses
- Improved error handling during agent steps with better error messages
- Zod tree-shaking for smaller bundle size
### Bug Fixes
- Fixed indentation lost in DOM extraction caused by `trimLines`
- Fixed `gpt-5-mini` temperature configuration
---
## [1.1.0] - 2026-02-02
### Features
- **Custom System Prompt** - New `systemPrompt` config option to customize or extend the default system prompt
- **Chrome Extension** - Extension with multi-tab control, main-world API with token auth, and tab lifecycle management
### Improvements
- Renamed `include_attributes` to `includeAttributes` in PageController config (camelCase consistency)
- Lazy-loaded mask module for faster initialization
- Better date formatting and error messages from LLM client
- Added `rawRequest` to step history for easier debugging
### Bug Fixes
- Fixed CSP errors by using local SVGs for cursor mask instead of inline styles
- Fixed `AbortError` being incorrectly retried and shown to users
- Fixed mask not working correctly when starting a new task after stopping a previous one
---
## [1.0.0] - 2026-01-19
### 🎉 First Stable Release
PageAgent is now ready for production use. The API is stable and breaking changes will follow semantic versioning.
### Features
#### Core
- **PageAgent** - Main entry class with built-in UI Panel
- **PageAgentCore** - Headless agent class for custom UI or programmatic use
- **DOM Analysis** - Text-based DOM extraction with high-intensity dehydration
- **LLM Support** - Works with OpenAI, Claude, DeepSeek, Qwen, and other OpenAI-compatible APIs
- **Tool System** - Built-in tools for click, input, scroll, select, and more
- **Custom Tools** - Extend agent capabilities with your own tools (experimental)
- **Lifecycle Hooks** - Hook into agent execution (experimental)
- **Instructions System** - System-level and page-level instructions to guide agent behavior
- **Data Masking** - Transform page content before sending to LLM
#### Page Controller
- **Element Interactions** - Click, input text, select options, scroll
- **Visual Mask** - Blocks user interaction during automation
- **DOM Tree Extraction** - Efficient page structure extraction for LLM consumption
#### UI
- **Interactive Panel** - Real-time task progress and agent thinking display
- **Ask User Tool** - Agent can ask users for clarification
- **i18n Support** - English and Chinese localization
### Packages
| Package | Description |
| ----------------------------- | ---------------------------------- |
| `page-agent` | Main entry with UI Panel |
| `@page-agent/core` | Core agent logic without UI |
| `@page-agent/llms` | LLM client with retry logic |
| `@page-agent/page-controller` | DOM operations and visual feedback |
| `@page-agent/ui` | Panel and i18n |
### Known Limitations
- Single-page application only (cannot navigate across pages)
- No visual recognition (relies on DOM structure)
- Limited interaction support (no hover, drag-drop, canvas operations)
- See [Limitations](https://alibaba.github.io/page-agent/docs/introduction/limitations) for details
### Acknowledgments
This 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).
================================================
FILE: docs/CODE_OF_CONDUCT.md
================================================
# Alibaba Open Source Code of Conduct
[¶中文版](#我们的保证)
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at opensource@alibaba-inc.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
---
> Chinese Version
> 《阿里巴巴开源行为准则》
## 我们的保证
为了促进一个开放透明且友好的环境,我们作为贡献者和维护者保证:无论年龄、种族、民族、性别认同和表达(方式)、体型、身体健全与否、经验水平、国籍、个人表现、宗教或性别取向,参与者在我们项目和社区中都免于骚扰。
## 我们的标准
有助于创造正面环境的行为包括但不限于:
* 使用友好和包容性语言
* 尊重不同的观点和经历
* 耐心地接受建设性批评
* 关注对社区最有利的事情
* 友善对待其他社区成员
身为参与者不能接受的行为包括但不限于:
* 使用与性有关的言语或是图像,以及不受欢迎的性骚扰
* 捣乱/煽动/造谣的行为或进行侮辱/贬损的评论,人身攻击及政治攻击
* 公开或私下的骚扰
* 未经许可地发布他人的个人资料,例如住址或是电子地址
* 其他可以被合理地认定为不恰当或者违反职业操守的行为
## 我们的责任
项目维护者有责任为「可接受的行为」标准做出诠释,以及对已发生的不被接受的行为采取恰当且公平的纠正措施。
项目维护者有权利及责任去删除、编辑、拒绝与本行为标准有所违背的评论 (comments)、提交 (commits)、代码、wiki 编辑、问题 (issues) 和其他贡献,以及项目维护者可暂时或永久性的禁止任何他们认为有不适当、威胁、冒犯、有害行为的贡献者。
## 使用范围
当一个人代表该项目或是其社区时,本行为标准适用于其项目平台和公共平台。
代表项目或是社区的情况,举例来说包括使用官方项目的电子邮件地址、通过官方的社区媒体账号发布或线上或线下事件中担任指定代表。
该项目的呈现方式可由其项目维护者进行进一步的定义及解释。
## 强制执行
可以通过 opensource@alibaba-inc.com 来联系项目团队来举报滥用、骚扰或其他不被接受的行为。
任何维护团队认为有必要且适合的所有投诉都将进行审查及调查,并做出相对应的回应。项目小组有对事件回报者有保密的义务。具体执行的方针近一步细节可能会单独公布。
没有切实地遵守或是执行本行为标准的项目维护人员,可能会因项目领导人或是其他成员的决定,暂时或是永久地取消其参与资格。
## 来源
本行为标准改编自[贡献者公约](https://www.contributor-covenant.org),版本 1.4
可在此查看[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)
================================================
FILE: docs/README-zh.md
================================================
# Page Agent
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
</picture>
[](https://opensource.org/licenses/MIT) [](http://www.typescriptlang.org/) [](https://bundlephobia.com/package/page-agent) [](https://www.npmjs.com/package/page-agent) [](https://github.com/alibaba/page-agent)
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
🌐 [English](../README.md) | **中文**
<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>
<video id="demo-video" src="https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2" controls crossorigin muted></video>
---
## ✨ Features
- **🎯 轻松集成**
- 无需 `浏览器插件` / `Python` / `无头浏览器`。
- 纯页面内 JavaScript,一切都在你的网页中完成。
- **📖 基于文本的 DOM 操作**
- 无需截图,无需多模态模型或特殊权限。
- **🧠 用你自己的 LLM**
- **🎨 精美 UI,支持人机协同**
- **🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),支持跨页面任务。**
## 💡 应用场景
- **SaaS AI 副驾驶** — 几行代码为你的产品加上 AI 副驾驶,无需重写后端。
- **智能表单填写** — 把 20 次点击变成一句话。ERP、CRM、管理后台的最佳拍档。
- **无障碍增强** — 用自然语言让任何网页无障碍。语音指令、屏幕阅读器,零门槛。
- **跨页面 Agent** — 通过可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),让你自己的 Agent 跨标签页工作。
## 🚀 快速开始
### 一行代码集成
通过我们免费的 Demo LLM 快速体验 PageAgent:
```html
<script src="{URL}" crossorigin="true"></script>
```
> **⚠️ 仅用于技术评估。** 该 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)。
| Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.6.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.6.0/files/dist/iife/page-agent.demo.js |
### NPM 安装
```bash
npm install page-agent
```
```javascript
import { PageAgent } from 'page-agent'
const agent = new PageAgent({
model: 'qwen3.5-plus',
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: 'YOUR_API_KEY',
language: 'zh-CN',
})
await agent.execute('点击登录按钮')
```
更多编程用法,请参阅 [📖 文档](https://alibaba.github.io/page-agent/docs/introduction/overview)。
## 🤝 贡献
欢迎社区贡献!请参阅 [CONTRIBUTING.md](../CONTRIBUTING.md) 了解安装与贡献指南。请在贡献前阅读[行为准则](CODE_OF_CONDUCT.md)。
我们不接受未经实质性人类参与、完全由 Bot 或 Agent 自动生成的代码,机器人账号可能被禁止参与互动。
## 👏 致谢
本项目基于 **[`browser-use`](https://github.com/browser-use/browser-use)** 的优秀工作构建。
`PageAgent` 专为**客户端网页增强**设计,不是服务端自动化工具。
```
DOM processing components and prompt are derived from browser-use:
Browser Use <https://github.com/browser-use/browser-use>
Copyright (c) 2024 Gregor Zunic
Licensed under the MIT License
We gratefully acknowledge the browser-use project and its contributors for their
excellent work on web automation and DOM interaction patterns that helped make
this project possible.
Third-party dependencies and their licenses can be found in the package.json
file and in the node_modules directory after installation.
```
## 📄 许可证
[MIT License](../LICENSE)
---
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
<a href="https://www.star-history.com/?repos=alibaba%2Fpage-agent&type=date">
<picture>
<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" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
</picture>
</a>
================================================
FILE: docs/terms-and-privacy.md
================================================
# Terms of Use & Privacy
**Last updated:** March 2026
"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.
---
## 1. Open Source Software Privacy
The 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.
All data transmission occurs **only** between your browser and the LLM provider you configure. You are in full control of which provider receives your data.
The 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
---
## 2. Testing API and Demo Disclaimer & Terms of Use
To 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.
This 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:
- **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.
- **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.
- **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.
- **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.
- **No Guaranteed Availability**: This testing API may be rate-limited, degraded, or discontinued at any time without prior notice.
- **"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.
- **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).
**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.
**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).
---
## 3. Browser Extension (Page Agent Ext)
### Data Processing
The 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.
Data is transmitted to external servers **only when you initiate an automation task**. When this occurs:
- Your task instructions (natural language commands)
- Simplified page structure (cleaned HTML) of all pages under the extension's control
are sent to the LLM API endpoint configured in **your settings**.
> **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.
**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.
**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.
### Data Storage
- **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)
- **No cloud sync**: Configuration is not synced to any external server
- **No analytics**: The extension does not include any analytics or tracking code
### Your Control
- The extension is open source and can be audited by anyone
- You choose which LLM provider to use
- You may configure your own API endpoint at any time
- You can clear all stored data by removing the extension
---
## Changes
We may update these terms at our discretion.
## Contact
https://github.com/alibaba/page-agent/issues
================================================
FILE: eslint.config.js
================================================
import js from '@eslint/js'
import reactDom from 'eslint-plugin-react-dom'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import reactX from 'eslint-plugin-react-x'
import { defineConfig, globalIgnores } from 'eslint/config'
import globals from 'globals'
import tseslint from 'typescript-eslint'
export default defineConfig([
globalIgnores([
'**/dist',
'**/node_modules',
'packages/*/src/components/ui',
'**/.wxt',
'**/.output',
]),
{
plugins: {
'react-hooks': reactHooks,
},
rules: reactHooks.configs.recommended.rules,
},
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
// reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
// Remove tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
// project: ['./tsconfig.json'],
// project: ['./packages/*/tsconfig.json'],
// tsconfigRootDir: import.meta.dirname,
projectService: true,
},
ecmaVersion: 2020,
globals: globals.browser,
},
rules: {
// Add any additional rules here
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-confusing-void-expression': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/no-dynamic-delete': 'off',
'@typescript-eslint/no-unnecessary-condition': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
'@typescript-eslint/no-misused-promises': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/restrict-plus-operands': 'off',
'react-dom/no-missing-button-type': 'off',
'react-x/no-nested-component-definitions': 'off',
'@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',
'@typescript-eslint/no-unnecessary-type-parameters': 'off',
// 'require-await': 'off',
'@typescript-eslint/require-await': 'off',
},
},
])
================================================
FILE: package.json
================================================
{
"name": "root",
"private": true,
"version": "1.6.0",
"type": "module",
"workspaces": [
"packages/page-controller",
"packages/ui",
"packages/llms",
"packages/core",
"packages/page-agent",
"packages/mcp",
"packages/extension",
"packages/website"
],
"description": "AI-powered UI agent for web applications",
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git"
},
"homepage": "https://alibaba.github.io/page-agent/",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"scripts": {
"start": "npm run dev --workspace=@page-agent/website",
"dev:ext": "npm run dev -w @page-agent/ext",
"dev:demo": "npm run dev:demo --workspace=page-agent",
"build": "npm run build:libs && npm run build:website",
"build:libs": "npm run build --workspaces --if-present",
"build:website": "npm run build:website --workspace=@page-agent/website",
"build:ext": "npm run build:libs && npm run zip -w @page-agent/ext",
"version": "node scripts/sync-version.js",
"lint": "eslint .",
"cleanup": "rm -rf packages/*/dist",
"prepare": "husky"
},
"devDependencies": {
"@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0",
"@eslint/js": "^9.39.2",
"@microsoft/api-extractor": "^7.57.7",
"@tailwindcss/vite": "^4.2.1",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.5.0",
"@vitejs/plugin-react-swc": "^4.3.0",
"chalk": "^5.6.2",
"concurrently": "^9.2.1",
"dotenv": "^17.3.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.13.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-react-x": "^2.13.0",
"globals": "^17.4.0",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"prettier": "^3.8.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.1",
"unplugin-dts": "^1.0.0-beta.6",
"vite": "^7.3.1",
"vite-plugin-css-injected-by-js": "^4.0.1",
"vite-bundle-analyzer": "^1.3.6"
},
"overrides": {
"typescript": "^5.9.3"
},
"lint-staged": {
"*.{js,ts,cjs,cts,mjs,mts}": [
"npx prettier --write --ignore-unknown",
"npx eslint --quiet"
],
"*.{jsx,tsx}": [
"npx prettier --write --ignore-unknown",
"npx eslint --quiet"
],
"*.css": [
"npx prettier --write --ignore-unknown"
]
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
],
"rules": {
"subject-case": [
0,
"never"
]
}
},
"prettier": {
"singleQuote": true,
"semi": false,
"useTabs": true,
"printWidth": 100,
"trailingComma": "es5",
"plugins": [
"@trivago/prettier-plugin-sort-imports"
],
"importOrder": [
"<THIRD_PARTY_MODULES>",
"^(@/).*(?<!css)$",
"^[./].*(?<!css)$",
".css$"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"overrides": [
{
"files": "*.md",
"options": {
"useTabs": false,
"tabWidth": 4
}
},
{
"files": "*.json",
"options": {
"useTabs": false,
"tabWidth": 4
}
}
]
}
}
================================================
FILE: packages/core/package.json
================================================
{
"name": "@page-agent/core",
"private": false,
"version": "1.6.0",
"type": "module",
"main": "./dist/esm/page-agent-core.js",
"module": "./dist/esm/page-agent-core.js",
"types": "./dist/esm/PageAgentCore.d.ts",
"exports": {
".": {
"types": "./dist/esm/PageAgentCore.d.ts",
"import": "./dist/esm/page-agent-core.js",
"default": "./dist/esm/page-agent-core.js"
}
},
"files": [
"dist/"
],
"description": "GUI agent for web applications - add intelligent automation to any webpage with a single script",
"keywords": [
"ai",
"automation",
"ui-agent",
"GUI-agent",
"browser-automation",
"web-agent",
"llm",
"dom-interaction",
"web-automation",
"GUI-simulation"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"dev:iife": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"chalk": "^5.6.2",
"@page-agent/llms": "1.6.0",
"@page-agent/page-controller": "1.6.0"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"zod": "^4.3.5"
}
}
================================================
FILE: packages/core/src/PageAgentCore.ts
================================================
/**
* Copyright (C) 2025 Alibaba Group Holding Limited
* Copyright (C) 2026 SimonLuvRamen
* All rights reserved.
*/
import { InvokeError, LLM, type Tool } from '@page-agent/llms'
import type { BrowserState, PageController } from '@page-agent/page-controller'
import chalk from 'chalk'
import * as z from 'zod/v4'
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
import { tools } from './tools'
import type {
AgentActivity,
AgentConfig,
AgentReflection,
AgentStatus,
AgentStepEvent,
ExecutionResult,
HistoricalEvent,
MacroToolInput,
MacroToolResult,
} from './types'
import { assert, fetchLlmsTxt, normalizeResponse, uid, waitFor } from './utils'
export { tool, type PageAgentTool } from './tools'
export type * from './types'
export type PageAgentCoreConfig = AgentConfig & { pageController: PageController }
/**
* AI agent for browser automation.
*
* @remarks
* ## Re-act Agent Loop
* - step
* - observe (gather information about current environment and context)
* - think (LLM calling)
* - reflection (evaluate history, generate memory, short-term planning)
* - action (give the action to approach the next goal)
* - act (execute the action)
* - loop
*
* ## Event System
* - `statuschange` - Agent status transitions (idle → running → completed/error)
* - `historychange` - History events updated (persistent, part of agent memory)
* - `activity` - Real-time activity feedback (transient, for UI only)
* - `dispose` - Agent cleanup triggered
*
* ## Information Streams
* 1. **History Events** (`history` array)
* - Persistent event stream that forms agent's memory
* - Included in LLM context across steps
* - Types: steps, observations, user takeovers, llm errors
*
* 2. **Activity Events** (via `activity` event)
* - Transient UI feedback during task execution
* - NOT included in LLM context
* - Types: thinking, executing, executed, retrying, error
*/
export class PageAgentCore extends EventTarget {
readonly id = uid()
readonly config: PageAgentCoreConfig & { maxSteps: number }
readonly tools: typeof tools
/** PageController for DOM operations */
readonly pageController: PageController
task = ''
taskId = ''
/** History events */
history: HistoricalEvent[] = []
/** Whether this agent has been disposed */
disposed = false
/**
* Callback for when agent needs user input (ask_user tool)
* If not set, ask_user tool will be disabled
* @example onAskUser: (q) => window.prompt(q) || ''
*/
onAskUser?: (question: string) => Promise<string>
#status: AgentStatus = 'idle'
#llm: LLM
#abortController = new AbortController()
#observations: string[] = []
/** internal states during a single task execution */
#states = {
/** Accumulated wait time in seconds */
totalWaitTime: 0,
/** For detecting navigation */
lastURL: '',
/** Browser state */
browserState: null as BrowserState | null,
}
constructor(config: PageAgentCoreConfig) {
super()
this.config = { ...config, maxSteps: config.maxSteps ?? 40 }
this.#llm = new LLM(this.config)
this.tools = new Map(tools)
this.pageController = config.pageController
// Listen to LLM retry events
this.#llm.addEventListener('retry', (e) => {
const { attempt, maxAttempts } = (e as CustomEvent).detail
this.#emitActivity({ type: 'retrying', attempt, maxAttempts })
// Also push to history for panel rendering
this.history.push({
type: 'retry',
message: `LLM retry attempt ${attempt} of ${maxAttempts}`,
attempt,
maxAttempts,
})
this.#emitHistoryChange()
})
this.#llm.addEventListener('error', (e) => {
const error = (e as CustomEvent).detail.error as Error | InvokeError
if ((error as any)?.rawError?.name === 'AbortError') return
const message = String(error)
this.#emitActivity({ type: 'error', message })
// Also push to history for panel rendering
this.history.push({
type: 'error',
message,
rawResponse: (error as InvokeError).rawResponse,
})
this.#emitHistoryChange()
})
if (this.config.customTools) {
for (const [name, tool] of Object.entries(this.config.customTools)) {
if (tool === null) {
this.tools.delete(name)
continue
}
this.tools.set(name, tool)
}
}
if (!this.config.experimentalScriptExecutionTool) {
this.tools.delete('execute_javascript')
}
}
/** Get current agent status */
get status(): AgentStatus {
return this.#status
}
/** Emit statuschange event */
#emitStatusChange(): void {
this.dispatchEvent(new Event('statuschange'))
}
/** Emit historychange event */
#emitHistoryChange(): void {
this.dispatchEvent(new Event('historychange'))
}
/**
* Emit activity event - for transient UI feedback
* @param activity - Current agent activity
*/
#emitActivity(activity: AgentActivity): void {
this.dispatchEvent(new CustomEvent('activity', { detail: activity }))
}
/** Update status and emit event */
#setStatus(status: AgentStatus): void {
if (this.#status !== status) {
this.#status = status
this.#emitStatusChange()
}
}
/**
* Push an observation message to the history event stream.
* This will be visible in <agent_history> and remain persistent in memory across steps.
* @experimental @internal
* @note history change will be emitted before next step starts
*/
pushObservation(content: string): void {
this.#observations.push(content)
}
/** Stop the current task. Agent remains reusable. */
stop() {
this.pageController.cleanUpHighlights()
this.pageController.hideMask()
this.#abortController.abort()
}
async execute(task: string): Promise<ExecutionResult> {
if (this.disposed) throw new Error('PageAgent has been disposed. Create a new instance.')
if (!task) throw new Error('Task is required')
this.task = task
this.taskId = uid()
// Disable ask_user tool if onAskUser is not set
if (!this.onAskUser) {
this.tools.delete('ask_user')
}
const onBeforeStep = this.config.onBeforeStep
const onAfterStep = this.config.onAfterStep
const onBeforeTask = this.config.onBeforeTask
const onAfterTask = this.config.onAfterTask
await onBeforeTask?.(this)
// Show mask
await this.pageController.showMask()
if (this.#abortController) {
this.#abortController.abort()
this.#abortController = new AbortController()
}
this.history = []
this.#setStatus('running')
this.#emitHistoryChange()
this.#observations = []
// Reset internal states
this.#states = { totalWaitTime: 0, lastURL: '', browserState: null }
let step = 0
while (true) {
try {
console.group(`step: ${step}`)
await onBeforeStep?.(this, step)
// observe
console.log(chalk.blue.bold('👀 Observing...'))
this.#states.browserState = await this.pageController.getBrowserState()
await this.#handleObservations(step)
// assemble prompts
const messages = [
{ role: 'system' as const, content: this.#getSystemPrompt() },
{ role: 'user' as const, content: await this.#assembleUserPrompt() },
]
const macroTool = { AgentOutput: this.#packMacroTool() }
// invoke LLM
console.log(chalk.blue.bold('🧠 Thinking...'))
this.#emitActivity({ type: 'thinking' })
const result = await this.#llm.invoke(messages, macroTool, this.#abortController.signal, {
toolChoiceName: 'AgentOutput',
normalizeResponse: (res) => normalizeResponse(res, this.tools),
})
// assemble history
const macroResult = result.toolResult as MacroToolResult
const input = macroResult.input
const output = macroResult.output
const reflection: Partial<AgentReflection> = {
evaluation_previous_goal: input.evaluation_previous_goal,
memory: input.memory,
next_goal: input.next_goal,
}
const actionName = Object.keys(input.action)[0]
const action: AgentStepEvent['action'] = {
name: actionName,
input: input.action[actionName],
output: output,
}
this.history.push({
type: 'step',
stepIndex: step,
reflection,
action,
usage: result.usage,
rawResponse: result.rawResponse,
rawRequest: result.rawRequest,
} as AgentStepEvent)
this.#emitHistoryChange()
//
await onAfterStep?.(this, this.history)
console.groupEnd()
// finish task if done
if (actionName === 'done') {
const success = action.input?.success ?? false
const text = action.input?.text || 'no text provided'
console.log(chalk.green.bold('Task completed'), success, text)
this.#onDone(success)
const result: ExecutionResult = {
success,
data: text,
history: this.history,
}
await onAfterTask?.(this, result)
return result
}
} catch (error: unknown) {
console.groupEnd() // to prevent nested groups
const isAbortError = (error as any)?.rawError?.name === 'AbortError'
console.error('Task failed', error)
const errorMessage = isAbortError ? 'Task stopped' : String(error)
this.#emitActivity({ type: 'error', message: errorMessage })
this.history.push({ type: 'error', message: errorMessage, rawResponse: error })
this.#emitHistoryChange()
this.#onDone(false)
const result: ExecutionResult = {
success: false,
data: errorMessage,
history: this.history,
}
await onAfterTask?.(this, result)
return result
}
step++
if (step > this.config.maxSteps) {
const errorMessage = 'Step count exceeded maximum limit'
this.history.push({ type: 'error', message: errorMessage })
this.#emitHistoryChange()
this.#onDone(false)
const result: ExecutionResult = {
success: false,
data: errorMessage,
history: this.history,
}
await onAfterTask?.(this, result)
return result
}
await waitFor(this.config.stepDelay ?? 0.4)
}
}
/**
* Merge all tools into a single MacroTool with the following input:
* - thinking: string
* - evaluation_previous_goal: string
* - memory: string
* - next_goal: string
* - action: { toolName: toolInput }
* where action must be selected from tools defined in this.tools
*/
#packMacroTool(): Tool<MacroToolInput, MacroToolResult> {
const tools = this.tools
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
return z.object({ [toolName]: tool.inputSchema }).describe(tool.description)
})
const actionSchema = z.union(actionSchemas as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]])
const macroToolSchema = z.object({
// thinking: z.string().optional(),
evaluation_previous_goal: z.string().optional(),
memory: z.string().optional(),
next_goal: z.string().optional(),
action: actionSchema,
})
return {
description: 'You MUST call this tool every step!',
inputSchema: macroToolSchema as z.ZodType<MacroToolInput>,
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
// abort
if (this.#abortController.signal.aborted) throw new Error('AbortError')
console.log(chalk.blue.bold('MacroTool input'), input)
const action = input.action
const toolName = Object.keys(action)[0]
const toolInput = action[toolName]
// Build reflection text, only include non-empty fields
const reflectionLines: string[] = []
if (input.evaluation_previous_goal)
reflectionLines.push(`✅: ${input.evaluation_previous_goal}`)
if (input.memory) reflectionLines.push(`💾: ${input.memory}`)
if (input.next_goal) reflectionLines.push(`🎯: ${input.next_goal}`)
const reflectionText = reflectionLines.length > 0 ? reflectionLines.join('\n') : ''
if (reflectionText) {
console.log(reflectionText)
}
// Find the corresponding tool
const tool = tools.get(toolName)
assert(tool, `Tool ${toolName} not found`)
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
// Emit executing activity
this.#emitActivity({ type: 'executing', tool: toolName, input: toolInput })
const startTime = Date.now()
// Execute tool, bind `this` to PageAgent
const result = await tool.execute.bind(this)(toolInput)
const duration = Date.now() - startTime
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result)
// Emit executed activity
this.#emitActivity({
type: 'executed',
tool: toolName,
input: toolInput,
output: result,
duration,
})
// counting wait time
if (toolName === 'wait') {
this.#states.totalWaitTime += toolInput?.seconds || 0
} else {
this.#states.totalWaitTime = 0
}
// Return structured result
return {
input,
output: result,
}
},
}
}
/**
* Get system prompt, dynamically replace language settings based on configured language
*/
#getSystemPrompt(): string {
if (this.config.customSystemPrompt) {
return this.config.customSystemPrompt
}
const targetLanguage = this.config.language === 'zh-CN' ? '中文' : 'English'
const systemPrompt = SYSTEM_PROMPT.replace(
/Default working language: \*\*.*?\*\*/,
`Default working language: **${targetLanguage}**`
)
return systemPrompt
}
/**
* Get instructions from config
*/
async #getInstructions(): Promise<string> {
const { instructions, experimentalLlmsTxt } = this.config
const systemInstructions = instructions?.system?.trim()
let pageInstructions: string | undefined
const url = this.#states.browserState?.url || ''
if (instructions?.getPageInstructions && url) {
try {
pageInstructions = instructions.getPageInstructions(url)?.trim()
} catch (error) {
console.error(
chalk.red('[PageAgent] Failed to execute getPageInstructions callback:'),
error
)
}
}
const llmsTxt = experimentalLlmsTxt && url ? await fetchLlmsTxt(url) : undefined
if (!systemInstructions && !pageInstructions && !llmsTxt) return ''
let result = '<instructions>\n'
if (systemInstructions) {
result += `<system_instructions>\n${systemInstructions}\n</system_instructions>\n`
}
if (pageInstructions) {
result += `<page_instructions>\n${pageInstructions}\n</page_instructions>\n`
}
if (llmsTxt) {
result += `<llms_txt>\n${llmsTxt}\n</llms_txt>\n`
}
result += '</instructions>\n\n'
return result
}
/**
* Generate system observations before each step
* @todo loop detection
* @todo console error
*/
async #handleObservations(step: number): Promise<void> {
// Accumulated wait time warning
if (this.#states.totalWaitTime >= 3) {
this.pushObservation(
`You have waited ${this.#states.totalWaitTime} seconds accumulatively. ` +
`DO NOT wait any longer unless you have a good reason.`
)
}
// Detect URL change
const currentURL = this.#states.browserState?.url || ''
if (currentURL !== this.#states.lastURL) {
this.pushObservation(`Page navigated to → ${currentURL}`)
this.#states.lastURL = currentURL
await waitFor(0.5) // wait for page to stabilize
}
// Remaining steps warning
const remaining = this.config.maxSteps - step
if (remaining === 5) {
this.pushObservation(
`⚠️ Only ${remaining} steps remaining. ` +
`Consider wrapping up or calling done with partial results.`
)
} else if (remaining === 2) {
this.pushObservation(
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
)
}
// Push observations to history and emit
if (this.#observations.length > 0) {
for (const content of this.#observations) {
this.history.push({ type: 'observation', content })
console.log(chalk.cyan('Observation:'), content)
}
this.#observations = []
this.#emitHistoryChange()
}
}
async #assembleUserPrompt(): Promise<string> {
const browserState = this.#states.browserState!
let prompt = ''
// <instructions> (optional)
prompt += await this.#getInstructions()
// <agent_state>
// - <user_request>
// - <step_info>
// <agent_state>
const stepCount = this.history.filter((e) => e.type === 'step').length
prompt += '<agent_state>\n'
prompt += '<user_request>\n'
prompt += `${this.task}\n`
prompt += '</user_request>\n'
prompt += '<step_info>\n'
prompt += `Step ${stepCount + 1} of ${this.config.maxSteps} max possible steps\n`
prompt += `Current time: ${new Date().toLocaleString()}\n`
prompt += '</step_info>\n'
prompt += '</agent_state>\n\n'
// <agent_history>
// - <step_N> for steps
// - <sys> for observations and system messages
prompt += '<agent_history>\n'
let stepIndex = 0
for (const event of this.history) {
if (event.type === 'step') {
stepIndex++
prompt += `<step_${stepIndex}>\n`
prompt += `Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}\n`
prompt += `Memory: ${event.reflection.memory}\n`
prompt += `Next Goal: ${event.reflection.next_goal}\n`
prompt += `Action Results: ${event.action.output}\n`
prompt += `</step_${stepIndex}>\n`
} else if (event.type === 'observation') {
prompt += `<sys>${event.content}</sys>\n`
} else if (event.type === 'user_takeover') {
prompt += `<sys>User took over control and made changes to the page</sys>\n`
} else if (event.type === 'error') {
// Error events are mainly for panel rendering, not included in LLM context
// to avoid polluting the agent's reasoning with transient errors
}
}
prompt += '</agent_history>\n\n'
// <browser_state>
let pageContent = browserState.content
if (this.config.transformPageContent) {
pageContent = await this.config.transformPageContent(pageContent)
}
prompt += '<browser_state>\n'
prompt += browserState.header + '\n'
prompt += pageContent + '\n'
prompt += browserState.footer + '\n\n'
prompt += '</browser_state>\n\n'
return prompt
}
#onDone(success = true) {
this.pageController.cleanUpHighlights()
this.pageController.hideMask() // No await - fire and forget
this.#setStatus(success ? 'completed' : 'error')
this.#abortController.abort()
}
dispose() {
console.log('Disposing PageAgent...')
this.disposed = true
this.pageController.dispose()
// this.history = []
this.#abortController.abort()
// Emit dispose event for UI cleanup
this.dispatchEvent(new Event('dispose'))
this.config.onDispose?.(this)
}
}
================================================
FILE: packages/core/src/env.d.ts
================================================
/// <reference types="vite/client" />
declare module '*.md?raw' {
const content: string
export default content
}
================================================
FILE: packages/core/src/prompts/.prettierignore
================================================
system_prompt.md
================================================
FILE: packages/core/src/prompts/system_prompt.md
================================================
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>.
<intro>
You excel at following tasks:
1. Navigating complex websites and extracting precise information
2. Automating form submissions and interactive web actions
3. Gathering and saving information
4. Operate effectively in an agent loop
5. Efficiently performing diverse web tasks
</intro>
<language_settings>
- Default working language: **English**
- Use the language that user is using. Return in user's language.
</language_settings>
<input>
At every step, your input will consist of:
1. <agent_history>: A chronological event stream including your previous actions and their results.
2. <agent_state>: Current <user_request> and <step_info>.
3. <browser_state>: Current URL, interactive elements indexed for actions, and visible page content.
</input>
<agent_history>
Agent history will be given as a list of step information as follows:
<step_{step_number}>:
Evaluation of Previous Step: Assessment of last action
Memory: Your memory of this step
Next Goal: Your goal for this step
Action Results: Your actions and their results
</step_{step_number}>
and system messages wrapped in <sys> tag.
</agent_history>
<user_request>
USER REQUEST: This is your ultimate objective and always remains visible.
- This has the highest priority. Make the user happy.
- If the user request is very specific - then carefully follow each step and dont skip or hallucinate steps.
- If the task is open ended you can plan yourself how to get it done.
</user_request>
<browser_state>
1. Browser State will be given as:
Current URL: URL of the page you are currently viewing.
Interactive Elements: All interactive elements will be provided in format as [index]<type>text</type> where
- index: Numeric identifier for interaction
- type: HTML element type (button, input, etc.)
- text: Element description
Examples:
[33]<div>User form</div>
\t*[35]<button aria-label='Submit form'>Submit</button>
Note that:
- Only elements with numeric indexes in [] are interactive
- (stacked) indentation (with \t) is important and means that the element is a (html) child of the element above (with a lower index)
- Elements tagged with `*[` are the new clickable elements that appeared on the website since the last step - if url has not changed.
- Pure text elements without [] are not interactive.
</browser_state>
<browser_rules>
Strictly follow these rules while using the browser and navigating the web:
- Only interact with elements that have a numeric [index] assigned.
- Only use indexes that are explicitly provided.
- 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.
- 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.
- 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).
- 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.
- If a captcha appears, tell user you can not solve captcha. Finish the task and ask user to solve it.
- If expected elements are missing, try scrolling, or navigating back.
- If the page is not fully loaded, use the `wait` action.
- Do not repeat one action for more than 3 times unless some conditions changed.
- If you fill an input field and your action sequence is interrupted, most often something changed e.g. suggestions popped up under the field.
- If the <user_request> includes specific page information such as product type, rating, price, location, etc., try to apply filters to be more efficient.
- The <user_request> is the ultimate goal. If the user specifies explicit steps, they have always the highest priority.
- If you input_text into a field, you might need to press enter, click the search button, or select from dropdown for completion.
- Don't login into a page if you don't have to. Don't login if you don't have the credentials.
- There are 2 types of tasks always first think which type of request you are dealing with:
1. Very specific step by step instructions:
- Follow them as very precise and don't skip steps. Try to complete everything as requested.
2. Open ended tasks. Plan yourself, be creative in achieving them.
- 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.
</browser_rules>
<capability>
- You can only handle single page app. Do not jump out of current page.
- Do not click on link if it will open in a new page (e.g., <a target="_blank">)
- It is ok to fail the task.
- 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.
- 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.
- 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.
- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.
</capability>
<task_completion_rules>
You must call the `done` action in one of three cases:
- When you have fully completed the USER REQUEST.
- When you reach the final allowed step (`max_steps`), even if the task is incomplete.
- When you feel stuck or unable to solve user request. Or user request is not clear or contains inappropriate content.
- If it is ABSOLUTELY IMPOSSIBLE to continue.
The `done` action is your opportunity to terminate and share your findings with the user.
- Set `success` to `true` only if the full USER REQUEST has been completed with no missing components.
- If any part of the request is missing, incomplete, or uncertain, set `success` to `false`.
- 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.
- You are ONLY ALLOWED to call `done` as a single action. Don't call it together with other actions.
- 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.
- 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!
</task_completion_rules>
<reasoning_rules>
Exhibit the following reasoning patterns to successfully achieve the <user_request>:
- Reason about <agent_history> to track progress and context toward <user_request>.
- Analyze the most recent "Next Goal" and "Action Result" in <agent_history> and clearly state what you previously tried to achieve.
- Analyze all relevant items in <agent_history> and <browser_state> to understand your state.
- 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.
- 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.
- Ask user for help if you have any difficulty. Keep user in the loop.
- If you see information relevant to <user_request>, plan saving the information to memory.
- 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.
</reasoning_rules>
<examples>
Here are examples of good output patterns. Use them as reference but never copy them directly.
<evaluation_examples>
"evaluation_previous_goal": "Successfully navigated to the product page and found the target information. Verdict: Success"
"evaluation_previous_goal": "Clicked the login button and user authentication form appeared. Verdict: Success"
</evaluation_examples>
<memory_examples>
"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."
</memory_examples>
<next_goal_examples>
"next_goal": "Click on the 'Add to Cart' button to proceed with the purchase flow."
</next_goal_examples>
</examples>
<output>
{
"evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.",
"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.",
"next_goal": "State the next immediate goal and action to achieve it, in one clear sentence.",
"action":{
"Action name": {// Action parameters}
}
}
</output>
================================================
FILE: packages/core/src/tools/index.ts
================================================
/**
* Internal tools for PageAgent.
* @note Adapted from browser-use
*/
import * as z from 'zod/v4'
import type { PageAgentCore } from '../PageAgentCore'
import { waitFor } from '../utils'
/**
* Internal tool definition that has access to PageAgent `this` context
*/
export interface PageAgentTool<TParams = any> {
// name: string
description: string
inputSchema: z.ZodType<TParams>
execute: (this: PageAgentCore, args: TParams) => Promise<string>
}
export function tool<TParams>(options: PageAgentTool<TParams>): PageAgentTool<TParams> {
return options
}
/**
* Internal tools for PageAgent.
* Note: Using any to allow different parameter types for each tool
*/
export const tools = new Map<string, PageAgentTool>()
tools.set(
'done',
tool({
description:
'Complete task. Text is your final response to the user — keep it concise unless the user explicitly asks for detail.',
inputSchema: z.object({
text: z.string(),
success: z.boolean().default(true),
}),
execute: async function (this: PageAgentCore, input) {
// @note main loop will handle this one
return Promise.resolve('Task completed')
},
})
)
tools.set(
'wait',
tool({
description: 'Wait for x seconds. Can be used to wait until the page or data is fully loaded.',
inputSchema: z.object({
seconds: z.number().min(1).max(10).default(1),
}),
execute: async function (this: PageAgentCore, input) {
// try to subtract LLM calling time from the actual wait time
const lastTimeUpdate = await this.pageController.getLastUpdateTime()
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
console.log(`actualWaitTime: ${actualWaitTime} seconds`)
await waitFor(actualWaitTime)
return `✅ Waited for ${input.seconds} seconds.`
},
})
)
tools.set(
'ask_user',
tool({
description:
'Ask the user a question and wait for their answer. Use this if you need more information or clarification.',
inputSchema: z.object({
question: z.string(),
}),
execute: async function (this: PageAgentCore, input) {
if (!this.onAskUser) {
throw new Error('ask_user tool requires onAskUser callback to be set')
}
const answer = await this.onAskUser(input.question)
return `User answered: ${answer}`
},
})
)
tools.set(
'click_element_by_index',
tool({
description: 'Click element by index',
inputSchema: z.object({
index: z.int().min(0),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.clickElement(input.index)
return result.message
},
})
)
tools.set(
'input_text',
tool({
description: 'Click and type text into an interactive input element',
inputSchema: z.object({
index: z.int().min(0),
text: z.string(),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.inputText(input.index, input.text)
return result.message
},
})
)
tools.set(
'select_dropdown_option',
tool({
description:
'Select dropdown option for interactive element index by the text of the option you want to select',
inputSchema: z.object({
index: z.int().min(0),
text: z.string(),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.selectOption(input.index, input.text)
return result.message
},
})
)
/**
* @note Reference from browser-use
*/
tools.set(
'scroll',
tool({
description: 'Scroll the page vertically. Use index for scroll elements (dropdowns/custom UI).',
inputSchema: z.object({
down: z.boolean().default(true),
num_pages: z.number().min(0).max(10).optional().default(0.1),
pixels: z.number().int().min(0).optional(),
index: z.number().int().min(0).optional(),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.scroll({
...input,
numPages: input.num_pages,
})
return result.message
},
})
)
/**
* @todo Tables need a dedicated parser to extract structured data. This tool is useless.
*/
tools.set(
'scroll_horizontally',
tool({
description:
'Scroll the page horizontally, or within a specific element by index. Useful for wide tables.',
inputSchema: z.object({
right: z.boolean().default(true),
pixels: z.number().int().min(0),
index: z.number().int().min(0).optional(),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.scrollHorizontally(input)
return result.message
},
})
)
tools.set(
'execute_javascript',
tool({
description:
'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution!',
inputSchema: z.object({
script: z.string(),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.executeJavascript(input.script)
return result.message
},
})
)
// @todo send_keys
// @todo upload_file
// @todo go_back
// @todo extract_structured_data
================================================
FILE: packages/core/src/types.ts
================================================
import type { LLMConfig } from '@page-agent/llms'
// @note circular dependency but okay
import type { PageAgentCore } from './PageAgentCore'
import type { PageAgentTool } from './tools'
/** Supported UI languages */
export type SupportedLanguage = 'en-US' | 'zh-CN'
export interface AgentConfig extends LLMConfig {
language?: SupportedLanguage
/**
* Maximum number of steps the agent can take per task.
* @default 40
*/
maxSteps?: number
/**
* Custom tools to extend PageAgent capabilities
* @experimental
* @note You can also override or remove internal tools by using the same name.
* @see PageAgentTool
*
* @example
* // override internal tool
* import { z } from 'zod/v4'
* import { tool } from 'page-agent'
* const customTools = {
* ask_user: tool({
* description:
* 'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
* inputSchema: z.object({
* question: z.string(),
* }),
* execute: async function (this: PageAgent, input) {
* const answer = await do_some_thing(input.question)
* return "✅ Received user answer: " + answer
* },
* })
* }
*
* @example
* // remove internal tool
* const customTools = {
* ask_user: null // never ask user questions
* }
*/
customTools?: Record<string, PageAgentTool | null>
/**
* Instructions to guide the agent's behavior
*/
instructions?: {
/**
* Global system-level instructions, applied to all tasks
*/
system?: string
/**
* Dynamic page-level instructions callback
* Called before each step to get instructions for the current page
* @param url - Current page URL (window.location.href)
* @returns Instructions string, or undefined/null to skip
*/
getPageInstructions?: (url: string) => string | undefined | null
}
/**
* Lifecycle hooks for task execution.
* @experimental API may change in future versions.
*
* All hooks receive the agent instance as first parameter.
*/
/**
* Called before each step execution.
* @experimental
* @param agent - The PageAgentCore instance
* @param stepCount - Current step number (0-indexed)
*/
onBeforeStep?: (agent: PageAgentCore, stepCount: number) => Promise<void> | void
/**
* Called after each step execution.
* @experimental
* @param agent - The PageAgentCore instance
* @param history - Current history of events
*/
onAfterStep?: (agent: PageAgentCore, history: HistoricalEvent[]) => Promise<void> | void
/**
* Called before task execution starts.
* @experimental
* @param agent - The PageAgentCore instance
*/
onBeforeTask?: (agent: PageAgentCore) => Promise<void> | void
/**
* Called after task execution completes (success or failure).
* @experimental
* @param agent - The PageAgentCore instance
* @param result - The execution result
*/
onAfterTask?: (agent: PageAgentCore, result: ExecutionResult) => Promise<void> | void
/**
* Called when the agent is disposed.
* @experimental
* @note This hook can block the disposal process if it's async.
* @param agent - The PageAgentCore instance
* @param reason - Optional reason for disposal
*/
onDispose?: (agent: PageAgentCore, reason?: string) => void
// page behavior hooks
/**
* @experimental
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
* @note Can cause unpredictable side effects.
* @note May bypass some safe guards and data-masking mechanisms.
*/
experimentalScriptExecutionTool?: boolean
/**
* @experimental
* Fetch /llms.txt from current site origin and include as context.
* Only fetched once per origin per task.
* @default false
*/
experimentalLlmsTxt?: boolean
/**
* Transform page content before sending to LLM.
* Called after DOM extraction and simplification, before LLM invocation.
* Use cases: inspect extraction results, modify page info, mask sensitive data.
*
* @param content - Simplified page content that will be sent to LLM
* @returns Transformed content
*
* @example
* // Mask phone numbers
* transformPageContent: async (content) => {
* return content.replace(/1[3-9]\d{9}/g, '***********')
* }
*/
transformPageContent?: (content: string) => Promise<string> | string
/**
* Completely override the default system prompt.
* @experimental Use with caution - incorrect prompts may break agent behavior.
*/
customSystemPrompt?: string
/**
* Delay between steps in seconds.
* @default 0.4
*/
stepDelay?: number
}
/**
* Agent reflection state - the reflection-before-action model
*
* Every tool call must first reflect on:
* - evaluation_previous_goal: How well did the previous action achieve its goal?
* - memory: Key information to remember for future steps
* - next_goal: What should be accomplished in the next action?
*/
export interface AgentReflection {
evaluation_previous_goal: string
memory: string
next_goal: string
}
/**
* MacroTool input structure
*
* This is the core abstraction that enforces the "reflection-before-action" mental model.
* Before executing any action, the LLM must output its reasoning state.
*/
export interface MacroToolInput extends Partial<AgentReflection> {
action: Record<string, any>
}
/**
* MacroTool output structure
*/
export interface MacroToolResult {
input: MacroToolInput
output: string
}
/**
* A single agent step with reflection and action
*/
export interface AgentStepEvent {
type: 'step'
stepIndex: number
reflection: Partial<AgentReflection>
action: {
name: string
input: any
output: string
}
usage: {
promptTokens: number
completionTokens: number
totalTokens: number
cachedTokens?: number
reasoningTokens?: number
}
/** Raw LLM response for debugging */
rawResponse?: unknown
/** Raw LLM request for debugging */
rawRequest?: unknown
}
/**
* Persistent observation event (stays in memory)
*/
export interface ObservationEvent {
type: 'observation'
content: string
}
/**
* User takeover event
*/
export interface UserTakeoverEvent {
type: 'user_takeover'
}
/**
* Retry event - LLM call is being retried
*/
export interface RetryEvent {
type: 'retry'
message: string
attempt: number
maxAttempts: number
}
/**
* Error event - fatal error from LLM or execution
*/
export interface AgentErrorEvent {
type: 'error'
message: string
rawResponse?: unknown
}
/**
* Union type for all history events
*/
export type HistoricalEvent =
| AgentStepEvent
| ObservationEvent
| UserTakeoverEvent
| RetryEvent
| AgentErrorEvent
/**
* Agent execution status
*/
export type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
/**
* Agent activity - transient state for immediate UI feedback.
*
* Unlike historical events (which are persisted), activities are ephemeral
* and represent "what the agent is doing right now". UI components should
* listen to 'activity' events to show real-time feedback.
*
* Note: There is no 'idle' activity - absence of activity events means idle.
*/
export type AgentActivity =
| { type: 'thinking' }
| { type: 'executing'; tool: string; input: unknown }
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
| { type: 'retrying'; attempt: number; maxAttempts: number }
| { type: 'error'; message: string }
export interface ExecutionResult {
success: boolean
data: string
history: HistoricalEvent[]
}
================================================
FILE: packages/core/src/utils/autoFixer.ts
================================================
import { InvokeError, InvokeErrorType } from '@page-agent/llms'
import chalk from 'chalk'
import * as z from 'zod/v4'
import type { PageAgentTool } from '../tools'
const log = console.log.bind(console, chalk.yellow('[autoFixer]'))
/**
* Normalize LLM response and fix common format issues.
*
* Handles:
* - No tool_calls but JSON in message.content (fallback)
* - Model returns action name as tool call instead of AgentOutput
* - Arguments wrapped as double JSON string
* - Nested function call format
* - Missing action field (fallback to wait)
* - Primitive action input for single-field tools (e.g. `{"click_element_by_index": 2}`)
* - etc.
*/
export function normalizeResponse(response: any, tools?: Map<string, PageAgentTool>): any {
let resolvedArguments = null as any
const choice = (response as { choices?: Choice[] }).choices?.[0]
if (!choice) throw new Error('No choices in response')
const message = choice.message
if (!message) throw new Error('No message in choice')
const toolCall = message.tool_calls?.[0]
// fix level and location of arguments
if (toolCall?.function?.arguments) {
resolvedArguments = safeJsonParse(toolCall.function.arguments)
// case: sometimes the model only returns the action level
if (toolCall.function.name && toolCall.function.name !== 'AgentOutput') {
log(`#1: fixing tool_call`)
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
}
} else {
// case: sometimes the model returns json in content instead of tool_calls
if (message.content) {
const content = message.content.trim()
const jsonInContent = retrieveJsonFromString(content)
if (jsonInContent) {
resolvedArguments = safeJsonParse(jsonInContent)
// case: sometimes the content json includes upper level wrapper
if (resolvedArguments?.name === 'AgentOutput') {
log(`#2: fixing tool_call`)
resolvedArguments = safeJsonParse(resolvedArguments.arguments)
}
// case: sometimes even 2-levels of wrapping
if (resolvedArguments?.type === 'function') {
log(`#3: fixing tool_call`)
resolvedArguments = safeJsonParse(resolvedArguments.function.arguments)
}
// case: and sometimes action level only
// todo: needs better detection logic
if (
!resolvedArguments?.action &&
!resolvedArguments?.evaluation_previous_goal &&
!resolvedArguments?.memory &&
!resolvedArguments?.next_goal &&
!resolvedArguments?.thinking
) {
log(`#4: fixing tool_call`)
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
}
} else {
throw new Error('No tool_call and the message content does not contain valid JSON')
}
} else {
throw new Error('No tool_call nor message content is present')
}
}
// fix double stringified arguments
resolvedArguments = safeJsonParse(resolvedArguments)
if (resolvedArguments.action) {
resolvedArguments.action = safeJsonParse(resolvedArguments.action)
}
// validate and fix action input using tool schemas
if (resolvedArguments.action && tools) {
resolvedArguments.action = validateAction(resolvedArguments.action, tools)
}
// fix incomplete formats
if (!resolvedArguments.action) {
log(`#5: fixing tool_call`)
resolvedArguments.action = { name: 'wait', input: { seconds: 1 } }
}
// pack back to standard format
return {
...response,
choices: [
{
...choice,
message: {
...message,
tool_calls: [
{
...(toolCall || {}),
function: {
...(toolCall?.function || {}),
name: 'AgentOutput',
arguments: JSON.stringify(resolvedArguments),
},
},
],
},
},
],
}
}
/**
* Validate action against tool schemas. Provides clear error messages
* instead of letting the union schema produce unreadable errors.
*
* Also coerces primitive inputs for single-field tools:
* e.g. `{"click_element_by_index": 2}` → `{"click_element_by_index": {"index": 2}}`
*/
function validateAction(action: any, tools: Map<string, PageAgentTool>): any {
if (typeof action !== 'object' || action === null) return action
const toolName = Object.keys(action)[0]
if (!toolName) return action
const tool = tools.get(toolName)
if (!tool) {
const available = Array.from(tools.keys()).join(', ')
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Unknown action "${toolName}". Available: ${available}`
)
}
let value = action[toolName]
const schema = tool.inputSchema
// coerce primitive input for single-field tools
if (schema instanceof z.ZodObject && value !== null && typeof value !== 'object') {
const requiredKey = Object.keys(schema.shape).find(
(k) => !(schema.shape as Record<string, z.ZodType>)[k].safeParse(undefined).success
)
if (requiredKey) {
log(`coercing primitive action input for "${toolName}"`)
value = { [requiredKey]: value }
}
}
const result = schema.safeParse(value)
if (!result.success) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Invalid input for action "${toolName}": ${z.prettifyError(result.error)}`
)
}
return { [toolName]: result.data }
}
/**
* Safely parse JSON, return original input if not json.
*/
function safeJsonParse(input: any): any {
if (typeof input === 'string') {
try {
return JSON.parse(input.trim())
} catch {
return input
}
}
return input
}
/**
* Extract and parse JSON from a string.
* - Treat content between the first `{` and the last `}` as JSON.
* - Try to parse that content as JSON and return the parsed value (object/array/primitive) if successful, otherwise return null.
*/
function retrieveJsonFromString(str: string): any {
try {
const json = /({[\s\S]*})/.exec(str) ?? []
if (json.length === 0) {
return null
}
return JSON.parse(json[0]!)
} catch {
return null
}
}
interface Choice {
message?: {
role?: 'assistant'
content?: string
tool_calls?: {
id?: string
type?: 'function'
function?: {
name?: string
arguments?: string
}
}[]
}
index?: 0
finish_reason?: 'tool_calls'
}
================================================
FILE: packages/core/src/utils/index.ts
================================================
import chalk from 'chalk'
export * from './autoFixer'
export async function waitFor(seconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
}
//
export function truncate(text: string, maxLength: number): string {
if (text.length > maxLength) {
return text.substring(0, maxLength) + '...'
}
return text
}
//
export function randomID(existingIDs?: string[]): string {
let id = Math.random().toString(36).substring(2, 11)
if (!existingIDs) {
return id
}
const MAX_TRY = 1000
let tryCount = 0
while (existingIDs.includes(id)) {
id = Math.random().toString(36).substring(2, 11)
tryCount++
if (tryCount > MAX_TRY) {
throw new Error('randomID: too many tries')
}
}
return id
}
//
const _global = globalThis as any
if (!_global.__PAGE_AGENT_IDS__) {
_global.__PAGE_AGENT_IDS__ = []
}
const ids = _global.__PAGE_AGENT_IDS__
/**
* Generate a random ID.
* @note Unique within this window.
*/
export function uid() {
const id = randomID(ids)
ids.push(id)
return id
}
const llmsTxtCache = new Map<string, string | null>()
/** Fetch /llms.txt for a URL's origin. Cached per origin, `null` = tried and not found. */
export async function fetchLlmsTxt(url: string): Promise<string | null> {
let origin: string
try {
origin = new URL(url).origin
} catch {
return null // Invalid URL
}
// about:blank, data:, file:
if (origin === 'null') return null
if (llmsTxtCache.has(origin)) return llmsTxtCache.get(origin)!
const endpoint = `${origin}/llms.txt`
let result: string | null = null
try {
console.log(chalk.gray(`[llms.txt] Fetching ${endpoint}`))
const res = await fetch(endpoint, { signal: AbortSignal.timeout(3000) })
if (res.ok) {
result = await res.text()
console.log(chalk.green(`[llms.txt] Found (${result.length} chars)`))
if (result.length > 1000) {
console.log(chalk.yellow(`[llms.txt] Truncating to 1000 chars`))
result = truncate(result, 1000)
}
} else {
console.debug(chalk.gray(`[llms.txt] ${res.status} for ${endpoint}`))
}
} catch (e) {
console.debug(chalk.gray(`[llms.txt] not found for ${endpoint}`), e)
}
llmsTxtCache.set(origin, result)
return result
}
/**
* Simple assertion function that throws an error if the condition is falsy
* @param condition - The condition to assert
* @param message - Optional error message
* @throws Error if condition is falsy
*/
export function assert(condition: unknown, message?: string, silent?: boolean): asserts condition {
if (!condition) {
const errorMessage = message ?? 'Assertion failed'
if (!silent) console.error(chalk.red(`❌ assert: ${errorMessage}`))
throw new Error(errorMessage)
}
}
================================================
FILE: packages/core/tsconfig.dts.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
================================================
FILE: packages/core/tsconfig.json
================================================
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist",
"paths": {
//
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"]
}
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"],
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" }
]
}
================================================
FILE: packages/core/vite.config.js
================================================
// @ts-check
import { dirname, resolve } from 'path'
import dts from 'unplugin-dts/vite'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
// ES Module for NPM Package
export default defineConfig({
clearScreen: false,
plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
],
publicDir: false,
esbuild: {
keepNames: true,
},
build: {
lib: {
entry: resolve(__dirname, 'src/PageAgentCore.ts'),
name: 'PageAgentCore',
fileName: 'page-agent-core',
formats: ['es'],
},
outDir: resolve(__dirname, 'dist', 'esm'),
rollupOptions: {
external: [
'chalk',
'zod',
'zod/v4',
// all the internal packages
/^@page-agent\//,
],
},
minify: false,
sourcemap: true,
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
})
================================================
FILE: packages/extension/.prettierignore
================================================
.wxt
src/components/ui
================================================
FILE: packages/extension/PRIVACY.md
================================================
# Privacy Policy for Page Agent Extension
This document has moved. Please see our full **[Terms of Use & Privacy](../../docs/terms-and-privacy.md)**.
Online: https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md
================================================
FILE: packages/extension/components.json
================================================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/assets/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/lib/hooks"
},
"registries": {
"@magicui": "https://magicui.design/r/{name}.json"
}
}
================================================
FILE: packages/extension/docs/extension_api.md
================================================
# Page Agent Extension API
Integrate the Page Agent extension into your web app and trigger multi-page browser tasks from page JavaScript.
## Installation
### 1. Install the browser extension
Primary channel:
- Chrome Web Store: https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj
Latest updates are often published earlier on:
- GitHub Releases: https://github.com/alibaba/page-agent/releases
### 2. Install type definitions (recommended)
```bash
npm install @page-agent/core --save-dev
```
### 3. Authorization (Token)
The token allows your page JS to call the extension API (`window.PAGE_AGENT_EXT`) and execute multi-page tasks.
Why token-based access is required:
- The extension has broad browser permissions (page access, navigation, multi-tab control).
- If abused, it can harm user privacy and security.
- Users must explicitly provide the token only to applications they trust.
Setup:
1. Open the extension side panel and copy your auth token.
2. Set the token in your page:
```typescript
localStorage.setItem('PageAgentExtUserAuthToken', 'your-token')
```
## Quick Start
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
// Wait for extension injection (up to 1 second)
async function waitForExtension(timeout = 1000): Promise<boolean> {
const start = Date.now()
while (Date.now() - start < timeout) {
if (window.PAGE_AGENT_EXT) return true
await new Promise((r) => setTimeout(r, 100))
}
return false
}
// Usage
if (await waitForExtension()) {
const result = await window.PAGE_AGENT_EXT!.execute('Click the login button', {
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'gpt-5.2',
onStatusChange: (status) => console.log('Status:', status),
onActivity: (activity) => console.log('Activity:', activity),
})
console.log('Result:', result)
}
```
## Global API
After token match, the extension injects APIs into `window`.
### `window.PAGE_AGENT_EXT_VERSION`
Extension version string (for capability checks before using the main API).
### `window.PAGE_AGENT_EXT`
Main namespace object.
#### `PAGE_AGENT_EXT.execute(task, config)`
Execute one agent task.
Parameters:
| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `task` | `string` | Yes | Task description |
| `config` | `ExecuteConfig` | Yes | LLM settings, options, and callbacks |
Returns: `Promise<ExecutionResult>`
#### `PAGE_AGENT_EXT.stop()`
Stop the current task.
## Types
Install `@page-agent/core` for complete types:
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
export interface ExecuteConfig {
baseURL: string
model: string
apiKey?: string
// Include the initial tab where page JS starts. Default: true.
includeInitialTab?: boolean
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
}
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
```
`AgentStatus`
```typescript
type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
```
`AgentActivity`
```typescript
type AgentActivity =
| { type: 'thinking' }
| { type: 'executing'; tool: string; input: unknown }
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
| { type: 'retrying'; attempt: number; maxAttempts: number }
| { type: 'error'; message: string }
```
`HistoricalEvent`
```typescript
type HistoricalEvent =
| { type: 'step'; stepIndex: number; reflection: AgentReflection; action: Action }
| { type: 'observation'; content: string }
| { type: 'user_takeover' }
| { type: 'retry'; message: string; attempt: number; maxAttempts: number }
| { type: 'error'; message: string; rawResponse?: unknown }
```
`ExecutionResult`
```typescript
interface ExecutionResult {
success: boolean
data: string
history: HistoricalEvent[]
}
```
## Usage Examples
### Basic Execution
```typescript
const result = await window.PAGE_AGENT_EXT!.execute(
'Fill in the email field with test@example.com and click Submit',
{
baseURL: 'https://api.openai.com/v1',
apiKey: process.env.OPENAI_API_KEY!,
model: 'gpt-5.2',
includeInitialTab: false, // Optional: exclude current tab
onStatusChange: (status) => console.log(status),
onActivity: (activity) => console.log(activity),
}
)
```
### Stop the Current Task
```typescript
window.PAGE_AGENT_EXT!.stop()
```
## Window Type Declaration
If you are not importing `@page-agent/core`, add:
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
interface ExecuteConfig {
baseURL: string
model: string
apiKey?: string
includeInitialTab?: boolean
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
}
declare global {
interface Window {
PAGE_AGENT_EXT_VERSION?: string
PAGE_AGENT_EXT?: {
version: string
execute: Execute
stop: () => void
}
}
}
```
================================================
FILE: packages/extension/package.json
================================================
{
"name": "@page-agent/ext",
"private": true,
"version": "1.6.0",
"type": "module",
"scripts": {
"dev": "wxt",
"build:ext": "wxt build",
"zip": "wxt zip",
"postinstall": "wxt prepare"
},
"devDependencies": {
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@types/chrome": "^0.1.37",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.1",
"@wxt-dev/module-react": "^1.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"idb": "^8.0.3",
"lucide-react": "^0.577.0",
"motion": "^12.37.0",
"next-themes": "^0.4.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"rough-notation": "^0.5.1",
"simple-icons": "^16.12.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"wxt": "^0.20.19"
},
"dependencies": {
"@page-agent/core": "1.6.0",
"@page-agent/llms": "1.6.0",
"@page-agent/page-controller": "1.6.0",
"@page-agent/ui": "1.6.0",
"ai-motion": "^0.4.8",
"chalk": "^5.6.2"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
}
================================================
FILE: packages/extension/public/_locales/en/messages.json
================================================
{
"extName": {
"message": "Page Agent Ext"
},
"extDescription": {
"message": "AI-powered browser automation assistant. Control web pages with natural language."
},
"extActionTitle": {
"message": "Open Page Agent"
}
}
================================================
FILE: packages/extension/public/_locales/zh_CN/messages.json
================================================
{
"extName": {
"message": "Page Agent Ext"
},
"extDescription": {
"message": "AI 驱动的浏览器自动化助手,用自然语言控制网页。"
},
"extActionTitle": {
"message": "打开 Page Agent"
}
}
================================================
FILE: packages/extension/src/agent/.prettierignore
================================================
system_prompt.md
================================================
FILE: packages/extension/src/agent/MultiPageAgent.ts
================================================
import { type AgentConfig, PageAgentCore } from '@page-agent/core'
import { RemotePageController } from './RemotePageController'
import { TabsController } from './TabsController'
import SYSTEM_PROMPT from './system_prompt.md?raw'
import { createTabTools } from './tabTools'
/** Detect user language from browser settings */
function detectLanguage(): 'en-US' | 'zh-CN' {
const lang = navigator.language || navigator.languages?.[0] || 'en-US'
return lang.startsWith('zh') ? 'zh-CN' : 'en-US'
}
/**
* MultiPageAgent
* - use with extension
* - can be used from a side panel or a content script
*/
export class MultiPageAgent extends PageAgentCore {
constructor(config: AgentConfig & { includeInitialTab?: boolean }) {
// multi page controller
const tabsController = new TabsController()
const pageController = new RemotePageController(tabsController)
const customTools = createTabTools(tabsController)
// system prompt - auto-detect language if not specified
const language = config.language ?? detectLanguage()
const targetLanguage = language === 'zh-CN' ? '中文' : 'English'
const systemPrompt = SYSTEM_PROMPT.replace(
/Default working language: \*\*.*?\*\*/,
`Default working language: **${targetLanguage}**`
)
// include initial tab for controlling
const includeInitialTab = config.includeInitialTab ?? true
/**
* When the agent is in side-panel and user closed the side-panel.
* There is no chance for isAgentRunning to be set false.
* (unload event doesn't work well in side panel.)
* (I'm trying not to use long-lived connection because the lifecycle of a sw is hard to predict.)
* This heartbeat mechanism acts as a backup.
*/
let heartBeatInterval: null | number = null
super({
...config,
pageController: pageController as any,
customTools: customTools,
customSystemPrompt: systemPrompt,
onBeforeTask: async (agent) => {
await tabsController.init(agent.task, includeInitialTab)
heartBeatInterval = window.setInterval(() => {
chrome.storage.local.set({
agentHeartbeat: Date.now(),
})
}, 1_000)
await chrome.storage.local.set({
isAgentRunning: true,
})
},
onAfterTask: async () => {
if (heartBeatInterval) {
window.clearInterval(heartBeatInterval)
heartBeatInterval = null
}
await chrome.storage.local.set({
isAgentRunning: false,
})
},
onBeforeStep: async (agent) => {
if (!tabsController.currentTabId) return
// make sure the current tab is loaded before the step starts
await tabsController.waitUntilTabLoaded(tabsController.currentTabId!)
},
onDispose: () => {
if (heartBeatInterval) {
window.clearInterval(heartBeatInterval)
heartBeatInterval = null
}
chrome.storage.local.set({
isAgentRunning: false,
})
tabsController.dispose()
},
})
}
}
================================================
FILE: packages/extension/src/agent/RemotePageController.background.ts
================================================
/**
* background logics for RemotePageController
* - redirect messages from RemotePageController(Agent, extension pages) to ContentScript
*/
export function handlePageControlMessage(
message: { type: 'PAGE_CONTROL'; action: string; payload: any; targetTabId: number },
sender: chrome.runtime.MessageSender,
sendResponse: (response: unknown) => void
): true | undefined {
const PREFIX = '[RemotePageController.background]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
const { action, payload, targetTabId } = message
if (action === 'get_my_tab_id') {
debug('get_my_tab_id', sender.tab?.id)
sendResponse({ tabId: sender.tab?.id || null })
return
}
// proxy to content script
chrome.tabs
.sendMessage(targetTabId, {
type: 'PAGE_CONTROL',
action,
payload,
})
.then((result) => {
sendResponse(result)
})
.catch((error) => {
console.error(PREFIX, error)
sendResponse({
success: false,
error: error instanceof Error ? error.message : String(error),
})
})
return true // async response
}
================================================
FILE: packages/extension/src/agent/RemotePageController.content.ts
================================================
/**
* content script for RemotePageController
*/
import { PageController } from '@page-agent/page-controller'
export function initPageController() {
let pageController: PageController | null = null
let intervalID: number | null = null
const myTabIdPromise = chrome.runtime
.sendMessage({ type: 'PAGE_CONTROL', action: 'get_my_tab_id' })
.then((response) => {
return (response as { tabId: number | null }).tabId
})
.catch((error) => {
console.error('[RemotePageController.ContentScript]: Failed to get my tab id', error)
return null
})
function getPC(): PageController {
if (!pageController) {
pageController = new PageController({ enableMask: false, viewportExpansion: 400 })
}
return pageController
}
intervalID = window.setInterval(async () => {
const agentHeartbeat = (await chrome.storage.local.get('agentHeartbeat')).agentHeartbeat
const now = Date.now()
const agentInTouch = typeof agentHeartbeat === 'number' && now - agentHeartbeat < 2_000
const isAgentRunning = (await chrome.storage.local.get('isAgentRunning')).isAgentRunning
const currentTabId = (await chrome.storage.local.get('currentTabId')).currentTabId
const shouldShowMask = isAgentRunning && agentInTouch && currentTabId === (await myTabIdPromise)
if (shouldShowMask) {
const pc = getPC()
pc.initMask()
await pc.showMask()
} else {
// await getPC().hideMask()
if (pageController) {
pageController.hideMask()
pageController.cleanUpHighlights()
}
}
if (!isAgentRunning && agentInTouch) {
if (pageController) {
pageController.dispose()
pageController = null
}
}
}, 500)
chrome.runtime.onMessage.addListener((message, sender, sendResponse): true | undefined => {
if (message.type !== 'PAGE_CONTROL') {
// sendResponse({
// success: false,
// error: `[RemotePageController.ContentScript]: Invalid message type: ${message.type}`,
// })
return
}
const { action, payload } = message
const methodName = getMethodName(action)
const pc = getPC() as any
switch (action) {
case 'get_last_update_time':
case 'get_browser_state':
case 'update_tree':
case 'clean_up_highlights':
case 'click_element':
case 'input_text':
case 'select_option':
case 'scroll':
case 'scroll_horizontally':
case 'execute_javascript':
pc[methodName](...(payload || []))
.then((result: any) => sendResponse(result))
.catch((error: any) =>
sendResponse({
success: false,
error: error instanceof Error ? error.message : String(error),
})
)
break
default:
sendResponse({
success: false,
error: `Unknown PAGE_CONTROL action: ${action}`,
})
}
return true
})
}
function getMethodName(action: string): string {
switch (action) {
case 'get_last_update_time':
return 'getLastUpdateTime' as const
case 'get_browser_state':
return 'getBrowserState' as const
case 'update_tree':
return 'updateTree' as const
case 'clean_up_highlights':
return 'cleanUpHighlights' as const
// DOM actions
case 'click_element':
return 'clickElement' as const
case 'input_text':
return 'inputText' as const
case 'select_option':
return 'selectOption' as const
case 'scroll':
return 'scroll' as const
case 'scroll_horizontally':
return 'scrollHorizontally' as const
case 'execute_javascript':
return 'executeJavascript' as const
default:
return action
}
}
================================================
FILE: packages/extension/src/agent/RemotePageController.ts
================================================
import type { BrowserState } from '@page-agent/page-controller'
import type { TabsController } from './TabsController'
const PREFIX = '[RemotePageController]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
function sendMessage(message: {
type: 'PAGE_CONTROL'
action: string
targetTabId: number
payload?: any
}): Promise<any> {
return chrome.runtime.sendMessage(message).catch((error) => {
console.error(PREFIX, message.action, error)
return null
})
}
/**
* Agent side page controller.
* - live in the agent env (extension page or content script)
* - communicates with remote PageController via sw
*/
export class RemotePageController {
tabsController: TabsController
constructor(tabsController: TabsController) {
this.tabsController = tabsController
}
get currentTabId(): number | null {
return this.tabsController.currentTabId
}
private async getCurrentUrl(): Promise<string> {
if (!this.currentTabId) return ''
const { url } = await this.tabsController.getTabInfo(this.currentTabId)
return url || ''
}
private async getCurrentTitle(): Promise<string> {
if (!this.currentTabId) return ''
const { title } = await this.tabsController.getTabInfo(this.currentTabId)
return title || ''
}
async getLastUpdateTime(): Promise<number> {
if (!this.currentTabId) throw new Error('tabsController not initialized.')
return sendMessage({
type: 'PAGE_CONTROL',
action: 'get_last_update_time',
targetTabId: this.currentTabId,
})
}
async getBrowserState(): Promise<BrowserState> {
let browserState = {} as BrowserState
debug('getBrowserState', this.currentTabId)
const currentUrl = await this.getCurrentUrl()
const currentTitle = await this.getCurrentTitle()
if (!this.currentTabId || !isContentScriptAllowed(currentUrl)) {
browserState = {
url: currentUrl,
title: currentTitle,
header: '',
content: '(empty page. either current page is not readable or not loaded yet.)',
footer: '',
}
} else {
browserState = await sendMessage({
type: 'PAGE_CONTROL',
action: 'get_browser_state',
targetTabId: this.currentTabId,
})
}
const sum = await this.tabsController.summarizeTabs()
browserState.header = sum + '\n\n' + (browserState.header || '')
debug('getBrowserState: success', this.currentTabId, browserState)
return browserState
}
async updateTree(): Promise<void> {
if (!this.currentTabId || !isContentScriptAllowed(await this.getCurrentUrl())) {
return
}
await sendMessage({
type: 'PAGE_CONTROL',
action: 'update_tree',
targetTabId: this.currentTabId,
})
}
async cleanUpHighlights(): Promise<void> {
if (!this.currentTabId || !isContentScriptAllowed(await this.getCurrentUrl())) {
return
}
await sendMessage({
type: 'PAGE_CONTROL',
action: 'clean_up_highlights',
targetTabId: this.currentTabId,
})
}
async clickElement(...args: any[]): Promise<DomActionReturn> {
const res = await this.remoteCallDomAction('click_element', args)
// @note may cause page navigation, wait for 1 second to ensure the page loading started
await new Promise((resolve) => setTimeout(resolve, 1000))
return res
}
async inputText(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('input_text', args)
}
async selectOption(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('select_option', args)
}
async scroll(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('scroll', args)
}
async scrollHorizontally(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('scroll_horizontally', args)
}
async executeJavascript(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('execute_javascript', args)
}
/** @note Managed by content script via storage polling. */
async showMask(): Promise<void> {}
/** @note Managed by content script via storage polling. */
async hideMask(): Promise<void> {}
/** @note Managed by content script via storage polling. */
dispose(): void {}
private async remoteCallDomAction(action: string, payload: any[]): Promise<DomActionReturn> {
if (!this.currentTabId) {
return { success: false, message: 'RemotePageController not initialized.' }
}
if (!isContentScriptAllowed(await this.getCurrentUrl())) {
return {
success: false,
message:
'Operation not allowed on this page. Use open_new_tab to navigate to a web page first.',
}
}
return sendMessage({
type: 'PAGE_CONTROL',
action: action,
targetTabId: this.currentTabId!,
payload,
})
}
}
interface DomActionReturn {
success: boolean
message: string
}
/**
* Check if a URL can run content scripts.
*/
export function isContentScriptAllowed(url: string | undefined): boolean {
if (!url) return false
const restrictedPatterns = [
/^chrome:\/\//,
/^chrome-extension:\/\//,
/^about:/,
/^edge:\/\//,
/^brave:\/\//,
/^opera:\/\//,
/^vivaldi:\/\//,
/^file:\/\//,
/^view-source:/,
/^devtools:\/\//,
]
return !restrictedPatterns.some((pattern) => pattern.test(url))
}
================================================
FILE: packages/extension/src/agent/TabsController.background.ts
================================================
/**
* background logics for TabsController
*/
import type { TabAction } from './TabsController'
const PREFIX = '[TabsController.background]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
export function handleTabControlMessage(
message: { type: 'TAB_CONTROL'; action: TabAction; payload: any },
sender: chrome.runtime.MessageSender,
sendResponse: (response: unknown) => void
): true | undefined {
const { action, payload } = message
switch (action as TabAction) {
case 'get_active_tab': {
debug('get_active_tab')
chrome.tabs
.query({ active: true, currentWindow: true })
.then((tabs) => {
const tabId = tabs.length > 0 ? tabs[0].id || null : null
debug('get_active_tab: success', tabId)
sendResponse({ success: true, tabId })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
case 'get_tab_info': {
debug('get_tab_info', payload)
chrome.tabs
.get(payload.tabId)
.then((tab) => {
debug('get_tab_info: success', tab)
sendResponse(tab)
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
case 'open_new_tab': {
debug('open_new_tab', payload)
chrome.tabs
.create({ url: payload.url, active: false })
.then((newTab) => {
debug('open_new_tab: success', newTab)
sendResponse({ success: true, tabId: newTab.id })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
case 'create_tab_group': {
debug('create_tab_group', payload)
chrome.tabs
.group({ tabIds: payload.tabIds })
.then((groupId) => {
debug('create_tab_group: success', groupId)
sendResponse({ success: true, groupId })
})
.catch((error) => {
console.error(PREFIX, 'Failed to create tab group', error)
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
case 'update_tab_group': {
debug('update_tab_group', payload)
chrome.tabGroups
.update(payload.groupId, payload.properties)
.then(() => {
sendResponse({ success: true })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
case 'add_tab_to_group': {
debug('add_tab_to_group', payload)
chrome.tabs
.group({ tabIds: payload.tabId, groupId: payload.groupId })
.then(() => {
sendResponse({ success: true })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
case 'close_tab': {
debug('close_tab', payload)
chrome.tabs
.remove(payload.tabId)
.then(() => {
sendResponse({ success: true })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
default:
sendResponse({ error: `Unknown action: ${action}` })
return
}
}
export function setupTabChangeEvents() {
console.log('[TabsController.background] setupTabChangeEvents')
chrome.tabs.onCreated.addListener((tab) => {
debug('onCreated', tab)
chrome.runtime
.sendMessage({ type: 'TAB_CHANGE', action: 'created', payload: { tab } })
.catch((error) => {
debug('onCreated error:', error)
})
})
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
debug('onRemoved', tabId, removeInfo)
chrome.runtime
.sendMessage({
type: 'TAB_CHANGE',
action: 'removed',
payload: { tabId, removeInfo },
})
.catch((error) => {
debug('onRemoved error:', error)
})
})
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
debug('onUpdated', tabId, changeInfo)
chrome.runtime
.sendMessage({
type: 'TAB_CHANGE',
action: 'updated',
payload: { tabId, changeInfo, tab },
})
.catch((error) => {
debug('onUpdated error:', error)
})
})
}
================================================
FILE: packages/extension/src/agent/TabsController.ts
================================================
import { isContentScriptAllowed } from './RemotePageController'
const PREFIX = '[TabsController]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
function sendMessage(message: {
type: 'TAB_CONTROL'
action: TabAction
payload?: any
}): Promise<any> {
return chrome.runtime.sendMessage(message).catch((error) => {
console.error(PREFIX, message.action, error)
return null
})
}
/**
* Controller for managing browser tabs.
* - live in the agent env (extension page or content script)
* - no chrome apis. call sw for tab operations
*/
export class TabsController extends EventTarget {
currentTabId: number | null = null
private tabs: TabMeta[] = []
private initialTabId: number | null = null
private tabGroupId: number | null = null
private task: string = ''
async init(task: string, includeInitialTab: boolean = true) {
debug('init', task, includeInitialTab)
this.task = task
this.tabs = []
this.currentTabId = null
this.tabGroupId = null
this.initialTabId = null
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_active_tab',
})
this.initialTabId = result.tabId
if (!this.initialTabId) {
throw new Error('Failed to get initial tab ID')
}
if (includeInitialTab) {
const info = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_tab_info',
payload: { tabId: this.initialTabId },
})
if (isContentScriptAllowed(info.url)) {
this.currentTabId = this.initialTabId
this.tabs.push({
id: result.tabId,
isInitial: true,
url: info.url,
title: info.title,
status: info.status,
})
await this.createTabGroup([this.initialTabId])
}
}
await this.updateCurrentTabId(this.currentTabId)
const tabChangeHandler = (message: any): void => {
if (message.type !== 'TAB_CHANGE') {
// throw new Error(`[TabsController]: Invalid message type: ${message.type}`)
return
}
if (message.action === 'created') {
const tab = message.payload.tab as chrome.tabs.Tab
if (tab.groupId === this.tabGroupId && tab.id != null) {
// Tab created in our controlled group
if (!this.tabs.find((t) => t.id === tab.id)) {
this.tabs.push({ id: tab.id, isInitial: false })
}
this.switchToTab(tab.id)
}
} else if (message.action === 'removed') {
const { tabId } = message.payload as { tabId: number }
const targetTab = this.tabs.find((t) => t.id === tabId)
if (targetTab) {
this.tabs = this.tabs.filter((t) => t.id !== tabId)
if (this.currentTabId === tabId) {
const newCurrentTab = this.tabs[this.tabs.length - 1] || null
if (newCurrentTab) {
this.switchToTab(newCurrentTab.id)
} else {
this.updateCurrentTabId(null)
}
}
}
} else if (message.action === 'updated') {
const { tabId, tab } = message.payload as { tabId: number; tab: chrome.tabs.Tab }
const targetTab = this.tabs.find((t) => t.id === tabId)
if (targetTab) {
targetTab.url = tab.url
targetTab.title = tab.title
targetTab.status = tab.status
}
}
}
chrome.runtime.onMessage.addListener(tabChangeHandler)
this.addEventListener('dispose', () => {
chrome.runtime.onMessage.removeListener(tabChangeHandler)
})
}
async openNewTab(url: string): Promise<string> {
debug('openNewTab', url)
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'open_new_tab',
payload: { url },
})
if (!result.success) {
throw new Error(`Failed to open new tab: ${result.error}`)
}
const tabId = result.tabId as number
this.tabs.push({
id: tabId,
isInitial: false,
})
await this.switchToTab(tabId)
if (!this.tabGroupId) {
await this.createTabGroup([tabId])
} else {
await sendMessage({
type: 'TAB_CONTROL',
action: 'add_tab_to_group',
payload: { tabId: result.tabId, groupId: this.tabGroupId },
})
}
await this.waitUntilTabLoaded(tabId)
return `✅ Opened new tab ID ${tabId} with URL ${url}`
}
async switchToTab(tabId: number): Promise<string> {
debug('switchToTab', tabId)
const targetTab = this.tabs.find((t) => t.id === tabId)
if (!targetTab) {
throw new Error(`Tab ID ${tabId} not found in tab list.`)
}
await this.updateCurrentTabId(tabId)
return `✅ Switched to tab ID ${tabId}.`
}
async closeTab(tabId: number): Promise<string> {
debug('closeTab', tabId)
const targetTab = this.tabs.find((t) => t.id === tabId)
if (!targetTab) {
throw new Error(`Tab ID ${tabId} not found in tab list.`)
}
if (targetTab.isInitial) {
throw new Error(`Cannot close the initial tab ID ${tabId}.`)
}
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'close_tab',
payload: { tabId },
})
if (result.success) {
this.tabs = this.tabs.filter((t) => t.id !== tabId)
if (this.currentTabId === tabId) {
const newCurrentTab = this.tabs[this.tabs.length - 1] || null
if (newCurrentTab) {
await this.switchToTab(newCurrentTab.id)
} else {
await this.updateCurrentTabId(null)
}
}
return `✅ Closed tab ID ${tabId}.`
} else {
throw new Error(`Failed to close tab ID ${tabId}: ${result.error}`)
}
}
private async createTabGroup(tabIds: number[]) {
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'create_tab_group',
payload: { tabIds },
})
if (!result?.success) {
throw new Error(`Failed to create tab group: ${result?.error}`)
}
this.tabGroupId = result.groupId as number
await sendMessage({
type: 'TAB_CONTROL',
action: 'update_tab_group',
payload: {
groupId: this.tabGroupId,
properties: {
title: `PageAgent(${this.task})`,
color: randomColor(),
collapsed: false,
},
},
})
}
async updateCurrentTabId(tabId: number | null) {
debug('updateCurrentTabId', tabId)
this.currentTabId = tabId
await chrome.storage.local.set({ currentTabId: tabId })
}
async getTabInfo(tabId: number): Promise<{ title: string; url: string }> {
// use cached tab info if available
const tabMeta = this.tabs.find((t) => t.id === tabId)
if (tabMeta && tabMeta.url && tabMeta.title) {
return { title: tabMeta.title, url: tabMeta.url }
}
// otherwise, pull the latest tab info from the background script
debug('getTabInfo: pulling from background script', tabId)
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_tab_info',
payload: { tabId },
})
if (tabMeta) {
tabMeta.url = result.url
tabMeta.title = result.title
}
return result
}
async summarizeTabs(): Promise<string> {
const summaries = [`| Tab ID | URL | Title | Current |`, `|-----|-----|-----|-----|`]
for (const tab of this.tabs) {
const { title, url } = await this.getTabInfo(tab.id)
summaries.push(
`| ${tab.id} | ${url} | ${title} | ${this.currentTabId === tab.id ? '✅' : ''} |`
)
}
if (!this.tabs.length) {
summaries.push('\nNo tabs available. Open a tab if needed.')
}
return summaries.join('\n')
}
async waitUntilTabLoaded(tabId: number): Promise<void> {
const tab = this.tabs.find((t) => t.id === tabId)
if (!tab) throw new Error(`Tab ID ${tabId} not found in tab list.`)
if (tab.status === 'unloaded') throw new Error(`Tab ID ${tabId} is unloaded.`)
if (tab.status === 'complete') return
debug('waitUntilTabLoaded', tabId)
await waitUntil(() => tab.status === 'complete', 4_000)
}
dispose() {
this.dispatchEvent(new Event('dispose'))
}
}
export type TabAction =
| 'get_active_tab'
| 'get_tab_info'
| 'open_new_tab'
| 'create_tab_group'
| 'update_tab_group'
| 'add_tab_to_group'
| 'close_tab'
| 'get_tab_title'
interface TabMeta {
id: number
isInitial: boolean
url?: string
title?: string
status?: 'loading' | 'unloaded' | 'complete'
}
const TAB_GROUP_COLORS = ['blue', 'red', 'yellow', 'green', 'pink', 'purple', 'cyan'] as const
type TabGroupColor = (typeof TAB_GROUP_COLORS)[number]
function randomColor(): TabGroupColor {
return TAB_GROUP_COLORS[Math.floor(Math.random() * TAB_GROUP_COLORS.length)]
}
/**
* Wait until condition becomes true
* @returns Returns when condition becomes true, throws otherwise
* @param timeoutMS Timeout in milliseconds, default 1 minutes, throws error on timeout
* @param error Error object to reject on timeout. If not provided, will resolve with false
*/
export async function waitUntil(
check: () => boolean | Promise<boolean>,
timeoutMS = 60_000,
error?: string
): Promise<boolean> {
if (await check()) return true
return new Promise((resolve, reject) => {
const start = Date.now()
const poll = async () => {
if (await check()) return resolve(true)
if (Date.now() - start > timeoutMS) {
if (error) {
return reject(new Error(error))
} else {
return resolve(false)
}
}
setTimeout(poll, 100)
}
setTimeout(poll, 100)
})
}
================================================
FILE: packages/extension/src/agent/constants.ts
================================================
import type { LLMConfig } from '@page-agent/llms'
// Demo LLM for testing
export const DEMO_MODEL = 'qwen3.5-plus'
export const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
// export const DEMO_API_KEY = 'NA'
export const DEMO_CONFIG: LLMConfig = {
baseURL: DEMO_BASE_URL,
model: DEMO_MODEL,
// apiKey: DEMO_API_KEY,
}
/** Legacy testing endpoints that should be auto-migrated to DEMO_BASE_URL */
export const LEGACY_TESTING_ENDPOINTS = [
'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy',
]
export function isTestingEndpoint(url: string): boolean {
const normalized = url.replace(/\/+$/, '')
return normalized === DEMO_BASE_URL || LEGACY_TESTING_ENDPOINTS.some((ep) => normalized === ep)
}
export function migrateLegacyEndpoint(config: LLMConfig): LLMConfig {
const normalized = config.baseURL.replace(/\/+$/, '')
if (LEGACY_TESTING_ENDPOINTS.some((ep) => normalized === ep)) {
return { ...DEMO_CONFIG }
}
return config
}
================================================
FILE: packages/extension/src/agent/system_prompt.md
================================================
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>.
<intro>
You excel at following tasks:
1. Navigating complex websites and extracting precise information
2. Automating form submissions and interactive web actions
3. Gathering and saving information
4. Operate effectively in an agent loop
5. Efficiently performing diverse web tasks
</intro>
<language_settings>
- Default working language: **English**
- Use the language that user is using. Return in user's language.
</language_settings>
<input>
At every step, your input will consist of:
1. <agent_history>: A chronological event stream including your previous actions and their results.
2. <agent_state>: Current <user_request> and <step_info>.
3. <browser_state>: Tabs, Current Tab, Current URL, interactive elements indexed for actions, and visible page content.
</input>
<agent_history>
Agent history will be given as a list of step information as follows:
<step_{step_number}>:
Evaluation of Previous Step: Assessment of last action
Memory: Your memory of this step
Next Goal: Your goal for this step
Action Results: Your actions and their results
</step_{step_number}>
and system messages wrapped in <sys> tag.
</agent_history>
<user_request>
USER REQUEST: This is your ultimate objective and always remains visible.
- This has the highest priority. Make the user happy.
- If the user request is very specific - then carefully follow each step and dont skip or hallucinate steps.
- If the task is open ended you can plan yourself how to get it done.
</user_request>
<browser_state>
1. Browser State will be given as:
Open Tabs: Open tabs with their ids.
Current Tab: The tab you are currently viewing.
Current URL: URL of the page you are currently viewing.
Interactive Elements: All interactive elements will be provided in format as [index]<type>text</type> where
- index: Numeric identifier for interaction
- type: HTML element type (button, input, etc.)
- text: Element description
Examples:
[33]<div>User form</div>
\t*[35]<button aria-label='Submit form'>Submit</button>
Note that:
- Only elements with numeric indexes in [] are interactive
- (stacked) indentation (with \t) is important and means that the element is a (html) child of the element above (with a lower index)
- Elements tagged with `*[` are the new clickable elements that appeared on the website since the last step - if url has not changed.
- Pure text elements without [] are not interactive.
</browser_state>
<browser_rules>
Strictly follow these rules while using the browser and navigating the web:
- Only interact with elements that have a numeric [index] assigned.
- Only use indexes that are explicitly provided.
- 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.
- 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.
- 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).
- 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.
- If a captcha appears, tell user you can not solve captcha. Finish the task and ask user to solve it.
- If expected elements are missing, try scrolling, or navigating back.
- If the page is not fully loaded, use the `wait` action.
- Do not repeat one action for more than 3 times unless some conditions changed.
- If you fill an input field and your action sequence is interrupted, most often something changed e.g. suggestions popped up under the field.
- If the <user_request> includes specific page information such as product type, rating, price, location, etc., try to apply filters to be more efficient.
- The <user_request> is the ultimate goal. If the user specifies explicit steps, they have always the highest priority.
- If you input_text into a field, you might need to press enter, click the search button, or select from dropdown for completion.
- Don't login into a page if you don't have to. Don't login if you don't have the credentials.
- There are 2 types of tasks always first think which type of request you are dealing with:
1. Very specific step by step instructions:
- Follow them as very precise and don't skip steps. Try to complete everything as requested.
2. Open ended tasks. Plan yourself, be creative in achieving them.
- 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.
</browser_rules>
<task_completion_rules>
You must call the `done` action in one of three cases:
- When you have fully completed the USER REQUEST.
- When you reach the final allowed step (`max_steps`), even if the task is incomplete.
- When you feel stuck or unable to solve user request. Or user request is not clear or contains inappropriate content.
- When it is ABSOLUTELY IMPOSSIBLE to continue.
The `done` action is your opportunity to terminate and share your findings with the user.
- Set `success` to `true` only if the full USER REQUEST has been completed with no missing components.
- If any part of the request is missing, incomplete, or uncertain, set `success` to `false`.
- 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.
- You are ONLY ALLOWED to call `done` as a single action. Don't call it together with other actions.
- 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.
- 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!
</task_completion_rules>
<reasoning_rules>
Exhibit the following reasoning patterns to successfully achieve the <user_request>:
- Reason about <agent_history> to track progress and context toward <user_request>.
- Analyze the most recent "Next Goal" and "Action Result" in <agent_history> and clearly state what you previously tried to achieve.
- Analyze all relevant items in <agent_history> and <browser_state> to understand your state.
- 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.
- 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.
- Ask user for help if you have any difficulty. Keep user in the loop.
- If you see information relevant to <user_request>, plan saving the information to memory.
- 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.
</reasoning_rules>
<examples>
Here are examples of good output patterns. Use them as reference but never copy them directly.
<evaluation_examples>
"evaluation_previous_goal": "Successfully navigated to the product page and found the target information. Verdict: Success"
"evaluation_previous_goal": "Clicked the login button and user authentication form appeared. Verdict: Success"
</evaluation_examples>
<memory_examples>
"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."
</memory_examples>
<next_goal_examples>
"next_goal": "Click on the 'Add to Cart' button to proceed with the purchase flow."
</next_goal_examples>
</examples>
<output>
{
"evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.",
"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.",
"next_goal": "State the next immediate goal and action to achieve it, in one clear sentence.",
"action":{
"Action name": {// Action parameters}
}
}
</output>
================================================
FILE: packages/extension/src/agent/tabTools.ts
================================================
/**
* Tab control tools for browser extension
*
* These tools allow the agent to manage multiple browser tabs:
* - open_new_tab: Open a new tab and set it as current
* - switch_to_tab: Switch to an existing tab
* - close_tab: Close a tab (optionally switch to another)
*/
import * as z from 'zod/v4'
import type { TabsController } from './TabsController'
/** Tool definition compatible with PageAgentCore customTools */
interface TabTool {
description: string
inputSchema: z.ZodType
execute: (input: unknown) => Promise<string>
}
/**
* Create tab control tools bound to a TabsManager instance.
* These tools are injected into PageAgentCore via customTools config.
*/
export function createTabTools(tabsController: TabsController): Record<string, TabTool> {
return {
open_new_tab: {
description:
'Open a new browser tab with the specified URL. The new tab becomes the current tab for all subsequent page operations.',
inputSchema: z.object({
url: z.string().describe('The URL to open in the new tab'),
}),
execute: async (input: unknown) => {
const { url } = input as { url: string }
try {
return await tabsController.openNewTab(url)
} catch (error) {
return `❌ Failed: ${error instanceof Error ? error.message : String(error)}`
}
},
},
switch_to_tab: {
description:
'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.',
inputSchema: z.object({
tab_id: z.number().int().describe('The tab ID to switch to'),
}),
execute: async (input: unknown) => {
const { tab_id } = input as { tab_id: number }
try {
return await tabsController.switchToTab(tab_id)
} catch (error) {
return `❌ Failed: ${error instanceof Error ? error.message : String(error)}`
}
},
},
close_tab: {
description:
'Close a tab by its ID. Cannot close the initial tab. Optionally specify which tab to switch to after closing.',
inputSchema: z.object({
tab_id: z.number().int().describe('The tab ID to close'),
}),
execute: async (input: unknown) => {
const { tab_id } = input as { tab_id: number }
try {
return await tabsController.closeTab(tab_id)
} catch (error) {
return `❌ Failed: ${error instanceof Error ? error.message : String(error)}`
}
},
},
}
}
================================================
FILE: packages/extension/src/agent/useAgent.ts
================================================
/**
* React hook for using AgentController
*/
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
SupportedLanguage,
} from '@page-agent/core'
import type { LLMConfig } from '@page-agent/llms'
import { useCallback, useEffect, useRef, useState } from 'react'
import { MultiPageAgent } from './MultiPageAgent'
import { DEMO_CONFIG, migrateLegacyEndpoint } from './constants'
/** Language preference: undefined means follow system */
export type LanguagePreference = SupportedLanguage | undefined
export interface AdvancedConfig {
maxSteps?: number
systemInstruction?: string
experimentalLlmsTxt?: boolean
disableNamedToolChoice?: boolean
}
export interface ExtConfig extends LLMConfig, AdvancedConfig {
language?: LanguagePreference
}
export interface UseAgentResult {
status: AgentStatus
history: HistoricalEvent[]
activity: AgentActivity | null
currentTask: string
config: ExtConfig | null
execute: (task: string) => Promise<ExecutionResult>
stop: () => void
configure: (config: ExtConfig) => Promise<void>
}
export function useAgent(): UseAgentResult {
const agentRef = useRef<MultiPageAgent | null>(null)
const [status, setStatus] = useState<AgentStatus>('idle')
const [history, setHistory] = useState<HistoricalEvent[]>([])
const [activity, setActivity] = useState<AgentActivity | null>(null)
const [currentTask, setCurrentTask] = useState('')
const [config, setConfig] = useState<ExtConfig | null>(null)
useEffect(() => {
chrome.storage.local.get(['llmConfig', 'language', 'advancedConfig']).then((result) => {
let llmConfig = (result.llmConfig as LLMConfig) ?? DEMO_CONFIG
const language = (result.language as SupportedLanguage) || undefined
const advancedConfig = (result.advancedConfig as AdvancedConfig) ?? {}
// Auto-migrate legacy testing endpoints
const migrated = migrateLegacyEndpoint(llmConfig)
if (migrated !== llmConfig) {
llmConfig = migrated
chrome.storage.local.set({ llmConfig: migrated })
} else if (!result.llmConfig) {
chrome.storage.local.set({ llmConfig: DEMO_CONFIG })
}
setConfig({ ...llmConfig, ...advancedConfig, language })
})
}, [])
useEffect(() => {
if (!config) return
const { systemInstruction, ...agentConfig } = config
const agent = new MultiPageAgent({
...agentConfig,
instructions: systemInstruction ? { system: systemInstruction } : undefined,
})
agentRef.current = agent
const handleStatusChange = (e: Event) => {
const newStatus = agent.status as AgentStatus
setStatus(newStatus)
if (newStatus === 'idle' || newStatus === 'completed' || newStatus === 'error') {
setActivity(null)
}
}
const handleHistoryChange = (e: Event) => {
setHistory([...agent.history])
}
const handleActivity = (e: Event) => {
const newActivity = (e as CustomEvent).detail as AgentActivity
setActivity(newActivity)
}
agent.addEventListener('statuschange', handleStatusChange)
agent.addEventListener('historychange', handleHistoryChange)
agent.addEventListener('activity', handleActivity)
return () => {
agent.removeEventListener('statuschange', handleStatusChange)
agent.removeEventListener('historychange', handleHistoryChange)
agent.removeEventListener('activity', handleActivity)
agent.dispose()
}
}, [config])
const execute = useCallback(async (task: string) => {
const agent = agentRef.current
console.log('🚀 [useAgent] start executing task:', task)
if (!agent) throw new Error('Agent not initialized')
setCurrentTask(task)
setHistory([])
return agent.execute(task)
}, [])
const stop = useCallback(() => {
agentRef.current?.stop()
}, [])
const configure = useCallback(
async ({
language,
maxSteps,
systemInstruction,
experimentalLlmsTxt,
disableNamedToolChoice,
...llmConfig
}: ExtConfig) => {
await chrome.storage.local.set({ llmConfig })
if (language) {
await chrome.storage.local.set({ language })
} else {
await chrome.storage.local.remove('language')
}
const advancedConfig: AdvancedConfig = {
maxSteps,
systemInstruction,
experimentalLlmsTxt,
disableNamedToolChoice,
}
await chrome.storage.local.set({ advancedConfig })
setConfig({ ...llmConfig, ...advancedConfig, language })
},
[]
)
return {
status,
history,
activity,
currentTask,
config,
execute,
stop,
configure,
}
}
================================================
FILE: packages/extension/src/assets/index.css
================================================
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.19 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-blink-cursor: blink-cursor 1.2s step-end infinite;
@keyframes blink-cursor {
0%,
49% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
}
}
@keyframes glow-a {
0%,
100% {
opacity: 0.45;
transform: scale(1);
}
50% {
opacity: 0;
transform: scale(1.1);
}
}
@keyframes glow-b {
0%,
100% {
opacity: 0;
transform: scale(1.1);
}
50% {
opacity: 0.45;
transform: scale(1);
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
================================================
FILE: packages/extension/src/components/ConfigPanel.tsx
================================================
import {
Copy,
CornerUpLeft,
ExternalLink,
Eye,
EyeOff,
FoldVertical,
HatGlasses,
Home,
Loader2,
Scale,
UnfoldVertical,
} from 'lucide-react'
import { useEffect, useState } from 'react'
import { siGithub } from 'simple-icons'
import { DEMO_BASE_URL, DEMO_MODEL, isTestingEndpoint } from '@/agent/constants'
import type { ExtConfig, LanguagePreference } from '@/agent/useAgent'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
interface ConfigPanelProps {
config: ExtConfig | null
onSave: (config: ExtConfig) => Promise<void>
onClose: () => void
}
export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL)
const [model, setModel] = useState(config?.model || DEMO_MODEL)
const [apiKey, setApiKey] = useState(config?.apiKey)
const [language, setLanguage] = useState<LanguagePreference>(config?.language)
const [maxSteps, setMaxSteps] = useState<number | undefined>(config?.maxSteps)
const [systemInstruction, setSystemInstruction] = useState(config?.systemInstruction ?? '')
const [experimentalLlmsTxt, setExperimentalLlmsTxt] = useState(
config?.experimentalLlmsTxt ?? false
)
const [disableNamedToolChoice, setDisableNamedToolChoice] = useState(
config?.disableNamedToolChoice ?? false
)
const [advancedOpen, setAdvancedOpen] = useState(false)
const [saving, setSaving] = useState(false)
const [userAuthToken, setUserAuthToken] = useState<string>('')
const [copied, setCopied] = useState(false)
const [showToken, setShowToken] = useState(false)
const [showApiKey, setShowApiKey] = useState(false)
useEffect(() => {
setBaseURL(config?.baseURL || DEMO_BASE_URL)
setModel(config?.model || DEMO_MODEL)
setApiKey(config?.apiKey)
setLanguage(config?.language)
setMaxSteps(config?.maxSteps)
setSystemInstruction(config?.systemInstruction ?? '')
setExperimentalLlmsTxt(config?.experimentalLlmsTxt ?? false)
setDisableNamedToolChoice(config?.disableNamedToolChoice ?? false)
}, [config])
// Poll for user auth token every second until found
useEffect(() => {
let interval: NodeJS.Timeout | null = null
const fetchToken = async () => {
const result = await chrome.storage.local.get('PageAgentExtUserAuthToken')
const token = result.PageAgentExtUserAuthToken
if (typeof token === 'string' && token) {
setUserAuthToken(token)
if (interval) {
clearInterval(interval)
interval = null
}
}
}
fetchToken()
interval = setInterval(fetchToken, 1000)
return () => {
if (interval) clearInterval(interval)
}
}, [])
const handleCopyToken = async () => {
if (userAuthToken) {
await navigator.clipboard.writeText(userAuthToken)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}
const handleSave = async () => {
setSaving(true)
try {
await onSave({
apiKey,
baseURL,
model,
language,
maxSteps: maxSteps || undefined,
systemInstruction: systemInstruction || undefined,
experimentalLlmsTxt,
disableNamedToolChoice,
})
} finally {
setSaving(false)
}
}
return (
<div className="flex flex-col gap-4 p-4 relative">
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold">Settings</h2>
<Button
variant="ghost"
size="icon-sm"
onClick={onClose}
className="absolute top-2 right-3 cursor-pointer"
>
<CornerUpLeft className="size-3.5" />
</Button>
</div>
{/* User Auth Token Section */}
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
<label className="text-xs font-medium text-muted-foreground">User Auth Token</label>
<p className="text-[10px] text-muted-foreground mb-1">
Give a website the ability to call this extension.
</p>
<div className="flex gap-2 items-center">
<Input
readOnly
value={
userAuthToken
? showToken
? userAuthToken
: `${userAuthToken.slice(0, 4)}${'•'.repeat(userAuthToken.length - 8)}${userAuthToken.slice(-4)}`
: 'Loading...'
}
className="text-xs h-8 font-mono bg-background"
/>
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0 cursor-pointer"
onClick={() => setShowToken(!showToken)}
disabled={!userAuthToken}
>
{showToken ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0 cursor-pointer"
onClick={handleCopyToken}
disabled={!userAuthToken}
>
{copied ? <span className="">✓</span> : <Copy className="size-3" />}
</Button>
</div>
</div>
{/* Hub link */}
<a
href="/hub.html"
target="_blank"
className="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"
>
Manage Page Agent Hub
<ExternalLink className="size-3" />
</a>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Base URL</label>
<Input
placeholder="https://api.openai.com/v1"
value={baseURL}
onChange={(e) => setBaseURL(e.target.value)}
className="text-xs h-8"
/>
</div>
{/* Testing API notice */}
{isTestingEndpoint(baseURL) && (
<div className="p-2.5 rounded-md border border-amber-500/30 bg-amber-500/5 text-[11px] text-muted-foreground leading-relaxed">
<Scale className="size-3 inline-block mr-1 -mt-0.5 text-amber-600" />
You are using our testing API. By using this you agree to the{' '}
<a
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
Terms of Use & Privacy Policy
</a>
</div>
)}
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Model</label>
<Input
placeholder="gpt-5.1"
value={model}
onChange={(e) => setModel(e.target.value)}
className="text-xs h-8"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">API Key</label>
<div className="flex gap-2 items-center">
<Input
type={showApiKey ? 'text' : 'password'}
// placeholder="sk-..."
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="text-xs h-8"
/>
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0 cursor-pointer"
onClick={() => setShowApiKey(!showApiKey)}
>
{showApiKey ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
</Button>
</div>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Response Language</label>
<select
value={language ?? ''}
onChange={(e) => setLanguage((e.target.value || undefined) as LanguagePreference)}
className="h-8 text-xs rounded-md border border-input bg-background px-2 cursor-pointer"
>
<option value="">System</option>
<option value="en-US">English</option>
<option value="zh-CN">中文</option>
</select>
</div>
{/* Advanced Config */}
<button
type="button"
onClick={() => setAdvancedOpen(!advancedOpen)}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground cursor-pointer mt-1 font-bold"
>
Advanced
{advancedOpen ? <FoldVertical className="size-3" /> : <UnfoldVertical className="size-3" />}
</button>
{advancedOpen && (
<>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Max Steps</label>
<Input
type="number"
placeholder="40"
min={1}
max={200}
value={maxSteps ?? ''}
onChange={(e) => setMaxSteps(e.target.value ? Number(e.target.value) : undefined)}
className="text-xs h-8 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [-moz-appearance:textfield]"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">System Instruction</label>
<textarea
placeholder="Additional instructions for the agent..."
value={systemInstruction}
onChange={(e) => setSystemInstruction(e.target.value)}
rows={3}
className="text-xs rounded-md border border-input bg-background px-3 py-2 resize-y min-h-[60px]"
/>
</div>
<label className="flex items-center justify-between cursor-pointer">
<span className="text-xs text-muted-foreground">Disable named tool_choice</span>
<Switch checked={disableNamedToolChoice} onCheckedChange={setDisableNamedToolChoice} />
</label>
<label className="flex items-center justify-between cursor-pointer">
<span className="text-xs text-muted-foreground">Experimental llms.txt support</span>
<Switch checked={experimentalLlmsTxt} onCheckedChange={setExperimentalLlmsTxt} />
</label>
</>
)}
<div className="flex gap-2 mt-2">
<Button variant="outline" onClick={onClose} className="flex-1 h-8 text-xs cursor-pointer">
Cancel
</Button>
<Button
onClick={handleSave}
disabled={saving}
className="flex-1 h-8 text-xs cursor-pointer"
>
{saving ? <Loader2 className="size-3 animate-spin" /> : 'Save'}
</Button>
</div>
{/* Footer */}
<div className="mt-4 mb-4 pt-4 border-t border-border/50 flex gap-2 justify-between text-[10px] text-muted-foreground">
<div className="flex flex-col justify-between">
<span>
Version <span className="font-mono">v{__VERSION__}</span>
</span>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 hover:text-foreground"
>
<svg role="img" viewBox="0 0 24 24" className="size-3 fill-current">
<path d={siGithub.path} />
</svg>
<span>Source Code</span>
</a>
</div>
<div className="flex flex-col items-end">
<a
href="https://alibaba.github.io/page-agent/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 hover:text-foreground"
>
<Home className="size-3" />
<span>Home Page</span>
</a>
<a
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 hover:text-foreground"
>
<HatGlasses className="size-3" />
<span>Privacy</span>
</a>
</div>
</div>
{/* attribute */}
<div className="text-[10px] text-muted-foreground bg-background fixed bottom-0 w-full flex justify-around">
<span className="leading-loose">
Built with ♥️ by{' '}
<a
href="https://github.com/gaomeng1900"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
@Simon
</a>
</span>
</div>
</div>
)
}
================================================
FILE: packages/extension/src/components/ErrorBoundary.tsx
================================================
import { AlertTriangle, Eraser, RotateCcw } from 'lucide-react'
import { Component, type ErrorInfo, type ReactNode } from 'react'
import { Button } from '@/components/ui/button'
interface Props {
children: ReactNode
}
interface State {
hasError: boolean
error: Error | null
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null }
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('[ErrorBoundary]', error, errorInfo.componentStack)
}
handleReload = () => {
window.location.reload()
}
handleResetConfig = async () => {
await chrome.storage.local.remove(['llmConfig', 'language', 'advancedConfig'])
window.location.reload()
}
render() {
if (!this.state.hasError) {
return this.props.children
}
return (
<div className="flex flex-col items-center justify-center h-screen bg-background p-6 text-center">
<AlertTriangle className="size-12 text-destructive mb-4" />
<h2 className="text-lg font-semibold mb-2">Something went wrong</h2>
<p className="text-sm text-muted-foreground mb-4 max-w-xs">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={this.handleResetConfig}>
<Eraser className="size-3.5 mr-2" />
Reset Config
</Button>
<Button variant="outline" size="sm" onClick={this.handleReload}>
<RotateCcw className="size-3.5 mr-2" />
Reload Panel
</Button>
</div>
</div>
)
}
}
================================================
FILE: packages/extension/src/components/HistoryDetail.tsx
================================================
import { ArrowLeft, RotateCcw, Trash2 } from 'lucide-react'
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { type SessionRecord, deleteSession, getSession } from '@/lib/db'
import { EventCard } from './cards'
export function HistoryDetail({
sessionId,
onBack,
onRerun,
}: {
sessionId: string
onBack: () => void
onRerun: (task: string) => void
}) {
const [session, setSession] = useState<SessionRecord | null>(null)
useEffect(() => {
getSession(sessionId).then((s) => setSession(s ?? null))
}, [sessionId])
if (!session) {
return (
<div className="flex items-center justify-center h-screen text-xs text-muted-foreground">
Loading...
</div>
)
}
return (
<div className="flex flex-col h-screen bg-background">
{/* Header */}
<header className="flex items-center gap-2 border-b px-3 py-2">
<Button variant="ghost" size="icon-sm" onClick={onBack} className="cursor-pointer">
<ArrowLeft className="size-3.5" />
</Button>
<span className="text-sm font-medium truncate">History</span>
</header>
{/* Task */}
<div className="border-b px-3 py-2 bg-muted/30">
<div className="text-[10px] text-muted-foreground uppercase tracking-wide">Task</div>
<div className="text-xs font-medium" title={session.task}>
{session.task}
</div>
<div className="mt-2 flex items-center gap-2">
<button
type="button"
onClick={() => onRerun(session.task)}
className="flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<RotateCcw className="size-3" />
Run again
</button>
<button
type="button"
onClick={async () => {
await deleteSession(sessionId)
onBack()
}}
className="flex items-center gap-1 text-[10px] text-muted-foreground hover:text-destructive transition-colors cursor-pointer"
>
<Trash2 className="size-3" />
Delete
</button>
</div>
</div>
{/* Events (read-only) */}
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{session.history.map((event, index) => (
// eslint-disable-next-line react-x/no-array-index-key
<EventCard key={index} event={event} />
))}
</div>
</div>
)
}
================================================
FILE: packages/extension/src/components/HistoryList.tsx
================================================
import { ArrowDownToLine, ArrowLeft, CheckCircle, RotateCcw, Trash2, XCircle } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { type SessionRecord, clearSessions, deleteSession, listSessions } from '@/lib/db'
import { downloadHistoryExport } from '@/lib/history-export'
function timeAgo(ts: number): string {
const seconds = Math.floor((Date.now() - ts) / 1000)
if (seconds < 60) return 'just now'
const minutes = Math.floor(seconds / 60)
if (minutes < 60) return `${minutes}m ago`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}h ago`
const days = Math.floor(hours / 24)
return `${days}d ago`
}
export function HistoryList({
onSelect,
onBack,
onRerun,
}: {
onSelect: (id: string) => void
onBack: () => void
onRerun: (task: string) => void
}) {
const [sessions, setSessions] = useState<SessionRecord[]>([])
const [loading, setLoading] = useState(true)
const load = useCallback(async () => {
setSessions(await listSessions())
setLoading(false)
}, [])
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
load()
}, [load])
const handleDelete = async (e: React.MouseEvent, id: string) => {
e.stopPropagation()
await deleteSession(id)
setSessions((prev) => prev.filter((s) => s.id !== id))
}
const handleExport = (e: React.MouseEvent, session: SessionRecord) => {
e.stopPropagation()
downloadHistoryExport(session.task, session.createdAt, session.history)
}
const handleRerun = (e: React.MouseEvent, task: string) => {
e.stopPropagation()
onRerun(task)
}
return (
<div className="flex flex-col h-screen bg-background">
{/* Header */}
<header className="flex items-center gap-2 border-b px-3 py-2">
<Button variant="ghost" size="icon-sm" onClick={onBack} className="cursor-pointer">
<ArrowLeft className="size-3.5" />
</Button>
<span className="text-sm font-medium flex-1">History</span>
{sessions.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={async () => {
await clearSessions()
setSessions([])
}}
className="text-[10px] text-muted-foreground hover:text-destructive cursor-pointer h-6 px-2"
>
<Trash2 className="size-3 mr-1" />
Clear All
</Button>
)}
</header>
{/* List */}
<div className="flex-1 overflow-y-auto">
{loading && (
<div className="flex items-center justify-center h-32 text-xs text-muted-foreground">
Loading...
</div>
)}
{!loading && sessions.length === 0 && (
<div className="flex items-center justify-center h-32 text-xs text-muted-foreground">
No history yet
</div>
)}
{sessions.map((session) => (
<div
key={session.id}
role="button"
tabIndex={0}
onClick={() => onSelect(session.id)}
className="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"
>
{/* Status icon */}
{session.status === 'completed' ? (
<CheckCircle className="size-3.5 text-green-500 shrink-0 mt-0.5" />
) : (
<XCircle className="size-3.5 text-destructive shrink-0 mt-0.5" />
)}
{/* Content */}
<div className="flex-1 min-w-0">
<p className="text-xs font-medium truncate">{session.task}</p>
<div className="flex items-center mt-0.5">
<p className="text-[10px] text-muted-foreground">
{timeAgo(session.createdAt)} · {session.history.length} steps
</p>
<div className="flex items-center gap-0.5 ml-auto opacity-0 group-hover:opacity-100 transition-opacity">
<button
type="button"
onClick={(e) => handleRerun(e, session.task)}
className="p-0.5 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
title="Run task again"
aria-label={`Run history task again: ${session.task}`}
>
<RotateCcw className="size-3" />
</button>
<button
type="button"
onClick={(e) => handleExport(e, session)}
className="p-1 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
title="Export history JSON"
aria-label={`Export history for ${session.task}`}
>
<ArrowDownToLine className="size-3" />
</button>
<button
type="button"
onClick={(e) => handleDelete(e, session.id)}
className="p-0.5 text-muted-foreground hover:text-destructive transition-colors cursor-pointer"
title="Delete history"
aria-label={`Delete history for ${session.task}`}
>
<Trash2 className="size-3" />
</button>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)
}
================================================
FILE: packages/extension/src/components/cards.tsx
================================================
import type {
AgentActivity,
AgentErrorEvent,
AgentStepEvent,
HistoricalEvent,
ObservationEvent,
RetryEvent,
} from '@page-agent/core'
import {
CheckCircle,
Eye,
Globe,
Keyboard,
Mouse,
MoveVertical,
RefreshCw,
Sparkles,
XCircle,
Zap,
} from 'lucide-react'
import { Fragment, useState } from 'react'
import { cn } from '@/lib/utils'
// Result card for done action
function ResultCard({
success,
text,
children,
}: {
success: boolean
text: string
children?: React.ReactNode
}) {
return (
<div
className={cn(
'rounded-lg border p-3',
success ? 'border-green-500/30 bg-green-500/10' : 'border-destructive/30 bg-destructive/10'
)}
>
<div className="flex items-center gap-2 mb-2">
{success ? (
<CheckCircle className="size-3.5 text-green-500" />
) : (
<XCircle className="size-3.5 text-destructive" />
)}
<span
className={cn(
'text-xs font-medium',
success ? 'text-green-600 dark:text-green-400' : 'text-destructive'
)}
>
Result: {success ? 'Success' : 'Failed'}
</span>
</div>
<p className="text-xs text-[11px] text-muted-foreground pl-5 whitespace-pre-wrap">{text}</p>
{children}
</div>
)
}
// Single reflection item with truncation
function ReflectionItem({ icon, value }: { icon: string; value: string }) {
const [expanded, setExpanded] = useState(false)
return (
<Fragment>
<span className="text-xs flex justify-center">{icon}</span>
<span
className={cn(
'text-[11px] text-muted-foreground cursor-pointer hover:text-muted-foreground/70',
!expanded && 'line-clamp-1'
)}
onClick={() => setExpanded(!expanded)}
>
{value}
</span>
</Fragment>
)
}
// Reflection section in step card
function ReflectionSection({
reflection,
}: {
reflection: {
evaluation_previous_goal?: string
memory?: string
next_
gitextract_5g_s5zem/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── deploy-demo.yml │ └── release.yml ├── .gitignore ├── .husky/ │ ├── commit-msg │ └── pre-commit ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── AGENTS.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs/ │ ├── CHANGELOG.md │ ├── CODE_OF_CONDUCT.md │ ├── README-zh.md │ └── terms-and-privacy.md ├── eslint.config.js ├── package.json ├── packages/ │ ├── core/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── PageAgentCore.ts │ │ │ ├── env.d.ts │ │ │ ├── prompts/ │ │ │ │ ├── .prettierignore │ │ │ │ └── system_prompt.md │ │ │ ├── tools/ │ │ │ │ └── index.ts │ │ │ ├── types.ts │ │ │ └── utils/ │ │ │ ├── autoFixer.ts │ │ │ └── index.ts │ │ ├── tsconfig.dts.json │ │ ├── tsconfig.json │ │ └── vite.config.js │ ├── extension/ │ │ ├── .prettierignore │ │ ├── PRIVACY.md │ │ ├── components.json │ │ ├── docs/ │ │ │ └── extension_api.md │ │ ├── package.json │ │ ├── public/ │ │ │ └── _locales/ │ │ │ ├── en/ │ │ │ │ └── messages.json │ │ │ └── zh_CN/ │ │ │ └── messages.json │ │ ├── src/ │ │ │ ├── agent/ │ │ │ │ ├── .prettierignore │ │ │ │ ├── MultiPageAgent.ts │ │ │ │ ├── RemotePageController.background.ts │ │ │ │ ├── RemotePageController.content.ts │ │ │ │ ├── RemotePageController.ts │ │ │ │ ├── TabsController.background.ts │ │ │ │ ├── TabsController.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── system_prompt.md │ │ │ │ ├── tabTools.ts │ │ │ │ └── useAgent.ts │ │ │ ├── assets/ │ │ │ │ └── index.css │ │ │ ├── components/ │ │ │ │ ├── ConfigPanel.tsx │ │ │ │ ├── ErrorBoundary.tsx │ │ │ │ ├── HistoryDetail.tsx │ │ │ │ ├── HistoryList.tsx │ │ │ │ ├── cards.tsx │ │ │ │ ├── misc.tsx │ │ │ │ └── ui/ │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── field.tsx │ │ │ │ ├── hover-card.tsx │ │ │ │ ├── input-group.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── item.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── spinner.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ └── typing-animation.tsx │ │ │ ├── entrypoints/ │ │ │ │ ├── background.ts │ │ │ │ ├── content.ts │ │ │ │ ├── hub/ │ │ │ │ │ ├── App.tsx │ │ │ │ │ ├── hub-ws.ts │ │ │ │ │ ├── index.html │ │ │ │ │ └── main.tsx │ │ │ │ ├── main-world.ts │ │ │ │ └── sidepanel/ │ │ │ │ ├── App.tsx │ │ │ │ ├── index.html │ │ │ │ └── main.tsx │ │ │ ├── lib/ │ │ │ │ ├── db.ts │ │ │ │ ├── history-export.ts │ │ │ │ └── utils.ts │ │ │ └── types/ │ │ │ ├── assets.d.ts │ │ │ ├── globals.d.ts │ │ │ └── markdown.d.ts │ │ ├── tsconfig.json │ │ └── wxt.config.js │ ├── llms/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── OpenAIClient.ts │ │ │ ├── constants.ts │ │ │ ├── errors.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.dts.json │ │ ├── tsconfig.json │ │ └── vite.config.js │ ├── mcp/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── hub-bridge.js │ │ ├── index.js │ │ └── launcher.html │ ├── page-agent/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── PageAgent.ts │ │ │ ├── demo.ts │ │ │ └── env.d.ts │ │ ├── tsconfig.dts.json │ │ ├── tsconfig.json │ │ ├── vite.config.js │ │ └── vite.iife.config.js │ ├── page-controller/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── PageController.ts │ │ │ ├── actions.ts │ │ │ ├── dom/ │ │ │ │ ├── dom_tree/ │ │ │ │ │ ├── index.js │ │ │ │ │ └── type.ts │ │ │ │ ├── getPageInfo.ts │ │ │ │ └── index.ts │ │ │ ├── env.d.ts │ │ │ ├── mask/ │ │ │ │ ├── SimulatorMask.module.css │ │ │ │ ├── SimulatorMask.ts │ │ │ │ ├── checkDarkMode.ts │ │ │ │ └── cursor.module.css │ │ │ ├── patches/ │ │ │ │ ├── antd.ts │ │ │ │ └── react.ts │ │ │ └── utils/ │ │ │ └── index.ts │ │ ├── tsconfig.dts.json │ │ ├── tsconfig.json │ │ └── vite.config.js │ ├── ui/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── env.d.ts │ │ │ ├── i18n/ │ │ │ │ ├── index.ts │ │ │ │ └── locales.ts │ │ │ ├── index.ts │ │ │ ├── motion-css/ │ │ │ │ ├── createMotion.ts │ │ │ │ ├── motion.module.css │ │ │ │ └── readme │ │ │ ├── panel/ │ │ │ │ ├── Panel.module.css │ │ │ │ ├── Panel.ts │ │ │ │ ├── cards.ts │ │ │ │ └── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.dts.json │ │ ├── tsconfig.json │ │ └── vite.config.js │ └── website/ │ ├── AGENTS.md │ ├── components.json │ ├── index.html │ ├── package.json │ ├── public/ │ │ └── robots.txt │ ├── src/ │ │ ├── components/ │ │ │ ├── APIReference.tsx │ │ │ ├── BetaNotice.tsx │ │ │ ├── CodeEditor.tsx │ │ │ ├── Footer.tsx │ │ │ ├── Header.tsx │ │ │ ├── Heading.tsx │ │ │ ├── HighlightSyntax.module.css │ │ │ ├── HighlightSyntax.tsx │ │ │ ├── JSConsole.module.css │ │ │ ├── JSConsole.tsx │ │ │ ├── LanguageSwitcher.tsx │ │ │ ├── ThemeSwitcher.tsx │ │ │ └── ui/ │ │ │ ├── alert.tsx │ │ │ ├── animated-gradient-text.tsx │ │ │ ├── animated-shiny-text.tsx │ │ │ ├── aurora-text.tsx │ │ │ ├── badge.tsx │ │ │ ├── bento-grid.tsx │ │ │ ├── blur-fade.tsx │ │ │ ├── button.tsx │ │ │ ├── highlighter.tsx │ │ │ ├── hyper-text.tsx │ │ │ ├── kbd.tsx │ │ │ ├── magic-card.tsx │ │ │ ├── marquee.tsx │ │ │ ├── neon-gradient-card.tsx │ │ │ ├── particles.tsx │ │ │ ├── separator.tsx │ │ │ ├── sonner.tsx │ │ │ ├── sparkles-text.tsx │ │ │ ├── spinner.tsx │ │ │ ├── switch.tsx │ │ │ ├── text-animate.tsx │ │ │ ├── tooltip.tsx │ │ │ └── typing-animation.tsx │ │ ├── constants.ts │ │ ├── env.d.ts │ │ ├── hooks/ │ │ │ └── useGitHubStars.ts │ │ ├── i18n/ │ │ │ └── context.tsx │ │ ├── index.css │ │ ├── lib/ │ │ │ ├── useDocumentTitle.ts │ │ │ └── utils.ts │ │ ├── main.tsx │ │ ├── pages/ │ │ │ ├── docs/ │ │ │ │ ├── Layout.tsx │ │ │ │ ├── advanced/ │ │ │ │ │ ├── custom-ui/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── page-agent/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── page-agent-core/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── page-controller/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── security-permissions/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── features/ │ │ │ │ │ ├── chrome-extension/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── custom-instructions/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── custom-tools/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── data-masking/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── third-party-agent/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── introduction/ │ │ │ │ ├── limitations/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── overview/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── quick-start/ │ │ │ │ │ └── page.tsx │ │ │ │ └── troubleshooting/ │ │ │ │ └── page.tsx │ │ │ └── home/ │ │ │ ├── FeaturesSection.tsx │ │ │ ├── HeroSection.tsx │ │ │ ├── OneMoreThingSection.tsx │ │ │ ├── ScenariosSection.tsx │ │ │ └── index.tsx │ │ └── router.tsx │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.js ├── scripts/ │ └── sync-version.js ├── tsconfig.base.json └── tsconfig.json
SYMBOL INDEX (571 symbols across 133 files)
FILE: packages/core/src/PageAgentCore.ts
type PageAgentCoreConfig (line 29) | type PageAgentCoreConfig = AgentConfig & { pageController: PageController }
class PageAgentCore (line 61) | class PageAgentCore extends EventTarget {
method constructor (line 97) | constructor(config: PageAgentCoreConfig) {
method status (line 149) | get status(): AgentStatus {
method #emitStatusChange (line 154) | #emitStatusChange(): void {
method #emitHistoryChange (line 159) | #emitHistoryChange(): void {
method #emitActivity (line 167) | #emitActivity(activity: AgentActivity): void {
method #setStatus (line 172) | #setStatus(status: AgentStatus): void {
method pushObservation (line 185) | pushObservation(content: string): void {
method stop (line 190) | stop() {
method execute (line 196) | async execute(task: string): Promise<ExecutionResult> {
method #packMacroTool (line 360) | #packMacroTool(): Tool<MacroToolInput, MacroToolResult> {
method #getSystemPrompt (line 448) | #getSystemPrompt(): string {
method #getInstructions (line 465) | async #getInstructions(): Promise<string> {
method #handleObservations (line 511) | async #handleObservations(step: number): Promise<void> {
method #assembleUserPrompt (line 552) | async #assembleUserPrompt(): Promise<string> {
method #onDone (line 622) | #onDone(success = true) {
method dispose (line 629) | dispose() {
FILE: packages/core/src/tools/index.ts
type PageAgentTool (line 13) | interface PageAgentTool<TParams = any> {
function tool (line 20) | function tool<TParams>(options: PageAgentTool<TParams>): PageAgentTool<T...
FILE: packages/core/src/types.ts
type SupportedLanguage (line 8) | type SupportedLanguage = 'en-US' | 'zh-CN'
type AgentConfig (line 10) | interface AgentConfig extends LLMConfig {
type AgentReflection (line 171) | interface AgentReflection {
type MacroToolInput (line 183) | interface MacroToolInput extends Partial<AgentReflection> {
type MacroToolResult (line 190) | interface MacroToolResult {
type AgentStepEvent (line 198) | interface AgentStepEvent {
type ObservationEvent (line 223) | interface ObservationEvent {
type UserTakeoverEvent (line 231) | interface UserTakeoverEvent {
type RetryEvent (line 238) | interface RetryEvent {
type AgentErrorEvent (line 248) | interface AgentErrorEvent {
type HistoricalEvent (line 257) | type HistoricalEvent =
type AgentStatus (line 267) | type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
type AgentActivity (line 278) | type AgentActivity =
type ExecutionResult (line 285) | interface ExecutionResult {
FILE: packages/core/src/utils/autoFixer.ts
function normalizeResponse (line 21) | function normalizeResponse(response: any, tools?: Map<string, PageAgentT...
function validateAction (line 130) | function validateAction(action: any, tools: Map<string, PageAgentTool>):...
function safeJsonParse (line 173) | function safeJsonParse(input: any): any {
function retrieveJsonFromString (line 189) | function retrieveJsonFromString(str: string): any {
type Choice (line 201) | interface Choice {
FILE: packages/core/src/utils/index.ts
function waitFor (line 5) | async function waitFor(seconds: number): Promise<void> {
function truncate (line 11) | function truncate(text: string, maxLength: number): string {
function randomID (line 20) | function randomID(existingIDs?: string[]): string {
function uid (line 54) | function uid() {
function fetchLlmsTxt (line 63) | async function fetchLlmsTxt(url: string): Promise<string | null> {
function assert (line 103) | function assert(condition: unknown, message?: string, silent?: boolean):...
FILE: packages/extension/src/agent/MultiPageAgent.ts
function detectLanguage (line 9) | function detectLanguage(): 'en-US' | 'zh-CN' {
class MultiPageAgent (line 19) | class MultiPageAgent extends PageAgentCore {
method constructor (line 20) | constructor(config: AgentConfig & { includeInitialTab?: boolean }) {
FILE: packages/extension/src/agent/RemotePageController.background.ts
function handlePageControlMessage (line 6) | function handlePageControlMessage(
FILE: packages/extension/src/agent/RemotePageController.content.ts
function initPageController (line 6) | function initPageController() {
function getMethodName (line 103) | function getMethodName(action: string): string {
FILE: packages/extension/src/agent/RemotePageController.ts
constant PREFIX (line 5) | const PREFIX = '[RemotePageController]'
function debug (line 7) | function debug(...messages: any[]) {
function sendMessage (line 11) | function sendMessage(message: {
class RemotePageController (line 28) | class RemotePageController {
method constructor (line 31) | constructor(tabsController: TabsController) {
method currentTabId (line 35) | get currentTabId(): number | null {
method getCurrentUrl (line 39) | private async getCurrentUrl(): Promise<string> {
method getCurrentTitle (line 45) | private async getCurrentTitle(): Promise<string> {
method getLastUpdateTime (line 51) | async getLastUpdateTime(): Promise<number> {
method getBrowserState (line 60) | async getBrowserState(): Promise<BrowserState> {
method updateTree (line 91) | async updateTree(): Promise<void> {
method cleanUpHighlights (line 103) | async cleanUpHighlights(): Promise<void> {
method clickElement (line 115) | async clickElement(...args: any[]): Promise<DomActionReturn> {
method inputText (line 122) | async inputText(...args: any[]): Promise<DomActionReturn> {
method selectOption (line 126) | async selectOption(...args: any[]): Promise<DomActionReturn> {
method scroll (line 130) | async scroll(...args: any[]): Promise<DomActionReturn> {
method scrollHorizontally (line 134) | async scrollHorizontally(...args: any[]): Promise<DomActionReturn> {
method executeJavascript (line 138) | async executeJavascript(...args: any[]): Promise<DomActionReturn> {
method showMask (line 143) | async showMask(): Promise<void> {}
method hideMask (line 145) | async hideMask(): Promise<void> {}
method dispose (line 147) | dispose(): void {}
method remoteCallDomAction (line 149) | private async remoteCallDomAction(action: string, payload: any[]): Pro...
type DomActionReturn (line 171) | interface DomActionReturn {
function isContentScriptAllowed (line 179) | function isContentScriptAllowed(url: string | undefined): boolean {
FILE: packages/extension/src/agent/TabsController.background.ts
constant PREFIX (line 6) | const PREFIX = '[TabsController.background]'
function debug (line 8) | function debug(...messages: any[]) {
function handleTabControlMessage (line 12) | function handleTabControlMessage(
function setupTabChangeEvents (line 123) | function setupTabChangeEvents() {
FILE: packages/extension/src/agent/TabsController.ts
constant PREFIX (line 3) | const PREFIX = '[TabsController]'
function debug (line 5) | function debug(...messages: any[]) {
function sendMessage (line 9) | function sendMessage(message: {
class TabsController (line 25) | class TabsController extends EventTarget {
method init (line 33) | async init(task: string, includeInitialTab: boolean = true) {
method openNewTab (line 124) | async openNewTab(url: string): Promise<string> {
method switchToTab (line 161) | async switchToTab(tabId: number): Promise<string> {
method closeTab (line 174) | async closeTab(tabId: number): Promise<string> {
method createTabGroup (line 208) | private async createTabGroup(tabIds: number[]) {
method updateCurrentTabId (line 235) | async updateCurrentTabId(tabId: number | null) {
method getTabInfo (line 242) | async getTabInfo(tabId: number): Promise<{ title: string; url: string ...
method summarizeTabs (line 265) | async summarizeTabs(): Promise<string> {
method waitUntilTabLoaded (line 280) | async waitUntilTabLoaded(tabId: number): Promise<void> {
method dispose (line 291) | dispose() {
type TabAction (line 296) | type TabAction =
type TabMeta (line 306) | interface TabMeta {
constant TAB_GROUP_COLORS (line 314) | const TAB_GROUP_COLORS = ['blue', 'red', 'yellow', 'green', 'pink', 'pur...
type TabGroupColor (line 316) | type TabGroupColor = (typeof TAB_GROUP_COLORS)[number]
function randomColor (line 318) | function randomColor(): TabGroupColor {
function waitUntil (line 328) | async function waitUntil(
FILE: packages/extension/src/agent/constants.ts
constant DEMO_MODEL (line 4) | const DEMO_MODEL = 'qwen3.5-plus'
constant DEMO_BASE_URL (line 5) | const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fc...
constant DEMO_CONFIG (line 8) | const DEMO_CONFIG: LLMConfig = {
constant LEGACY_TESTING_ENDPOINTS (line 15) | const LEGACY_TESTING_ENDPOINTS = [
function isTestingEndpoint (line 19) | function isTestingEndpoint(url: string): boolean {
function migrateLegacyEndpoint (line 24) | function migrateLegacyEndpoint(config: LLMConfig): LLMConfig {
FILE: packages/extension/src/agent/tabTools.ts
type TabTool (line 14) | interface TabTool {
function createTabTools (line 24) | function createTabTools(tabsController: TabsController): Record<string, ...
FILE: packages/extension/src/agent/useAgent.ts
type LanguagePreference (line 18) | type LanguagePreference = SupportedLanguage | undefined
type AdvancedConfig (line 20) | interface AdvancedConfig {
type ExtConfig (line 27) | interface ExtConfig extends LLMConfig, AdvancedConfig {
type UseAgentResult (line 31) | interface UseAgentResult {
function useAgent (line 42) | function useAgent(): UseAgentResult {
FILE: packages/extension/src/components/ConfigPanel.tsx
type ConfigPanelProps (line 23) | interface ConfigPanelProps {
function ConfigPanel (line 29) | function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
FILE: packages/extension/src/components/ErrorBoundary.tsx
type Props (line 6) | interface Props {
type State (line 10) | interface State {
class ErrorBoundary (line 15) | class ErrorBoundary extends Component<Props, State> {
method getDerivedStateFromError (line 18) | static getDerivedStateFromError(error: Error): State {
method componentDidCatch (line 22) | componentDidCatch(error: Error, errorInfo: ErrorInfo) {
method render (line 35) | render() {
FILE: packages/extension/src/components/HistoryDetail.tsx
function HistoryDetail (line 9) | function HistoryDetail({
FILE: packages/extension/src/components/HistoryList.tsx
function timeAgo (line 8) | function timeAgo(ts: number): string {
function HistoryList (line 19) | function HistoryList({
FILE: packages/extension/src/components/cards.tsx
function ResultCard (line 26) | function ResultCard({
function ReflectionItem (line 64) | function ReflectionItem({ icon, value }: { icon: string; value: string }) {
function ReflectionSection (line 84) | function ReflectionSection({
function ActionIcon (line 116) | function ActionIcon({ name, className }: { name: string; className?: str...
function CopyButton (line 127) | function CopyButton({ text, label }: { text: string; label: string }) {
function extractPrompt (line 146) | function extractPrompt(rawRequest: unknown, role: 'system' | 'user'): st...
function RawSection (line 158) | function RawSection({ rawRequest, rawResponse }: { rawRequest?: unknown;...
function StepCard (line 221) | function StepCard({ event }: { event: AgentStepEvent }) {
function ObservationCard (line 268) | function ObservationCard({ event }: { event: ObservationEvent }) {
function RetryCard (line 282) | function RetryCard({ event }: { event: RetryEvent }) {
function ErrorCard (line 295) | function ErrorCard({ event }: { event: AgentErrorEvent }) {
function EventCard (line 308) | function EventCard({ event }: { event: HistoricalEvent }) {
function ActivityCard (line 343) | function ActivityCard({ activity }: { activity: AgentActivity }) {
FILE: packages/extension/src/components/misc.tsx
function StatusDot (line 11) | function StatusDot({ status }: { status: AgentStatus }) {
function Logo (line 36) | function Logo({ className }: { className?: string }) {
function MotionOverlay (line 41) | function MotionOverlay({ active }: { active: boolean }) {
function EmptyState (line 94) | function EmptyState() {
FILE: packages/extension/src/components/ui/button.tsx
function Button (line 37) | function Button({
FILE: packages/extension/src/components/ui/card.tsx
function Card (line 5) | function Card({ className, ...props }: React.ComponentProps<'div'>) {
function CardHeader (line 18) | function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
function CardTitle (line 31) | function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
function CardDescription (line 41) | function CardDescription({ className, ...props }: React.ComponentProps<'...
function CardAction (line 51) | function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
function CardContent (line 61) | function CardContent({ className, ...props }: React.ComponentProps<'div'...
function CardFooter (line 65) | function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
FILE: packages/extension/src/components/ui/field.tsx
function FieldSet (line 8) | function FieldSet({ className, ...props }: React.ComponentProps<'fieldse...
function FieldLegend (line 22) | function FieldLegend({
function FieldGroup (line 42) | function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
function Field (line 76) | function Field({
function FieldContent (line 92) | function FieldContent({ className, ...props }: React.ComponentProps<'div...
function FieldLabel (line 102) | function FieldLabel({ className, ...props }: React.ComponentProps<typeof...
function FieldTitle (line 117) | function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
function FieldDescription (line 130) | function FieldDescription({ className, ...props }: React.ComponentProps<...
function FieldSeparator (line 145) | function FieldSeparator({
function FieldError (line 175) | function FieldError({
FILE: packages/extension/src/components/ui/hover-card.tsx
function HoverCard (line 6) | function HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPr...
function HoverCardTrigger (line 10) | function HoverCardTrigger({ ...props }: React.ComponentProps<typeof Hove...
function HoverCardContent (line 14) | function HoverCardContent({
FILE: packages/extension/src/components/ui/input-group.tsx
function InputGroup (line 9) | function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
function InputGroupAddon (line 57) | function InputGroupAddon({
function InputGroupButton (line 93) | function InputGroupButton({
function InputGroupText (line 112) | function InputGroupText({ className, ...props }: React.ComponentProps<'s...
function InputGroupInput (line 124) | function InputGroupInput({ className, ...props }: React.ComponentProps<'...
function InputGroupTextarea (line 137) | function InputGroupTextarea({ className, ...props }: React.ComponentProp...
FILE: packages/extension/src/components/ui/input.tsx
function Input (line 5) | function Input({ className, type, ...props }: React.ComponentProps<'inpu...
FILE: packages/extension/src/components/ui/item.tsx
function ItemGroup (line 8) | function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
function ItemSeparator (line 19) | function ItemSeparator({ className, ...props }: React.ComponentProps<typ...
function Item (line 51) | function Item({
function ItemMedia (line 86) | function ItemMedia({
function ItemContent (line 101) | function ItemContent({ className, ...props }: React.ComponentProps<'div'...
function ItemTitle (line 111) | function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
function ItemDescription (line 121) | function ItemDescription({ className, ...props }: React.ComponentProps<'...
function ItemActions (line 135) | function ItemActions({ className, ...props }: React.ComponentProps<'div'...
function ItemHeader (line 141) | function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
function ItemFooter (line 151) | function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
FILE: packages/extension/src/components/ui/label.tsx
function Label (line 6) | function Label({ className, ...props }: React.ComponentProps<typeof Labe...
FILE: packages/extension/src/components/ui/separator.tsx
function Separator (line 6) | function Separator({
FILE: packages/extension/src/components/ui/spinner.tsx
function Spinner (line 5) | function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
FILE: packages/extension/src/components/ui/switch.tsx
function Switch (line 6) | function Switch({ className, ...props }: React.ComponentProps<typeof Swi...
FILE: packages/extension/src/components/ui/textarea.tsx
function Textarea (line 5) | function Textarea({ className, ...props }: React.ComponentProps<'textare...
FILE: packages/extension/src/components/ui/typing-animation.tsx
type TypingAnimationProps (line 6) | interface TypingAnimationProps extends MotionProps {
function TypingAnimation (line 23) | function TypingAnimation({
FILE: packages/extension/src/entrypoints/background.ts
function openOrFocusHubTab (line 50) | async function openOrFocusHubTab(wsPort: number) {
FILE: packages/extension/src/entrypoints/content.ts
constant DEBUG_PREFIX (line 5) | const DEBUG_PREFIX = '[Content]'
method main (line 11) | main() {
function exposeAgentToPage (line 39) | async function exposeAgentToPage() {
FILE: packages/extension/src/entrypoints/hub/App.tsx
function App (line 12) | function App() {
function HubConfig (line 147) | function HubConfig() {
function ProtocolDocsCollapsible (line 193) | function ProtocolDocsCollapsible() {
FILE: packages/extension/src/entrypoints/hub/hub-ws.ts
type ExecuteMessage (line 23) | interface ExecuteMessage {
type StopMessage (line 29) | interface StopMessage {
type InboundMessage (line 33) | type InboundMessage = ExecuteMessage | StopMessage
type ReadyMessage (line 35) | interface ReadyMessage {
type ResultMessage (line 39) | interface ResultMessage {
type ErrorMessage (line 45) | interface ErrorMessage {
type OutboundMessage (line 50) | type OutboundMessage = ReadyMessage | ResultMessage | ErrorMessage
type HubWsState (line 52) | type HubWsState = 'connecting' | 'connected' | 'disconnected'
type HubWsHandlers (line 56) | interface HubWsHandlers {
class HubWs (line 69) | class HubWs {
method constructor (line 78) | constructor(port: number, handlers: HubWsHandlers, onStateChange: (sta...
method state (line 84) | get state() {
method busy (line 88) | get busy() {
method connect (line 92) | connect() {
method disconnect (line 116) | disconnect() {
method #setState (line 124) | #setState(state: HubWsState) {
method #send (line 130) | #send(msg: OutboundMessage) {
method #handleMessage (line 136) | async #handleMessage(raw: string) {
method #checkApproval (line 159) | async #checkApproval(): Promise<boolean> {
method #handleExecute (line 175) | async #handleExecute(msg: ExecuteMessage) {
function useHubWs (line 199) | function useHubWs(
FILE: packages/extension/src/entrypoints/main-world.ts
type Execute (line 3) | type Execute = (task: string, config: ExecuteConfig) => Promise<Executio...
type ExecuteConfig (line 5) | interface ExecuteConfig {
function getId (line 23) | function getId() {
function handleMessage (line 38) | function handleMessage(e: MessageEvent) {
FILE: packages/extension/src/entrypoints/sidepanel/App.tsx
type View (line 20) | type View =
function App (line 26) | function App() {
FILE: packages/extension/src/lib/db.ts
constant DB_NAME (line 4) | const DB_NAME = 'page-agent-ext'
constant DB_VERSION (line 5) | const DB_VERSION = 1
type SessionRecord (line 7) | interface SessionRecord {
type PageAgentDB (line 15) | interface PageAgentDB extends DBSchema {
function getDB (line 25) | function getDB() {
function saveSession (line 37) | async function saveSession(
function listSessions (line 51) | async function listSessions(): Promise<SessionRecord[]> {
function getSession (line 57) | async function getSession(id: string): Promise<SessionRecord | undefined> {
function deleteSession (line 62) | async function deleteSession(id: string): Promise<void> {
function clearSessions (line 67) | async function clearSessions(): Promise<void> {
FILE: packages/extension/src/lib/history-export.ts
constant EXPORT_FILE_PREFIX (line 3) | const EXPORT_FILE_PREFIX = 'page-agent-history'
constant MAX_TASK_SLUG_LENGTH (line 4) | const MAX_TASK_SLUG_LENGTH = 40
function serializeHistoryExport (line 6) | function serializeHistoryExport(history: HistoricalEvent[]): string {
function buildHistoryExportFilename (line 10) | function buildHistoryExportFilename(task: string, createdAt: number): st...
function downloadHistoryExport (line 19) | function downloadHistoryExport(
function sanitizeTaskForFilename (line 37) | function sanitizeTaskForFilename(task: string): string {
function formatTimestampForFilename (line 46) | function formatTimestampForFilename(createdAt: number): string {
function pad (line 58) | function pad(value: number): string {
FILE: packages/extension/src/lib/utils.ts
function cn (line 4) | function cn(...inputs: ClassValue[]) {
FILE: packages/llms/src/OpenAIClient.ts
class OpenAIClient (line 13) | class OpenAIClient implements LLMClient {
method constructor (line 17) | constructor(config: Required<LLMConfig>) {
method invoke (line 22) | async invoke(
FILE: packages/llms/src/constants.ts
constant LLM_MAX_RETRIES (line 2) | const LLM_MAX_RETRIES = 2
constant DEFAULT_TEMPERATURE (line 3) | const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
FILE: packages/llms/src/errors.ts
type InvokeErrorType (line 22) | type InvokeErrorType = (typeof InvokeErrorType)[keyof typeof InvokeError...
class InvokeError (line 24) | class InvokeError extends Error {
method constructor (line 33) | constructor(type: InvokeErrorType, message: string, rawError?: unknown...
method isRetryable (line 42) | private isRetryable(type: InvokeErrorType, rawError?: unknown): boolean {
FILE: packages/llms/src/index.ts
function parseLLMConfig (line 9) | function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
class LLM (line 29) | class LLM extends EventTarget {
method constructor (line 33) | constructor(config: LLMConfig) {
method invoke (line 46) | async invoke(
function withRetry (line 77) | async function withRetry<T>(
FILE: packages/llms/src/types.ts
type Message (line 9) | interface Message {
type Tool (line 28) | interface Tool<TParams = any, TResult = any> {
type InvokeOptions (line 38) | interface InvokeOptions {
type LLMClient (line 57) | interface LLMClient {
type InvokeResult (line 69) | interface InvokeResult<TResult = unknown> {
type LLMConfig (line 90) | interface LLMConfig {
FILE: packages/llms/src/utils.ts
function zodToOpenAITool (line 15) | function zodToOpenAITool(name: string, tool: Tool) {
function modelPatch (line 30) | function modelPatch(body: Record<string, any>) {
function normalizeModelName (line 123) | function normalizeModelName(modelName: string): string {
FILE: packages/mcp/src/hub-bridge.js
constant EXT_ID (line 7) | const EXT_ID = 'akldabonmimlicnjlflnapfeklbfemhj'
constant STORE_URL (line 8) | const STORE_URL = `https://chromewebstore.google.com/detail/page-agent-e...
class HubBridge (line 20) | class HubBridge {
method constructor (line 37) | constructor(port) {
method start (line 52) | async start() {
method connected (line 70) | get connected() {
method busy (line 74) | get busy() {
method executeTask (line 83) | async executeTask(task, config) {
method stopTask (line 93) | stopTask() {
method #onConnection (line 102) | #onConnection(ws) {
FILE: packages/page-agent/src/PageAgent.ts
type PageAgentConfig (line 11) | type PageAgentConfig = AgentConfig & PageControllerConfig
class PageAgent (line 13) | class PageAgent extends PageAgentCore {
method constructor (line 16) | constructor(config: PageAgentConfig) {
FILE: packages/page-agent/src/demo.ts
constant DEMO_MODEL (line 16) | const DEMO_MODEL = 'qwen3.5-plus'
constant DEMO_BASE_URL (line 17) | const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fc...
constant DEMO_API_KEY (line 18) | const DEMO_API_KEY = 'NA'
FILE: packages/page-agent/src/env.d.ts
type Window (line 5) | interface Window {
FILE: packages/page-controller/src/PageController.ts
type PageControllerConfig (line 26) | interface PageControllerConfig extends dom.DomConfig {
type BrowserState (line 34) | interface BrowserState {
type ActionResult (line 45) | interface ActionResult {
class PageController (line 58) | class PageController extends EventTarget {
method constructor (line 89) | constructor(config: PageControllerConfig = {}) {
method initMask (line 102) | initMask() {
method getCurrentUrl (line 114) | async getCurrentUrl(): Promise<string> {
method getLastUpdateTime (line 121) | async getLastUpdateTime(): Promise<number> {
method getBrowserState (line 129) | async getBrowserState(): Promise<BrowserState> {
method updateTree (line 174) | async updateTree(): Promise<string> {
method cleanUpHighlights (line 220) | async cleanUpHighlights(): Promise<void> {
method assertIndexed (line 230) | private assertIndexed(): void {
method clickElement (line 239) | async clickElement(index: number): Promise<ActionResult> {
method inputText (line 269) | async inputText(index: number, text: string): Promise<ActionResult> {
method selectOption (line 291) | async selectOption(index: number, optionText: string): Promise<ActionR...
method scroll (line 313) | async scroll(options: {
method scrollHorizontally (line 345) | async scrollHorizontally(options: {
method executeJavascript (line 376) | async executeJavascript(script: string): Promise<ActionResult> {
method showMask (line 399) | async showMask(): Promise<void> {
method hideMask (line 408) | async hideMask(): Promise<void> {
method dispose (line 416) | dispose(): void {
FILE: packages/page-controller/src/actions.ts
function getElementByIndex (line 19) | function getElementByIndex(
function blurLastClickedElement (line 42) | function blurLastClickedElement() {
function clickElement (line 58) | async function clickElement(element: HTMLElement) {
function inputTextElement (line 92) | async function inputTextElement(element: HTMLElement, text: string) {
function selectOptionElement (line 200) | async function selectOptionElement(selectElement: HTMLSelectElement, opt...
type ScrollableElement (line 218) | interface ScrollableElement extends Element {
function scrollIntoViewIfNeeded (line 222) | async function scrollIntoViewIfNeeded(element: Element) {
function scrollVertically (line 234) | async function scrollVertically(
function scrollHorizontally (line 362) | async function scrollHorizontally(
FILE: packages/page-controller/src/dom/dom_tree/index.js
function addExtraData (line 54) | function addExtraData(element, data) {
function getCachedBoundingRect (line 77) | function getCachedBoundingRect(element) {
function getCachedComputedStyle (line 98) | function getCachedComputedStyle(element) {
function getCachedClientRects (line 119) | function getCachedClientRects(element) {
function highlightElement (line 166) | function highlightElement(element, index, parentIframe = null) {
function getElementPosition (line 433) | function getElementPosition(currentElement) {
function getXPathTree (line 452) | function getXPathTree(element, stopAtBoundary = true) {
function isScrollableElement (line 487) | function isScrollableElement(element) {
function isTextNodeVisible (line 558) | function isTextNodeVisible(textNode) {
function isElementAccepted (line 639) | function isElementAccepted(element) {
function isElementVisible (line 676) | function isElementVisible(element) {
function isInteractiveElement (line 695) | function isInteractiveElement(element) {
function isTopElement (line 962) | function isTopElement(element) {
function isInExpandedViewport (line 1074) | function isInExpandedViewport(element, viewportExpansion) {
function isInteractiveCandidate (line 1146) | function isInteractiveCandidate(element) {
function isHeuristicallyInteractive (line 1219) | function isHeuristicallyInteractive(element) {
function isElementDistinctInteraction (line 1263) | function isElementDistinctInteraction(element) {
function handleHighlighting (line 1381) | function handleHighlighting(nodeData, node, parentIframe, isParentHighli...
function buildDomTree (line 1433) | function buildDomTree(node, parentIframe = null, isParentHighlighted = f...
FILE: packages/page-controller/src/dom/dom_tree/type.ts
type FlatDomTree (line 4) | interface FlatDomTree {
type DomNode (line 9) | type DomNode = TextDomNode | ElementDomNode | InteractiveElementDomNode
type TextDomNode (line 11) | interface TextDomNode {
type ElementDomNode (line 19) | interface ElementDomNode {
type InteractiveElementDomNode (line 35) | interface InteractiveElementDomNode {
FILE: packages/page-controller/src/dom/getPageInfo.ts
function getPageInfo (line 1) | function getPageInfo() {
FILE: packages/page-controller/src/dom/index.ts
constant DEFAULT_VIEWPORT_EXPANSION (line 18) | const DEFAULT_VIEWPORT_EXPANSION = -1
function resolveViewportExpansion (line 20) | function resolveViewportExpansion(viewportExpansion?: number): number {
type DomConfig (line 24) | interface DomConfig {
function getFlatTree (line 38) | function getFlatTree(config: DomConfig): FlatDomTree {
function globToRegex (line 96) | function globToRegex(pattern: string): RegExp {
function matchAttributes (line 106) | function matchAttributes(
type TreeNode (line 134) | interface TreeNode {
function flatTreeToString (line 174) | function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: str...
function getSelectorMap (line 451) | function getSelectorMap(flatTree: FlatDomTree): Map<number, InteractiveE...
function getElementTextMap (line 465) | function getElementTextMap(simplifiedHTML: string) {
function cleanUpHighlights (line 483) | function cleanUpHighlights() {
FILE: packages/page-controller/src/mask/SimulatorMask.ts
class SimulatorMask (line 8) | class SimulatorMask {
method constructor (line 21) | constructor() {
method #createCursor (line 87) | #createCursor() {
method #moveCursorToTarget (line 108) | #moveCursorToTarget() {
method setCursorPosition (line 135) | setCursorPosition(x: number, y: number) {
method triggerClickAnimation (line 140) | triggerClickAnimation() {
method show (line 147) | show() {
method hide (line 165) | hide() {
method dispose (line 179) | dispose() {
FILE: packages/page-controller/src/mask/checkDarkMode.ts
function hasDarkModeClass (line 5) | function hasDarkModeClass() {
function parseRgbColor (line 32) | function parseRgbColor(colorString: string) {
function isColorDark (line 50) | function isColorDark(colorString: string, threshold = 128) {
function isBackgroundDark (line 70) | function isBackgroundDark() {
function isPageDark (line 95) | function isPageDark() {
FILE: packages/page-controller/src/patches/antd.ts
function fixAntdSelect (line 9) | function fixAntdSelect() {
function patchAntd (line 14) | function patchAntd(pageController: PageController) {
FILE: packages/page-controller/src/patches/react.ts
function patchReact (line 4) | function patchReact(pageController: PageController) {
FILE: packages/page-controller/src/utils/index.ts
function isHTMLElement (line 4) | function isHTMLElement(el: unknown): el is HTMLElement {
function isInputElement (line 9) | function isInputElement(el: Element): el is HTMLInputElement {
function isTextAreaElement (line 13) | function isTextAreaElement(el: Element): el is HTMLTextAreaElement {
function isSelectElement (line 17) | function isSelectElement(el: Element): el is HTMLSelectElement {
function isAnchorElement (line 21) | function isAnchorElement(el: Element): el is HTMLAnchorElement {
function getIframeOffset (line 28) | function getIframeOffset(element: HTMLElement): { x: number; y: number } {
function getNativeValueSetter (line 39) | function getNativeValueSetter(element: HTMLInputElement | HTMLTextAreaEl...
function waitFor (line 47) | async function waitFor(seconds: number): Promise<void> {
function movePointerToElement (line 53) | async function movePointerToElement(element: HTMLElement) {
FILE: packages/ui/src/i18n/index.ts
class I18n (line 9) | class I18n {
method constructor (line 13) | constructor(language: SupportedLanguage = 'en-US') {
method t (line 19) | t(key: TranslationKey, params?: TranslationParams): string {
method getNestedValue (line 32) | private getNestedValue(obj: any, path: string): string | undefined {
method interpolate (line 36) | private interpolate(template: string, params: TranslationParams): stri...
method getLanguage (line 43) | getLanguage(): SupportedLanguage {
FILE: packages/ui/src/i18n/locales.ts
type DeepStringify (line 98) | type DeepStringify<T> = {
type TranslationSchema (line 102) | type TranslationSchema = DeepStringify<typeof enUS>
type NestedKeyOf (line 105) | type NestedKeyOf<ObjectType extends object> = {
type TranslationKey (line 112) | type TranslationKey = NestedKeyOf<TranslationSchema>
type TranslationParams (line 115) | type TranslationParams = Record<string, string | number>
type SupportedLanguage (line 122) | type SupportedLanguage = keyof typeof locales
FILE: packages/ui/src/motion-css/createMotion.ts
function createMotion (line 3) | function createMotion() {
FILE: packages/ui/src/panel/Panel.ts
type PanelConfig (line 11) | interface PanelConfig {
class Panel (line 30) | class Panel {
method wrapper (line 56) | get wrapper(): HTMLElement {
method constructor (line 65) | constructor(agent: PanelAgentAdapter, config: PanelConfig = {}) {
method #handleStatusChange (line 100) | #handleStatusChange(): void {
method #handleHistoryChange (line 135) | #handleHistoryChange(): void {
method #handleActivity (line 143) | #handleActivity(activity: AgentActivity): void {
method #askUser (line 174) | #askUser(question: string): Promise<string> {
method show (line 203) | show(): void {
method hide (line 210) | hide(): void {
method reset (line 216) | reset(): void {
method expand (line 228) | expand(): void {
method collapse (line 232) | collapse(): void {
method dispose (line 239) | dispose(): void {
method #getToolExecutingText (line 254) | #getToolExecutingText(toolName: string, args: unknown): string {
method #handleActionButton (line 279) | #handleActionButton(): void {
method #submitTask (line 290) | #submitTask() {
method #handleUserAnswer (line 309) | #handleUserAnswer(input: string): void {
method #showInputArea (line 330) | #showInputArea(placeholder?: string): void {
method #hideInputArea (line 344) | #hideInputArea(): void {
method #shouldShowInputArea (line 351) | #shouldShowInputArea(): boolean {
method #createWrapper (line 371) | #createWrapper(): HTMLElement {
method #setupEventListeners (line 419) | #setupEventListeners(): void {
method #toggle (line 457) | #toggle(): void {
method #expand (line 465) | #expand(): void {
method #collapse (line 471) | #collapse(): void {
method #startHeaderUpdateLoop (line 480) | #startHeaderUpdateLoop(): void {
method #stopHeaderUpdateLoop (line 490) | #stopHeaderUpdateLoop(): void {
method #checkAndUpdateHeader (line 500) | #checkAndUpdateHeader(): void {
method #animateTextChange (line 521) | #animateTextChange(newText: string): void {
method #updateStatusIndicator (line 542) | #updateStatusIndicator(
method #scrollToBottom (line 552) | #scrollToBottom(): void {
method #renderHistory (line 568) | #renderHistory(): void {
method #createTaskCard (line 587) | #createTaskCard(task: string): string {
method #createHistoryCards (line 592) | #createHistoryCards(event: PanelAgentAdapter['history'][number]): stri...
method #createActionCards (line 634) | #createActionCards(
FILE: packages/ui/src/panel/cards.ts
type CardType (line 8) | type CardType = 'default' | 'input' | 'output' | 'question' | 'observation'
type CardOptions (line 10) | interface CardOptions {
function createCard (line 18) | function createCard({ icon, content, meta, type }: CardOptions): string {
function createReflectionLines (line 36) | function createReflectionLines(reflection: {
FILE: packages/ui/src/panel/types.ts
type AgentActivity (line 12) | type AgentActivity =
type PanelAgentAdapter (line 30) | interface PanelAgentAdapter extends EventTarget {
FILE: packages/ui/src/utils.ts
function truncate (line 1) | function truncate(text: string, maxLength: number): string {
function escapeHtml (line 11) | function escapeHtml(text: string): string {
FILE: packages/website/src/components/APIReference.tsx
type PropDefinition (line 15) | interface PropDefinition {
type APIReferenceProps (line 30) | interface APIReferenceProps {
function APIReference (line 47) | function APIReference({
function PropRow (line 93) | function PropRow({ name, type, required, defaultValue, description, stat...
function TypeRef (line 156) | function TypeRef({ children }: { children: React.ReactNode }) {
function APIDivider (line 165) | function APIDivider({ title }: { title: string }) {
FILE: packages/website/src/components/BetaNotice.tsx
function BetaNotice (line 3) | function BetaNotice() {
FILE: packages/website/src/components/CodeEditor.tsx
type CodeEditorProps (line 8) | interface CodeEditorProps {
FILE: packages/website/src/components/Footer.tsx
function Footer (line 5) | function Footer() {
FILE: packages/website/src/components/Header.tsx
function Header (line 13) | function Header() {
FILE: packages/website/src/components/Heading.tsx
type Level (line 5) | type Level = 2 | 3
type HeadingProps (line 7) | interface HeadingProps extends Omit<ComponentPropsWithoutRef<'h2'>, 'chi...
function Heading (line 18) | function Heading({ id, level = 2, className, children, ...props }: Headi...
FILE: packages/website/src/components/HighlightSyntax.tsx
type HighlightSyntaxProps (line 8) | interface HighlightSyntaxProps {
function escapeHtml (line 28) | function escapeHtml(text: string): string {
function highlightSyntax (line 33) | function highlightSyntax(code: string): string {
FILE: packages/website/src/components/JSConsole.tsx
class ConsoleInterceptor (line 12) | class ConsoleInterceptor {
method constructor (line 22) | private constructor() {
method getInstance (line 30) | static getInstance() {
method subscribe (line 37) | subscribe(callback: (type: string, args: unknown[]) => void) {
method unsubscribe (line 42) | unsubscribe(callback: (type: string, args: unknown[]) => void) {
method startIntercepting (line 49) | private startIntercepting() {
method stopIntercepting (line 70) | private stopIntercepting() {
method notifySubscribers (line 79) | private notifySubscribers(type: string, args: unknown[]) {
type JSConsoleProps (line 86) | interface JSConsoleProps {
type JSConsoleRef (line 94) | interface JSConsoleRef {
type OutputItem (line 100) | interface OutputItem {
constant DEFAULT_CONTEXT (line 106) | const DEFAULT_CONTEXT = {}
function JSConsole (line 108) | function JSConsole({
FILE: packages/website/src/components/LanguageSwitcher.tsx
function LanguageSwitcher (line 5) | function LanguageSwitcher() {
FILE: packages/website/src/components/ThemeSwitcher.tsx
type Theme (line 3) | type Theme = 'light' | 'dark'
function ThemeSwitcher (line 5) | function ThemeSwitcher() {
FILE: packages/website/src/components/ui/alert.tsx
function Alert (line 22) | function Alert({
function AlertTitle (line 37) | function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
function AlertDescription (line 47) | function AlertDescription({ className, ...props }: React.ComponentProps<...
FILE: packages/website/src/components/ui/animated-gradient-text.tsx
type AnimatedGradientTextProps (line 5) | interface AnimatedGradientTextProps extends ComponentPropsWithoutRef<'di...
function AnimatedGradientText (line 11) | function AnimatedGradientText({
FILE: packages/website/src/components/ui/animated-shiny-text.tsx
type AnimatedShinyTextProps (line 5) | interface AnimatedShinyTextProps extends ComponentPropsWithoutRef<'span'> {
FILE: packages/website/src/components/ui/aurora-text.tsx
type AuroraTextProps (line 3) | interface AuroraTextProps {
FILE: packages/website/src/components/ui/badge.tsx
function Badge (line 26) | function Badge({
FILE: packages/website/src/components/ui/bento-grid.tsx
type BentoGridProps (line 7) | interface BentoGridProps extends ComponentPropsWithoutRef<'div'> {
type BentoCardProps (line 12) | interface BentoCardProps extends ComponentPropsWithoutRef<'div'> {
FILE: packages/website/src/components/ui/blur-fade.tsx
type MarginType (line 11) | type MarginType = UseInViewOptions['margin']
type BlurFadeProps (line 13) | interface BlurFadeProps extends MotionProps {
function BlurFade (line 29) | function BlurFade({
FILE: packages/website/src/components/ui/button.tsx
function Button (line 37) | function Button({
FILE: packages/website/src/components/ui/highlighter.tsx
type AnnotationAction (line 7) | type AnnotationAction =
type HighlighterProps (line 16) | interface HighlighterProps {
function Highlighter (line 28) | function Highlighter({
FILE: packages/website/src/components/ui/hyper-text.tsx
type CharacterSet (line 6) | type CharacterSet = string[] | readonly string[]
type HyperTextProps (line 8) | interface HyperTextProps extends MotionProps {
constant DEFAULT_CHARACTER_SET (line 27) | const DEFAULT_CHARACTER_SET = Object.freeze(
function HyperText (line 33) | function HyperText({
FILE: packages/website/src/components/ui/kbd.tsx
function Kbd (line 3) | function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
function KbdGroup (line 18) | function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
FILE: packages/website/src/components/ui/magic-card.tsx
type MagicCardProps (line 6) | interface MagicCardProps {
function MagicCard (line 16) | function MagicCard({
FILE: packages/website/src/components/ui/marquee.tsx
type MarqueeProps (line 5) | interface MarqueeProps extends ComponentPropsWithoutRef<'div'> {
function Marquee (line 36) | function Marquee({
FILE: packages/website/src/components/ui/neon-gradient-card.tsx
type NeonColorsProps (line 5) | interface NeonColorsProps {
type NeonGradientCardProps (line 10) | interface NeonGradientCardProps extends React.HTMLAttributes<HTMLDivElem...
FILE: packages/website/src/components/ui/particles.tsx
type MousePosition (line 5) | interface MousePosition {
function MousePosition (line 10) | function MousePosition(): MousePosition {
type ParticlesProps (line 31) | interface ParticlesProps extends ComponentPropsWithoutRef<'div'> {
function hexToRgb (line 43) | function hexToRgb(hex: string): number[] {
type Circle (line 60) | type Circle = {
FILE: packages/website/src/components/ui/separator.tsx
function Separator (line 6) | function Separator({
FILE: packages/website/src/components/ui/sparkles-text.tsx
type Sparkle (line 6) | interface Sparkle {
type SparklesTextProps (line 40) | interface SparklesTextProps {
FILE: packages/website/src/components/ui/spinner.tsx
function Spinner (line 5) | function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
FILE: packages/website/src/components/ui/switch.tsx
function Switch (line 6) | function Switch({ className, ...props }: React.ComponentProps<typeof Swi...
FILE: packages/website/src/components/ui/text-animate.tsx
type AnimationType (line 6) | type AnimationType = 'text' | 'word' | 'character' | 'line'
type AnimationVariant (line 7) | type AnimationVariant =
type TextAnimateProps (line 19) | interface TextAnimateProps extends MotionProps {
FILE: packages/website/src/components/ui/tooltip.tsx
function TooltipProvider (line 6) | function TooltipProvider({
function Tooltip (line 19) | function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimit...
function TooltipTrigger (line 27) | function TooltipTrigger({ ...props }: React.ComponentProps<typeof Toolti...
function TooltipContent (line 31) | function TooltipContent({
FILE: packages/website/src/components/ui/typing-animation.tsx
type TypingAnimationProps (line 6) | interface TypingAnimationProps extends MotionProps {
function TypingAnimation (line 23) | function TypingAnimation({
FILE: packages/website/src/constants.ts
constant CDN_DEMO_URL (line 2) | const CDN_DEMO_URL =
constant CDN_DEMO_CN_URL (line 4) | const CDN_DEMO_CN_URL =
constant DEMO_MODEL (line 8) | const DEMO_MODEL = 'qwen3.5-flash'
constant DEMO_BASE_URL (line 9) | const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fc...
FILE: packages/website/src/env.d.ts
type ImportMetaEnv (line 3) | interface ImportMetaEnv {
FILE: packages/website/src/hooks/useGitHubStars.ts
constant STATS_URL (line 3) | const STATS_URL = 'https://page-agent.github.io/gh-stats/stats.json'
function useGitHubStars (line 7) | function useGitHubStars() {
function formatStars (line 24) | function formatStars(n: number): string {
FILE: packages/website/src/i18n/context.tsx
type Lang (line 3) | type Lang = 'en-US' | 'zh-CN'
function LanguageProvider (line 11) | function LanguageProvider({ children }: { children: ReactNode }) {
function useLanguage (line 31) | function useLanguage() {
FILE: packages/website/src/lib/useDocumentTitle.ts
constant DEFAULT_TITLE (line 3) | const DEFAULT_TITLE = 'PageAgent - The GUI Agent Living in Your Webpage'
function useDocumentTitle (line 5) | function useDocumentTitle(title?: string) {
FILE: packages/website/src/lib/utils.ts
function cn (line 4) | function cn(...inputs: ClassValue[]) {
FILE: packages/website/src/pages/docs/Layout.tsx
type DocsLayoutProps (line 9) | interface DocsLayoutProps {
type NavItem (line 13) | interface NavItem {
type NavSection (line 18) | interface NavSection {
function DocsLayout (line 23) | function DocsLayout({ children }: DocsLayoutProps) {
FILE: packages/website/src/pages/docs/advanced/custom-ui/page.tsx
function CustomUIDocs (line 6) | function CustomUIDocs() {
FILE: packages/website/src/pages/docs/advanced/page-agent-core/page.tsx
function PageAgentCoreDocs (line 8) | function PageAgentCoreDocs() {
FILE: packages/website/src/pages/docs/advanced/page-agent/page.tsx
function PageAgentDocs (line 7) | function PageAgentDocs() {
FILE: packages/website/src/pages/docs/advanced/page-controller/page.tsx
function PageControllerDocs (line 8) | function PageControllerDocs() {
FILE: packages/website/src/pages/docs/advanced/security-permissions/page.tsx
function SecurityPermissions (line 5) | function SecurityPermissions() {
FILE: packages/website/src/pages/docs/features/chrome-extension/page.tsx
function ChromeExtension (line 7) | function ChromeExtension() {
FILE: packages/website/src/pages/docs/features/custom-instructions/page.tsx
function Instructions (line 5) | function Instructions() {
FILE: packages/website/src/pages/docs/features/custom-tools/page.tsx
function CustomTools (line 5) | function CustomTools() {
FILE: packages/website/src/pages/docs/features/data-masking/page.tsx
function DataMasking (line 5) | function DataMasking() {
FILE: packages/website/src/pages/docs/features/models/page.tsx
constant BASELINE (line 7) | const BASELINE = new Set([
constant MODEL_GROUPS (line 17) | const MODEL_GROUPS: Record<string, string[]> = {
function Models (line 55) | function Models() {
FILE: packages/website/src/pages/docs/features/third-party-agent/page.tsx
function ThirdPartyAgentPage (line 5) | function ThirdPartyAgentPage() {
FILE: packages/website/src/pages/docs/index.tsx
function DocsPage (line 24) | function DocsPage({ children }: { children: React.ReactNode }) {
function DocsRouter (line 32) | function DocsRouter() {
FILE: packages/website/src/pages/docs/introduction/limitations/page.tsx
function LimitationsPage (line 6) | function LimitationsPage() {
FILE: packages/website/src/pages/docs/introduction/overview/page.tsx
function Overview (line 4) | function Overview() {
FILE: packages/website/src/pages/docs/introduction/quick-start/page.tsx
function QuickStart (line 6) | function QuickStart() {
FILE: packages/website/src/pages/docs/introduction/troubleshooting/page.tsx
type TroubleshootingSection (line 12) | interface TroubleshootingSection {
constant SECTIONS (line 20) | const SECTIONS: TroubleshootingSection[] = [
function FormatErrorsContent (line 67) | function FormatErrorsContent(isZh: boolean) {
function LowSuccessRateContent (line 135) | function LowSuccessRateContent(isZh: boolean) {
function WrongElementContent (line 189) | function WrongElementContent(isZh: boolean) {
function ApiErrorsContent (line 256) | function ApiErrorsContent(isZh: boolean) {
constant SYMPTOM_COLORS (line 303) | const SYMPTOM_COLORS = {
function useActiveSection (line 316) | function useActiveSection(ids: string[]) {
function TroubleshootingPage (line 356) | function TroubleshootingPage() {
FILE: packages/website/src/pages/home/FeaturesSection.tsx
constant LLM_CLOUD (line 10) | const LLM_CLOUD: {
constant CARD_HEIGHT (line 30) | const CARD_HEIGHT = 'h-72'
function FeaturesSection (line 32) | function FeaturesSection() {
FILE: packages/website/src/pages/home/HeroSection.tsx
function getInjection (line 21) | function getInjection(useCN?: boolean) {
function HeroSection (line 41) | function HeroSection() {
FILE: packages/website/src/pages/home/OneMoreThingSection.tsx
function OneMoreThingSection (line 9) | function OneMoreThingSection() {
FILE: packages/website/src/pages/home/ScenariosSection.tsx
function ScenariosSection (line 7) | function ScenariosSection() {
FILE: packages/website/src/pages/home/index.tsx
function HomePage (line 11) | function HomePage() {
FILE: packages/website/src/router.tsx
function ScrollToTop (line 11) | function ScrollToTop() {
function Router (line 19) | function Router() {
FILE: packages/website/vite.config.js
constant SPA_ROUTES (line 19) | const SPA_ROUTES = [
constant SITE_URL (line 38) | const SITE_URL = 'https://alibaba.github.io/page-agent'
function spaRoutes (line 40) | function spaRoutes() {
FILE: scripts/sync-version.js
function isInternalPackage (line 56) | function isInternalPackage(name) {
function updateInternalDeps (line 64) | function updateInternalDeps(deps, newVersion) {
constant CDN_DEMO_URL_OLD (line 108) | const CDN_DEMO_URL_OLD = `https://cdn.jsdelivr.net/npm/page-agent@${oldV...
constant CDN_DEMO_URL_NEW (line 109) | const CDN_DEMO_URL_NEW = `https://cdn.jsdelivr.net/npm/page-agent@${newV...
constant CDN_DEMO_CN_URL_OLD (line 110) | const CDN_DEMO_CN_URL_OLD = `https://registry.npmmirror.com/page-agent/$...
constant CDN_DEMO_CN_URL_NEW (line 111) | const CDN_DEMO_CN_URL_NEW = `https://registry.npmmirror.com/page-agent/$...
Condensed preview — 227 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (884K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 1657,
"preview": "name: Bug Report\ndescription: Report a bug\ntitle: '[Bug] '\nlabels: ['bug']\nbody:\n - type: markdown\n attributes:\n "
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 733,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: Questions & Ideas / 问题与想法(Discussions)\n url: https://github.com/"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 1493,
"preview": "name: Feature Request\ndescription: Suggest a feature\ntitle: '[Feature] '\nlabels: ['enhancement']\nbody:\n - type: markdow"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 628,
"preview": "## What\n\nBrief description of changes.\n\n## Type\n\n- [ ] Bug fix\n- [ ] Feature / Improvement\n- [ ] Refactor\n- [ ] Document"
},
{
"path": ".github/dependabot.yml",
"chars": 668,
"preview": "version: 2\nupdates:\n - package-ecosystem: 'npm'\n directory: '/'\n schedule:\n interval: 'weekly'\n groups:\n "
},
{
"path": ".github/workflows/ci.yml",
"chars": 802,
"preview": "permissions:\n contents: read\nname: CI\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n t"
},
{
"path": ".github/workflows/deploy-demo.yml",
"chars": 768,
"preview": "name: Deploy Demo\n\non:\n push:\n branches: [main]\n\njobs:\n deploy:\n runs-on: ubuntu-latest\n permissions:\n c"
},
{
"path": ".github/workflows/release.yml",
"chars": 1041,
"preview": "name: Release\n\non:\n push:\n tags:\n - 'v*'\n\npermissions:\n id-token: write # Required for OIDC\n contents: read\n\n"
},
{
"path": ".gitignore",
"chars": 368,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\n# /"
},
{
"path": ".husky/commit-msg",
"chars": 33,
"preview": "npx --no -- commitlint --edit $1\n"
},
{
"path": ".husky/pre-commit",
"chars": 29,
"preview": "npx lint-staged --allow-empty"
},
{
"path": ".vscode/extensions.json",
"chars": 80,
"preview": "{\n \"recommendations\": [\"dbaeumer.vscode-eslint\", \"esbenp.prettier-vscode\"]\n}\n"
},
{
"path": ".vscode/settings.json",
"chars": 910,
"preview": "{\n \"cSpell.words\": [\n \"contenteditable\",\n \"deepseek\",\n \"historychange\",\n \"HITL\",\n "
},
{
"path": "AGENTS.md",
"chars": 5904,
"preview": "# Instructions for Coding Assistants\n\n## Project Overview\n\nThis is a **monorepo** with npm workspaces:\n\n- **Page Agent**"
},
{
"path": "CONTRIBUTING.md",
"chars": 6074,
"preview": "# Contributing to PageAgent\n\n♥️ We welcome contributions from everyone.\n\n## 🚀 Quick Start\n\n### Development Setup\n\n1. **P"
},
{
"path": "LICENSE",
"chars": 1119,
"preview": "MIT License\n\nCopyright (c) 2026 SimonLuvRamen\nCopyright (c) 2026 Alibaba Group Holding Limited\n\nPermission is hereby gra"
},
{
"path": "README.md",
"chars": 5595,
"preview": "# Page Agent\n\n<picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://img.alicdn.com/imgextra/i4/O1CN01"
},
{
"path": "SECURITY.md",
"chars": 1756,
"preview": "# Security Policy\n\n## Supported Versions\n\nWe provide security fixes on a best-effort basis for:\n\n| Version "
},
{
"path": "docs/CHANGELOG.md",
"chars": 9333,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
},
{
"path": "docs/CODE_OF_CONDUCT.md",
"chars": 4419,
"preview": "# Alibaba Open Source Code of Conduct\n\n[¶中文版](#我们的保证)\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming"
},
{
"path": "docs/README-zh.md",
"chars": 4618,
"preview": "# Page Agent\n\n<picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://img.alicdn.com/imgextra/i4/O1CN01"
},
{
"path": "docs/terms-and-privacy.md",
"chars": 6062,
"preview": "# Terms of Use & Privacy\n\n**Last updated:** March 2026\n\n\"We\" in this document refers to the maintainers of the open-sour"
},
{
"path": "eslint.config.js",
"chars": 2939,
"preview": "import js from '@eslint/js'\nimport reactDom from 'eslint-plugin-react-dom'\nimport reactHooks from 'eslint-plugin-react-h"
},
{
"path": "package.json",
"chars": 3949,
"preview": "{\n \"name\": \"root\",\n \"private\": true,\n \"version\": \"1.6.0\",\n \"type\": \"module\",\n \"workspaces\": [\n \"pa"
},
{
"path": "packages/core/package.json",
"chars": 1764,
"preview": "{\n \"name\": \"@page-agent/core\",\n \"private\": false,\n \"version\": \"1.6.0\",\n \"type\": \"module\",\n \"main\": \"./dis"
},
{
"path": "packages/core/src/PageAgentCore.ts",
"chars": 18343,
"preview": "/**\n * Copyright (C) 2025 Alibaba Group Holding Limited\n * Copyright (C) 2026 SimonLuvRamen\n * All rights reserved.\n */\n"
},
{
"path": "packages/core/src/env.d.ts",
"chars": 116,
"preview": "/// <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",
"chars": 16,
"preview": "system_prompt.md"
},
{
"path": "packages/core/src/prompts/system_prompt.md",
"chars": 9864,
"preview": "You are an AI agent designed to operate in an iterative loop to automate browser tasks. Your ultimate goal is accomplish"
},
{
"path": "packages/core/src/tools/index.ts",
"chars": 5007,
"preview": "/**\n * Internal tools for PageAgent.\n * @note Adapted from browser-use\n */\nimport * as z from 'zod/v4'\n\nimport type { Pa"
},
{
"path": "packages/core/src/types.ts",
"chars": 7480,
"preview": "import type { LLMConfig } from '@page-agent/llms'\n\n// @note circular dependency but okay\nimport type { PageAgentCore } f"
},
{
"path": "packages/core/src/utils/autoFixer.ts",
"chars": 6065,
"preview": "import { InvokeError, InvokeErrorType } from '@page-agent/llms'\nimport chalk from 'chalk'\nimport * as z from 'zod/v4'\n\ni"
},
{
"path": "packages/core/src/utils/index.ts",
"chars": 2700,
"preview": "import chalk from 'chalk'\n\nexport * from './autoFixer'\n\nexport async function waitFor(seconds: number): Promise<void> {\n"
},
{
"path": "packages/core/tsconfig.dts.json",
"chars": 213,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n // @workaround DTS bug\n // dts do not work w"
},
{
"path": "packages/core/tsconfig.json",
"chars": 638,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/ts"
},
{
"path": "packages/core/vite.config.js",
"chars": 1023,
"preview": "// @ts-check\nimport { dirname, resolve } from 'path'\nimport dts from 'unplugin-dts/vite'\nimport { fileURLToPath } from '"
},
{
"path": "packages/extension/.prettierignore",
"chars": 22,
"preview": ".wxt\nsrc/components/ui"
},
{
"path": "packages/extension/PRIVACY.md",
"chars": 234,
"preview": "# Privacy Policy for Page Agent Extension\n\nThis document has moved. Please see our full **[Terms of Use & Privacy](../.."
},
{
"path": "packages/extension/components.json",
"chars": 581,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": false,\n \"tsx\": true,\n \"t"
},
{
"path": "packages/extension/docs/extension_api.md",
"chars": 5299,
"preview": "# Page Agent Extension API\n\nIntegrate the Page Agent extension into your web app and trigger multi-page browser tasks fr"
},
{
"path": "packages/extension/package.json",
"chars": 1497,
"preview": "{\n \"name\": \"@page-agent/ext\",\n \"private\": true,\n \"version\": \"1.6.0\",\n \"type\": \"module\",\n \"scripts\": {\n "
},
{
"path": "packages/extension/public/_locales/en/messages.json",
"chars": 229,
"preview": "{\n\t\"extName\": {\n\t\t\"message\": \"Page Agent Ext\"\n\t},\n\t\"extDescription\": {\n\t\t\"message\": \"AI-powered browser automation assis"
},
{
"path": "packages/extension/public/_locales/zh_CN/messages.json",
"chars": 171,
"preview": "{\n\t\"extName\": {\n\t\t\"message\": \"Page Agent Ext\"\n\t},\n\t\"extDescription\": {\n\t\t\"message\": \"AI 驱动的浏览器自动化助手,用自然语言控制网页。\"\n\t},\n\t\"ex"
},
{
"path": "packages/extension/src/agent/.prettierignore",
"chars": 16,
"preview": "system_prompt.md"
},
{
"path": "packages/extension/src/agent/MultiPageAgent.ts",
"chars": 2880,
"preview": "import { type AgentConfig, PageAgentCore } from '@page-agent/core'\n\nimport { RemotePageController } from './RemotePageCo"
},
{
"path": "packages/extension/src/agent/RemotePageController.background.ts",
"chars": 1099,
"preview": "/**\n * background logics for RemotePageController\n * - redirect messages from RemotePageController(Agent, extension page"
},
{
"path": "packages/extension/src/agent/RemotePageController.content.ts",
"chars": 3463,
"preview": "/**\n * content script for RemotePageController\n */\nimport { PageController } from '@page-agent/page-controller'\n\nexport "
},
{
"path": "packages/extension/src/agent/RemotePageController.ts",
"chars": 5166,
"preview": "import type { BrowserState } from '@page-agent/page-controller'\n\nimport type { TabsController } from './TabsController'\n"
},
{
"path": "packages/extension/src/agent/TabsController.background.ts",
"chars": 4248,
"preview": "/**\n * background logics for TabsController\n */\nimport type { TabAction } from './TabsController'\n\nconst PREFIX = '[Tabs"
},
{
"path": "packages/extension/src/agent/TabsController.ts",
"chars": 8928,
"preview": "import { isContentScriptAllowed } from './RemotePageController'\n\nconst PREFIX = '[TabsController]'\n\nfunction debug(...me"
},
{
"path": "packages/extension/src/agent/constants.ts",
"chars": 994,
"preview": "import type { LLMConfig } from '@page-agent/llms'\n\n// Demo LLM for testing\nexport const DEMO_MODEL = 'qwen3.5-plus'\nexpo"
},
{
"path": "packages/extension/src/agent/system_prompt.md",
"chars": 8998,
"preview": "You are an AI agent designed to operate in an iterative loop to automate browser tasks. Your ultimate goal is accomplish"
},
{
"path": "packages/extension/src/agent/tabTools.ts",
"chars": 2421,
"preview": "/**\n * Tab control tools for browser extension\n *\n * These tools allow the agent to manage multiple browser tabs:\n * - o"
},
{
"path": "packages/extension/src/agent/useAgent.ts",
"chars": 4402,
"preview": "/**\n * React hook for using AgentController\n */\nimport type {\n\tAgentActivity,\n\tAgentStatus,\n\tExecutionResult,\n\tHistorica"
},
{
"path": "packages/extension/src/assets/index.css",
"chars": 4565,
"preview": "@import 'tailwindcss';\n@import 'tw-animate-css';\n\n@custom-variant dark (&:is(.dark *));\n\n:root {\n\t--background: oklch(1 "
},
{
"path": "packages/extension/src/components/ConfigPanel.tsx",
"chars": 11400,
"preview": "import {\n\tCopy,\n\tCornerUpLeft,\n\tExternalLink,\n\tEye,\n\tEyeOff,\n\tFoldVertical,\n\tHatGlasses,\n\tHome,\n\tLoader2,\n\tScale,\n\tUnfol"
},
{
"path": "packages/extension/src/components/ErrorBoundary.tsx",
"chars": 1652,
"preview": "import { AlertTriangle, Eraser, RotateCcw } from 'lucide-react'\nimport { Component, type ErrorInfo, type ReactNode } fro"
},
{
"path": "packages/extension/src/components/HistoryDetail.tsx",
"chars": 2358,
"preview": "import { ArrowLeft, RotateCcw, Trash2 } from 'lucide-react'\nimport { useEffect, useState } from 'react'\n\nimport { Button"
},
{
"path": "packages/extension/src/components/HistoryList.tsx",
"chars": 4914,
"preview": "import { ArrowDownToLine, ArrowLeft, CheckCircle, RotateCcw, Trash2, XCircle } from 'lucide-react'\nimport { useCallback,"
},
{
"path": "packages/extension/src/components/cards.tsx",
"chars": 11335,
"preview": "import type {\n\tAgentActivity,\n\tAgentErrorEvent,\n\tAgentStepEvent,\n\tHistoricalEvent,\n\tObservationEvent,\n\tRetryEvent,\n} fro"
},
{
"path": "packages/extension/src/components/misc.tsx",
"chars": 4512,
"preview": "import type { AgentStatus } from '@page-agent/core'\nimport { Motion } from 'ai-motion'\nimport { BookOpen, Globe } from '"
},
{
"path": "packages/extension/src/components/ui/button.tsx",
"chars": 2080,
"preview": "import { Slot } from '@radix-ui/react-slot'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport * a"
},
{
"path": "packages/extension/src/components/ui/card.tsx",
"chars": 1803,
"preview": "import * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Card({ className, ...props }: React.Component"
},
{
"path": "packages/extension/src/components/ui/field.tsx",
"chars": 5598,
"preview": "import { type VariantProps, cva } from 'class-variance-authority'\nimport { useMemo } from 'react'\n\nimport { Label } from"
},
{
"path": "packages/extension/src/components/ui/hover-card.tsx",
"chars": 1452,
"preview": "import * as HoverCardPrimitive from '@radix-ui/react-hover-card'\nimport * as React from 'react'\n\nimport { cn } from '@/l"
},
{
"path": "packages/extension/src/components/ui/input-group.tsx",
"chars": 4779,
"preview": "import { type VariantProps, cva } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport { Button } from"
},
{
"path": "packages/extension/src/components/ui/input.tsx",
"chars": 925,
"preview": "import * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Input({ className, type, ...props }: React.Co"
},
{
"path": "packages/extension/src/components/ui/item.tsx",
"chars": 4091,
"preview": "import { Slot } from '@radix-ui/react-slot'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport * a"
},
{
"path": "packages/extension/src/components/ui/label.tsx",
"chars": 567,
"preview": "import * as LabelPrimitive from '@radix-ui/react-label'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/utils'"
},
{
"path": "packages/extension/src/components/ui/separator.tsx",
"chars": 649,
"preview": "import * as SeparatorPrimitive from '@radix-ui/react-separator'\nimport * as React from 'react'\n\nimport { cn } from '@/li"
},
{
"path": "packages/extension/src/components/ui/sonner.tsx",
"chars": 939,
"preview": "import {\n\tCircleCheckIcon,\n\tInfoIcon,\n\tLoader2Icon,\n\tOctagonXIcon,\n\tTriangleAlertIcon,\n} from 'lucide-react'\nimport { us"
},
{
"path": "packages/extension/src/components/ui/spinner.tsx",
"chars": 313,
"preview": "import { Loader2Icon } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Spinner({ className, ...props }: "
},
{
"path": "packages/extension/src/components/ui/switch.tsx",
"chars": 1123,
"preview": "import * as SwitchPrimitive from '@radix-ui/react-switch'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/util"
},
{
"path": "packages/extension/src/components/ui/textarea.tsx",
"chars": 733,
"preview": "import * as React from 'react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Textarea({ className, ...props }: React.Compo"
},
{
"path": "packages/extension/src/components/ui/typing-animation.tsx",
"chars": 4005,
"preview": "import { MotionProps, motion, useInView } from 'motion/react'\nimport { useEffect, useMemo, useRef, useState } from 'reac"
},
{
"path": "packages/extension/src/entrypoints/background.ts",
"chars": 1883,
"preview": "import { handlePageControlMessage } from '@/agent/RemotePageController.background'\nimport { handleTabControlMessage, set"
},
{
"path": "packages/extension/src/entrypoints/content.ts",
"chars": 3721,
"preview": "import { initPageController } from '@/agent/RemotePageController.content'\n\n// import { DEMO_CONFIG } from '@/agent/const"
},
{
"path": "packages/extension/src/entrypoints/hub/App.tsx",
"chars": 8329,
"preview": "import { FoldVertical, Plug, PlugZap, Square, UnfoldVertical, Unplug } from 'lucide-react'\nimport { useEffect, useRef, u"
},
{
"path": "packages/extension/src/entrypoints/hub/hub-ws.ts",
"chars": 5499,
"preview": "/**\n * Hub WebSocket Protocol\n *\n * Hub connects as WS client to `ws://localhost:{port}`.\n * All messages are JSON. One "
},
{
"path": "packages/extension/src/entrypoints/hub/index.html",
"chars": 346,
"preview": "<!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-s"
},
{
"path": "packages/extension/src/entrypoints/hub/main.tsx",
"chars": 575,
"preview": "import React from 'react'\nimport ReactDOM from 'react-dom/client'\n\nimport { ErrorBoundary } from '@/components/ErrorBoun"
},
{
"path": "packages/extension/src/entrypoints/main-world.ts",
"chars": 2772,
"preview": "import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'\n\nexport type Execut"
},
{
"path": "packages/extension/src/entrypoints/sidepanel/App.tsx",
"chars": 6143,
"preview": "import { History, Send, Settings, Square } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from "
},
{
"path": "packages/extension/src/entrypoints/sidepanel/index.html",
"chars": 342,
"preview": "<!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-s"
},
{
"path": "packages/extension/src/entrypoints/sidepanel/main.tsx",
"chars": 616,
"preview": "import React from 'react'\nimport ReactDOM from 'react-dom/client'\n\nimport { ErrorBoundary } from '@/components/ErrorBoun"
},
{
"path": "packages/extension/src/lib/db.ts",
"chars": 1668,
"preview": "import type { HistoricalEvent } from '@page-agent/core'\nimport { type DBSchema, type IDBPDatabase, openDB } from 'idb'\n\n"
},
{
"path": "packages/extension/src/lib/history-export.ts",
"chars": 1716,
"preview": "import type { HistoricalEvent } from '@page-agent/core'\n\nconst EXPORT_FILE_PREFIX = 'page-agent-history'\nconst MAX_TASK_"
},
{
"path": "packages/extension/src/lib/utils.ts",
"chars": 165,
"preview": "import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: Cla"
},
{
"path": "packages/extension/src/types/assets.d.ts",
"chars": 363,
"preview": "// Asset type declarations\ndeclare module '*.webp' {\n\tconst src: string\n\texport default src\n}\n\ndeclare module '*.png' {\n"
},
{
"path": "packages/extension/src/types/globals.d.ts",
"chars": 34,
"preview": "declare const __VERSION__: string\n"
},
{
"path": "packages/extension/src/types/markdown.d.ts",
"chars": 77,
"preview": "declare module '*.md?raw' {\n\tconst content: string\n\texport default content\n}\n"
},
{
"path": "packages/extension/tsconfig.json",
"chars": 863,
"preview": "{\n \"extends\": \"./.wxt/tsconfig.json\",\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconf"
},
{
"path": "packages/extension/wxt.config.js",
"chars": 2005,
"preview": "import tailwindcss from '@tailwindcss/vite'\nimport { mkdirSync, readFileSync } from 'node:fs'\nimport { defineConfig } fr"
},
{
"path": "packages/llms/package.json",
"chars": 1382,
"preview": "{\n \"name\": \"@page-agent/llms\",\n \"version\": \"1.6.0\",\n \"type\": \"module\",\n \"main\": \"./dist/lib/page-agent-llms."
},
{
"path": "packages/llms/src/OpenAIClient.ts",
"chars": 6024,
"preview": "/**\n * OpenAI Client implementation\n */\nimport * as z from 'zod/v4'\n\nimport { InvokeError, InvokeErrorType } from './err"
},
{
"path": "packages/llms/src/constants.ts",
"chars": 135,
"preview": "// Internal constants\nexport const LLM_MAX_RETRIES = 2\nexport const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps"
},
{
"path": "packages/llms/src/errors.ts",
"chars": 1849,
"preview": "/**\n * Error types and error handling for LLM invocations\n */\n\nexport const InvokeErrorType = {\n\t// Retryable\n\tNETWORK_E"
},
{
"path": "packages/llms/src/index.ts",
"chars": 3127,
"preview": "import { OpenAIClient } from './OpenAIClient'\nimport { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'\nimport "
},
{
"path": "packages/llms/src/types.ts",
"chars": 2618,
"preview": "/**\n * Core types for LLM integration\n */\nimport type * as z from 'zod/v4'\n\n/**\n * Message format - OpenAI standard (ind"
},
{
"path": "packages/llms/src/utils.ts",
"chars": 4165,
"preview": "/**\n * Utility functions for LLM integration\n */\nimport chalk from 'chalk'\nimport * as z from 'zod/v4'\n\nimport type { To"
},
{
"path": "packages/llms/tsconfig.dts.json",
"chars": 213,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n // @workaround DTS bug\n // dts do not work w"
},
{
"path": "packages/llms/tsconfig.json",
"chars": 337,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/ts"
},
{
"path": "packages/llms/vite.config.js",
"chars": 843,
"preview": "// @ts-check\nimport chalk from 'chalk'\nimport { dirname, resolve } from 'path'\nimport dts from 'unplugin-dts/vite'\nimpor"
},
{
"path": "packages/mcp/README.md",
"chars": 3593,
"preview": "# @page-agent/mcp\n\nMCP server that lets AI agent clients (Claude Desktop, Copilot, etc.) control your browser through th"
},
{
"path": "packages/mcp/package.json",
"chars": 839,
"preview": "{\n \"name\": \"@page-agent/mcp\",\n \"private\": false,\n \"version\": \"1.6.0\",\n \"type\": \"module\",\n \"bin\": {\n "
},
{
"path": "packages/mcp/src/hub-bridge.js",
"chars": 3798,
"preview": "#!/usr/bin/env node\nimport { readFileSync } from 'node:fs'\nimport http from 'node:http'\nimport { fileURLToPath } from 'n"
},
{
"path": "packages/mcp/src/index.js",
"chars": 2594,
"preview": "#!/usr/bin/env node\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StdioServerTransport } "
},
{
"path": "packages/mcp/src/launcher.html",
"chars": 5563,
"preview": "<!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-s"
},
{
"path": "packages/page-agent/package.json",
"chars": 1889,
"preview": "{\n \"name\": \"page-agent\",\n \"private\": false,\n \"version\": \"1.6.0\",\n \"type\": \"module\",\n \"main\": \"./dist/esm/"
},
{
"path": "packages/page-agent/src/PageAgent.ts",
"chars": 702,
"preview": "/**\n * Copyright (C) 2025 Alibaba Group Holding Limited\n * All rights reserved.\n */\nimport { type AgentConfig, PageAgent"
},
{
"path": "packages/page-agent/src/demo.ts",
"chars": 1702,
"preview": "/**\n * IIFE demo entry - auto-initializes with built-in demo API for testing\n */\nimport { PageAgent, type PageAgentConfi"
},
{
"path": "packages/page-agent/src/env.d.ts",
"chars": 180,
"preview": "/// <reference types=\"vite/client\" />\nimport type { PageAgent } from './PageAgent'\n\ndeclare global {\n\tinterface Window {"
},
{
"path": "packages/page-agent/tsconfig.dts.json",
"chars": 213,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n // @workaround DTS bug\n // dts do not work w"
},
{
"path": "packages/page-agent/tsconfig.json",
"chars": 818,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/ts"
},
{
"path": "packages/page-agent/vite.config.js",
"chars": 1010,
"preview": "// @ts-check\nimport { dirname, resolve } from 'path'\nimport dts from 'unplugin-dts/vite'\nimport { fileURLToPath } from '"
},
{
"path": "packages/page-agent/vite.iife.config.js",
"chars": 1847,
"preview": "// @ts-check\nimport { config as dotenvConfig } from 'dotenv'\nimport { dirname, resolve } from 'path'\nimport { fileURLToP"
},
{
"path": "packages/page-controller/package.json",
"chars": 1303,
"preview": "{\n \"name\": \"@page-agent/page-controller\",\n \"version\": \"1.6.0\",\n \"type\": \"module\",\n \"main\": \"./dist/lib/page-"
},
{
"path": "packages/page-controller/src/PageController.ts",
"chars": 11061,
"preview": "/**\n * Copyright (C) 2025 Alibaba Group Holding Limited\n * All rights reserved.\n *\n * PageController - Manages DOM opera"
},
{
"path": "packages/page-controller/src/actions.ts",
"chars": 15659,
"preview": "/**\n * Copyright (C) 2025 Alibaba Group Holding Limited\n * All rights reserved.\n */\nimport type { InteractiveElementDomN"
},
{
"path": "packages/page-controller/src/dom/dom_tree/index.js",
"chars": 51810,
"preview": "/**\n * @file port from browser-use\n * @see https://github.com/browser-use/browser-use/commits/main/browser_use/dom/dom_t"
},
{
"path": "packages/page-controller/src/dom/dom_tree/type.ts",
"chars": 1039,
"preview": "// FlatDomTree: 扁平化 DOM 树结构,适用于高效存储和遍历页面结构。\n// 每个节点通过 map 索引,支持文本节点和元素节点,字段区分 undefined 和 false。\n\nexport interface FlatD"
},
{
"path": "packages/page-controller/src/dom/getPageInfo.ts",
"chars": 1264,
"preview": "export function getPageInfo() {\n\tconst viewport_width = window.innerWidth\n\tconst viewport_height = window.innerHeight\n\n\t"
},
{
"path": "packages/page-controller/src/dom/index.ts",
"chars": 13595,
"preview": "import domTree from './dom_tree/index.js'\nimport {\n\tElementDomNode,\n\tFlatDomTree,\n\tInteractiveElementDomNode,\n\tTextDomNo"
},
{
"path": "packages/page-controller/src/env.d.ts",
"chars": 136,
"preview": "/// <reference types=\"vite/client\" />\n\ndeclare module '*.module.css' {\n\tconst classes: Record<string, string>\n\texport de"
},
{
"path": "packages/page-controller/src/mask/SimulatorMask.module.css",
"chars": 179,
"preview": ".wrapper {\n\tposition: fixed;\n\tinset: 0;\n\tz-index: 2147483641; /* 确保在所有元素之上,除了 panel */\n\tcursor: wait;\n\toverflow: hidden;"
},
{
"path": "packages/page-controller/src/mask/SimulatorMask.ts",
"chars": 4750,
"preview": "import { Motion } from 'ai-motion'\n\nimport { isPageDark } from './checkDarkMode'\n\nimport styles from './SimulatorMask.mo"
},
{
"path": "packages/page-controller/src/mask/checkDarkMode.ts",
"chars": 3668,
"preview": "/**\n * Checks for common dark mode CSS classes on the html or body elements.\n * @returns {boolean} - True if a common da"
},
{
"path": "packages/page-controller/src/mask/cursor.module.css",
"chars": 1265,
"preview": "/* AI 光标样式 */\n.cursor {\n\tposition: absolute;\n\twidth: var(--cursor-size, 75px);\n\theight: var(--cursor-size, 75px);\n\tpoint"
},
{
"path": "packages/page-controller/src/patches/antd.ts",
"chars": 597,
"preview": "import type { PageController } from '../PageController'\n\nconst clearFunctions = [] as (() => void)[]\n\n/**\n * antd 的 sele"
},
{
"path": "packages/page-controller/src/patches/react.ts",
"chars": 760,
"preview": "import type { PageController } from '../PageController'\n\n// Find common React root elements and add data-page-agent-not-"
},
{
"path": "packages/page-controller/src/utils/index.ts",
"chars": 2119,
"preview": "// ======= type guards =======\n// @note instanceof fails for elements inside iframes\n\nexport function isHTMLElement(el: "
},
{
"path": "packages/page-controller/tsconfig.dts.json",
"chars": 213,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n // @workaround DTS bug\n // dts do not work w"
},
{
"path": "packages/page-controller/tsconfig.json",
"chars": 348,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/ts"
},
{
"path": "packages/page-controller/vite.config.js",
"chars": 1128,
"preview": "// @ts-check\nimport chalk from 'chalk'\nimport { dirname, resolve } from 'path'\nimport dts from 'unplugin-dts/vite'\nimpor"
},
{
"path": "packages/ui/package.json",
"chars": 1141,
"preview": "{\n \"name\": \"@page-agent/ui\",\n \"version\": \"1.6.0\",\n \"type\": \"module\",\n \"main\": \"./dist/lib/page-agent-ui.js\","
},
{
"path": "packages/ui/src/env.d.ts",
"chars": 136,
"preview": "/// <reference types=\"vite/client\" />\n\ndeclare module '*.module.css' {\n\tconst classes: Record<string, string>\n\texport de"
},
{
"path": "packages/ui/src/i18n/index.ts",
"chars": 1334,
"preview": "import {\n\ttype SupportedLanguage,\n\ttype TranslationKey,\n\ttype TranslationParams,\n\ttype TranslationSchema,\n\tlocales,\n} fr"
},
{
"path": "packages/ui/src/i18n/locales.ts",
"chars": 3754,
"preview": "// English translations (base/reference language)\nconst enUS = {\n\tui: {\n\t\tpanel: {\n\t\t\tready: 'Ready',\n\t\t\tthinking: 'Thin"
},
{
"path": "packages/ui/src/index.ts",
"chars": 131,
"preview": "export { Panel, type PanelConfig } from './panel/Panel'\nexport { I18n, type SupportedLanguage, type TranslationKey } fro"
},
{
"path": "packages/ui/src/motion-css/createMotion.ts",
"chars": 1787,
"preview": "import styles from './motion.module.css'\n\nexport function createMotion() {\n\tconst wrapper = document.createElement('div'"
},
{
"path": "packages/ui/src/motion-css/motion.module.css",
"chars": 6829,
"preview": ".wrapper {\n\tposition: absolute;\n\tinset: 0;\n\tpointer-events: none;\n\n\ttransform-origin: center;\n\n\t--color-1: rgb(57, 182, "
},
{
"path": "packages/ui/src/motion-css/readme",
"chars": 162,
"preview": "This is the CSS implementation of ai-motion.\n\nEasy to use but Terrible performance. Causing full screen glitching in som"
},
{
"path": "packages/ui/src/panel/Panel.module.css",
"chars": 11662,
"preview": ".wrapper {\n\tposition: fixed;\n\tbottom: 100px;\n\tleft: 50%;\n\ttransform: translateX(-50%) translateY(20px);\n\topacity: 0;\n\tz-"
},
{
"path": "packages/ui/src/panel/Panel.ts",
"chars": 18563,
"preview": "import { I18n, type SupportedLanguage } from '../i18n'\nimport { truncate } from '../utils'\nimport { createCard, createRe"
},
{
"path": "packages/ui/src/panel/cards.ts",
"chars": 1432,
"preview": "/**\n * Card HTML generation utilities for Panel\n */\nimport { escapeHtml } from '../utils'\n\nimport styles from './Panel.m"
},
{
"path": "packages/ui/src/panel/types.ts",
"chars": 2354,
"preview": "/**\n * Agent activity - transient state for immediate UI feedback.\n *\n * Unlike historical events (which are persisted),"
},
{
"path": "packages/ui/src/utils.ts",
"chars": 439,
"preview": "export function truncate(text: string, maxLength: number): string {\n\tif (text.length > maxLength) {\n\t\treturn text.substr"
},
{
"path": "packages/ui/tsconfig.dts.json",
"chars": 213,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n // @workaround DTS bug\n // dts do not work w"
},
{
"path": "packages/ui/tsconfig.json",
"chars": 348,
"preview": "{\n \"extends\": \"../../tsconfig.base.json\",\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/ts"
},
{
"path": "packages/ui/vite.config.js",
"chars": 965,
"preview": "// @ts-check\nimport chalk from 'chalk'\nimport { dirname, resolve } from 'path'\nimport dts from 'unplugin-dts/vite'\nimpor"
},
{
"path": "packages/website/AGENTS.md",
"chars": 3731,
"preview": "# Website Package - Instructions for Coding Assistants\n\n## Tech Stack\n\n- **React** with TypeScript\n- **Vite** for dev se"
},
{
"path": "packages/website/components.json",
"chars": 570,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": false,\n \"tsx\": true,\n \"t"
},
{
"path": "packages/website/index.html",
"chars": 2712,
"preview": "<!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\t"
},
{
"path": "packages/website/package.json",
"chars": 1069,
"preview": "{\n \"name\": \"@page-agent/website\",\n \"private\": true,\n \"version\": \"1.6.0\",\n \"type\": \"module\",\n \"scripts\": {"
},
{
"path": "packages/website/public/robots.txt",
"chars": 82,
"preview": "User-agent: *\nAllow: /\n\nSitemap: https://alibaba.github.io/page-agent/sitemap.xml\n"
},
{
"path": "packages/website/src/components/APIReference.tsx",
"chars": 5694,
"preview": "/**\n * API Reference component for displaying TypeScript interface definitions\n *\n * Provides a beautiful, readable tabl"
},
{
"path": "packages/website/src/components/BetaNotice.tsx",
"chars": 855,
"preview": "import { useLanguage } from '@/i18n/context'\n\nexport default function BetaNotice() {\n\tconst { isZh } = useLanguage()\n\n\tr"
},
{
"path": "packages/website/src/components/CodeEditor.tsx",
"chars": 4058,
"preview": "/**\n * 代码编辑器组件,模拟现代代码编辑器的外观\n */\nimport React from 'react'\n\nimport HighlightSyntax from './HighlightSyntax'\n\ninterface Co"
},
{
"path": "packages/website/src/components/Footer.tsx",
"chars": 2492,
"preview": "import { siGithub, siX } from 'simple-icons'\n\nimport { useLanguage } from '@/i18n/context'\n\nexport default function Foot"
},
{
"path": "packages/website/src/components/Header.tsx",
"chars": 6669,
"preview": "import { BookOpen, Menu, X } from 'lucide-react'\nimport { useState } from 'react'\nimport { siGithub } from 'simple-icons"
},
{
"path": "packages/website/src/components/Heading.tsx",
"chars": 1157,
"preview": "import { ComponentPropsWithoutRef, useEffect, useRef } from 'react'\n\nimport { cn } from '@/lib/utils'\n\ntype Level = 2 | "
},
{
"path": "packages/website/src/components/HighlightSyntax.module.css",
"chars": 1547,
"preview": ".syntax {\n\twhite-space: pre-wrap;\n\tword-break: break-word;\n\toverflow-wrap: break-word;\n\tfont-family: monospace;\n\tfont-si"
},
{
"path": "packages/website/src/components/HighlightSyntax.tsx",
"chars": 4820,
"preview": "/**\n * js 语法高亮组件,适合在文章中演示代码片段\n */\nimport React from 'react'\n\nimport styles from './HighlightSyntax.module.css'\n\ninterfac"
},
{
"path": "packages/website/src/components/JSConsole.module.css",
"chars": 2338,
"preview": ".console {\n\tdisplay: flex;\n\tflex-direction: column;\n\tbackground-color: #ffffff;\n\tborder: 1px solid #e0e0e0;\n\tborder-radi"
},
{
"path": "packages/website/src/components/JSConsole.tsx",
"chars": 9014,
"preview": "/**\n * JS 调试台,适合在文档中直接让用户运行代码,并且实时查看运行结果\n */\n/* eslint-disable @typescript-eslint/no-base-to-string */\nimport { Keyboard"
},
{
"path": "packages/website/src/components/LanguageSwitcher.tsx",
"chars": 3402,
"preview": "import { useEffect, useRef, useState } from 'react'\n\nimport { useLanguage } from '@/i18n/context'\n\nexport default functi"
},
{
"path": "packages/website/src/components/ThemeSwitcher.tsx",
"chars": 4661,
"preview": "import { useEffect, useState } from 'react'\n\ntype Theme = 'light' | 'dark'\n\nexport default function ThemeSwitcher() {\n\tc"
},
{
"path": "packages/website/src/components/ui/alert.tsx",
"chars": 1491,
"preview": "import { type VariantProps, cva } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport { cn } from '@/"
},
{
"path": "packages/website/src/components/ui/animated-gradient-text.tsx",
"chars": 819,
"preview": "import { ComponentPropsWithoutRef } from 'react'\n\nimport { cn } from '@/lib/utils'\n\nexport interface AnimatedGradientTex"
},
{
"path": "packages/website/src/components/ui/animated-shiny-text.tsx",
"chars": 927,
"preview": "import { CSSProperties, ComponentPropsWithoutRef, FC } from 'react'\n\nimport { cn } from '@/lib/utils'\n\nexport interface "
},
{
"path": "packages/website/src/components/ui/aurora-text.tsx",
"chars": 903,
"preview": "import React, { memo } from 'react'\n\ninterface AuroraTextProps {\n\tchildren: React.ReactNode\n\tclassName?: string\n\tcolors?"
},
{
"path": "packages/website/src/components/ui/badge.tsx",
"chars": 1527,
"preview": "import { Slot } from '@radix-ui/react-slot'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport * a"
},
{
"path": "packages/website/src/components/ui/bento-grid.tsx",
"chars": 2878,
"preview": "import { ArrowRightIcon } from '@radix-ui/react-icons'\nimport { ComponentPropsWithoutRef, ReactNode } from 'react'\n\nimpo"
},
{
"path": "packages/website/src/components/ui/blur-fade.tsx",
"chars": 1643,
"preview": "import {\n\tAnimatePresence,\n\tMotionProps,\n\tUseInViewOptions,\n\tVariants,\n\tmotion,\n\tuseInView,\n} from 'motion/react'\nimport"
},
{
"path": "packages/website/src/components/ui/button.tsx",
"chars": 2080,
"preview": "import { Slot } from '@radix-ui/react-slot'\nimport { type VariantProps, cva } from 'class-variance-authority'\nimport * a"
},
{
"path": "packages/website/src/components/ui/highlighter.tsx",
"chars": 1986,
"preview": "import { useInView } from 'motion/react'\nimport { useEffect, useRef } from 'react'\nimport type React from 'react'\nimport"
},
{
"path": "packages/website/src/components/ui/hyper-text.tsx",
"chars": 3672,
"preview": "import { AnimatePresence, MotionProps, motion } from 'motion/react'\nimport { useEffect, useRef, useState } from 'react'\n"
},
{
"path": "packages/website/src/components/ui/kbd.tsx",
"chars": 813,
"preview": "import { cn } from '@/lib/utils'\n\nfunction Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {\n\treturn (\n\t\t<kbd\n"
},
{
"path": "packages/website/src/components/ui/magic-card.tsx",
"chars": 2762,
"preview": "import { motion, useMotionTemplate, useMotionValue } from 'motion/react'\nimport React, { useCallback, useEffect } from '"
},
{
"path": "packages/website/src/components/ui/marquee.tsx",
"chars": 1503,
"preview": "import { ComponentPropsWithoutRef } from 'react'\n\nimport { cn } from '@/lib/utils'\n\ninterface MarqueeProps extends Compo"
},
{
"path": "packages/website/src/components/ui/neon-gradient-card.tsx",
"chars": 3953,
"preview": "import { CSSProperties, ReactElement, ReactNode, useEffect, useRef, useState } from 'react'\n\nimport { cn } from '@/lib/u"
},
{
"path": "packages/website/src/components/ui/particles.tsx",
"chars": 7558,
"preview": "import React, { ComponentPropsWithoutRef, useEffect, useRef, useState } from 'react'\n\nimport { cn } from '@/lib/utils'\n\n"
},
{
"path": "packages/website/src/components/ui/separator.tsx",
"chars": 649,
"preview": "import * as SeparatorPrimitive from '@radix-ui/react-separator'\nimport * as React from 'react'\n\nimport { cn } from '@/li"
},
{
"path": "packages/website/src/components/ui/sonner.tsx",
"chars": 939,
"preview": "import {\n\tCircleCheckIcon,\n\tInfoIcon,\n\tLoader2Icon,\n\tOctagonXIcon,\n\tTriangleAlertIcon,\n} from 'lucide-react'\nimport { us"
},
{
"path": "packages/website/src/components/ui/sparkles-text.tsx",
"chars": 3946,
"preview": "import { motion } from 'motion/react'\nimport { CSSProperties, ReactElement, useEffect, useState } from 'react'\n\nimport {"
},
{
"path": "packages/website/src/components/ui/spinner.tsx",
"chars": 313,
"preview": "import { Loader2Icon } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'\n\nfunction Spinner({ className, ...props }: "
},
{
"path": "packages/website/src/components/ui/switch.tsx",
"chars": 1108,
"preview": "import * as SwitchPrimitive from '@radix-ui/react-switch'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/util"
},
{
"path": "packages/website/src/components/ui/text-animate.tsx",
"chars": 7872,
"preview": "import { AnimatePresence, MotionProps, Variants, motion } from 'motion/react'\nimport { ElementType, memo } from 'react'\n"
},
{
"path": "packages/website/src/components/ui/tooltip.tsx",
"chars": 1790,
"preview": "import * as TooltipPrimitive from '@radix-ui/react-tooltip'\nimport * as React from 'react'\n\nimport { cn } from '@/lib/ut"
},
{
"path": "packages/website/src/components/ui/typing-animation.tsx",
"chars": 4005,
"preview": "import { MotionProps, motion, useInView } from 'motion/react'\nimport { useEffect, useMemo, useRef, useState } from 'reac"
},
{
"path": "packages/website/src/constants.ts",
"chars": 513,
"preview": "// Demo build (auto-init with demo LLM, for quick testing)\nexport const CDN_DEMO_URL =\n\t'https://cdn.jsdelivr.net/npm/pa"
},
{
"path": "packages/website/src/env.d.ts",
"chars": 191,
"preview": "/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n\treadonly VERSION: string\n}\n\ndeclare module '*.module.c"
},
{
"path": "packages/website/src/hooks/useGitHubStars.ts",
"chars": 600,
"preview": "import { useEffect, useState } from 'react'\n\nconst STATS_URL = 'https://page-agent.github.io/gh-stats/stats.json'\n\nlet c"
},
{
"path": "packages/website/src/i18n/context.tsx",
"chars": 992,
"preview": "import { ReactNode, createContext, use, useState } from 'react'\n\ntype Lang = 'en-US' | 'zh-CN'\n\nconst LanguageContext = "
},
{
"path": "packages/website/src/index.css",
"chars": 10779,
"preview": "@config '../tailwind.config.js';\n@import 'tailwindcss';\n@import 'tw-animate-css';\n\n@custom-variant dark (&:is(.dark *));"
},
{
"path": "packages/website/src/lib/useDocumentTitle.ts",
"chars": 260,
"preview": "import { useEffect } from 'react'\n\nconst DEFAULT_TITLE = 'PageAgent - The GUI Agent Living in Your Webpage'\n\nexport func"
},
{
"path": "packages/website/src/lib/utils.ts",
"chars": 165,
"preview": "import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: Cla"
},
{
"path": "packages/website/src/main.tsx",
"chars": 600,
"preview": "import { createRoot } from 'react-dom/client'\nimport { Router } from 'wouter'\n\nimport { LanguageProvider } from './i18n/"
},
{
"path": "packages/website/src/pages/docs/Layout.tsx",
"chars": 4535,
"preview": "import { ReactNode } from 'react'\nimport { siGooglechrome } from 'simple-icons'\nimport { Link, useLocation } from 'woute"
},
{
"path": "packages/website/src/pages/docs/advanced/custom-ui/page.tsx",
"chars": 9348,
"preview": "import { APIDivider, APIReference } from '@/components/APIReference'\nimport CodeEditor from '@/components/CodeEditor'\nim"
}
]
// ... and 27 more files (download for full content)
About this extraction
This page contains the full source code of the alibaba/page-agent GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 227 files (756.6 KB), approximately 225.8k tokens, and a symbol index with 571 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.