Compare commits

..

342 Commits

Author SHA1 Message Date
Simon 29fee0012a chore(version): bump version to 1.12.0 2026-07-09 15:50:06 +08:00
Simon 33dcf67252 chore(llms): limit live test concurrency (#604) 2026-07-09 15:47:12 +08:00
Simon ab7e9e9a89 docs: point CI badge at main-ci.yml (#603) 2026-07-09 15:46:53 +08:00
Simon d77ac0322d chore(ci): run main workflows on self-hosted runners (#602)
Split PR CI (GitHub-hosted) from main push CI/deploy (page-agent-trusted).
2026-07-09 15:35:59 +08:00
Simon 3162ac5b45 chore(ci): restrict release workflow to authorized actor 2026-07-08 17:16:39 +08:00
Simon 2c67f3498b chore: rename workflow 2026-07-08 16:30:54 +08:00
Simon 7ee27b4962 feat(chore): update PR template 2026-07-08 16:21:04 +08:00
Simon cad49d5d1f feat(extension): add tab status to context (#599) 2026-07-08 16:14:04 +08:00
Simon 35ff6d44fe fix(extension): stateless SW, pull tab state instead of ports (#596)
* feat(extension)!: make sw stateless; tabs update with pulling instead of pushing

* fix(extension): harden tab loading wait

* fix: safer `waitUntil`

* chore: comments

* chore: reduce polling logs
2026-07-07 21:08:27 +08:00
dependabot[bot] 9609592be3 chore(deps-dev): bump the development-dependencies group with 8 updates (#595) 2026-07-07 11:31:19 +08:00
Simon 08624ec245 fix(ci): format README and align markdown tooling with Prettier (#593) 2026-07-06 17:34:56 +08:00
Simon 8bc2483ab5 docs: improve README copy and quick start (#592)
* docs: improve README copy and quick start

* docs: put each badge on its own line
2026-07-06 16:53:43 +08:00
Simon 68245e8b68 docs: update CHANGELOG 2026-07-04 00:27:49 +08:00
Simon f73831036d chore(version): bump version to 1.11.0 2026-07-03 23:57:03 +08:00
Simon f8af56032a chore(ui): remove legacy motion-css code (#583)
Unused CSS motion experiment with known performance issues; no references remain.
2026-07-03 23:51:29 +08:00
Erwann Robin 60fe4c4c84 chore: set the bookmark script to English locale (#580)
* chore: set the bookmark script to English locale

* chore: adapt the booklet to the locale

* refactor(demo): improve naming to better distinguish mirror and lang

---------

Co-authored-by: Simon <10131203+gaomeng1900@users.noreply.github.com>
2026-07-03 22:49:12 +08:00
Simon 4ce6d9e5c7 Merge pull request #581 from alibaba/refactor/llms
refactor(llms): rewrite model patching and deprecate temperature
2026-07-03 20:37:43 +08:00
Simon 9a75ead6ad fix: do not enable OpenRouter reasoning by default 2026-07-03 20:30:57 +08:00
Simon 10276c6e67 fix: new URL should always be in a try block 2026-07-03 20:14:11 +08:00
Simon 5aeb67f6d6 feat(llms): update model list; update patch 2026-07-03 03:04:43 +08:00
Simon 556bc7448a chore(llms): add real-api test for all supported models 2026-07-03 02:29:06 +08:00
Simon 04b84b6e8a fix(llms): model compatibility problem 2026-07-03 02:26:01 +08:00
Simon 2163a55cc7 docs: remove models that can not be tested anymore from the list 2026-07-03 02:25:03 +08:00
Simon a47e12c0e0 fix: model patch for GPT and Claude 2026-07-02 22:11:28 +08:00
Simon d1c3dc811e docs: update model list 2026-07-02 21:51:55 +08:00
Simon 8ad719744a feat: rewrite llm config and model patch 2026-07-02 21:31:44 +08:00
Simon 83fee37fb4 fix: ci 2026-07-02 03:13:39 +08:00
Simon b41535ef5e Merge pull request #579 from alibaba/chore/cspell-vscode-config
chore: move cSpell config to .vscode/cspell.json
2026-07-02 03:09:00 +08:00
Simon 8ad1174f20 chore: move cSpell config to .vscode/cspell.json
Centralize project dictionary and ignore paths; fix minor prompt typo and GA tag spell-check noise.
2026-07-02 03:05:38 +08:00
Simon 026171715c Merge pull request #578 from alibaba/fix/wrong-active-tab-multi-window
fix: wrong active tab/window in multi-window scenarios
2026-07-02 02:43:04 +08:00
Simon 8b902e1e10 docs(ext): add limitations section 2026-07-02 02:32:33 +08:00
Simon 1f45e74378 fix(extension): resolve active tab/window per caller context 2026-07-02 02:11:32 +08:00
Simon 585e7a4900 chore: update deps 2026-07-01 17:09:57 +08:00
Simon 04a397c19e Merge pull request #577 from alibaba/dependabot/npm_and_yarn/development-dependencies-333bd1d898
chore(deps-dev): bump the development-dependencies group with 16 updates
2026-06-30 19:08:33 +08:00
Simon ae4276c624 fix: code style 2026-06-30 19:00:58 +08:00
dependabot[bot] 7579def667 chore(deps-dev): bump the development-dependencies group with 16 updates
Bumps the development-dependencies group with 16 updates:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `21.0.2` | `21.1.0` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `21.0.2` | `21.1.0` |
| [@eslint-react/eslint-plugin](https://github.com/Rel1cx/eslint-react/tree/HEAD/plugins/eslint-plugin) | `5.9.2` | `5.10.0` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.1` | `4.3.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.0.0` | `26.0.1` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.2` | `6.0.3` |
| [eslint](https://github.com/eslint/eslint) | `10.5.0` | `10.6.0` |
| [prettier](https://github.com/prettier/prettier) | `3.8.4` | `3.9.3` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.62.0` | `8.62.1` |
| [unplugin-dts](https://github.com/qmhc/unplugin-dts/tree/HEAD/packages/unplugin-dts) | `1.0.2` | `1.0.3` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.16` | `8.1.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.21.0` | `1.22.0` |
| [motion](https://github.com/motiondivision/motion) | `12.40.0` | `12.42.0` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.24.0` | `16.24.1` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.1` | `4.3.2` |
| [wxt](https://github.com/wxt-dev/wxt) | `0.20.26` | `0.20.27` |


Updates `@commitlint/cli` from 21.0.2 to 21.1.0
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v21.1.0/@commitlint/cli)

Updates `@commitlint/config-conventional` from 21.0.2 to 21.1.0
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v21.1.0/@commitlint/config-conventional)

Updates `@eslint-react/eslint-plugin` from 5.9.2 to 5.10.0
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v5.10.0/plugins/eslint-plugin)

Updates `@tailwindcss/vite` from 4.3.1 to 4.3.2
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/@tailwindcss-vite)

Updates `@types/node` from 26.0.0 to 26.0.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@vitejs/plugin-react` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.3/packages/plugin-react)

Updates `eslint` from 10.5.0 to 10.6.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.5.0...v10.6.0)

Updates `prettier` from 3.8.4 to 3.9.3
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.4...3.9.3)

Updates `typescript-eslint` from 8.62.0 to 8.62.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.1/packages/typescript-eslint)

Updates `unplugin-dts` from 1.0.2 to 1.0.3
- [Release notes](https://github.com/qmhc/unplugin-dts/releases)
- [Changelog](https://github.com/qmhc/unplugin-dts/blob/main/packages/unplugin-dts/CHANGELOG.md)
- [Commits](https://github.com/qmhc/unplugin-dts/commits/unplugin-dts@1.0.3/packages/unplugin-dts)

Updates `vite` from 8.0.16 to 8.1.0
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.1.0/packages/vite)

Updates `lucide-react` from 1.21.0 to 1.22.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.22.0/packages/lucide-react)

Updates `motion` from 12.40.0 to 12.42.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.40.0...v12.42.0)

Updates `simple-icons` from 16.24.0 to 16.24.1
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.24.0...16.24.1)

Updates `tailwindcss` from 4.3.1 to 4.3.2
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/tailwindcss)

Updates `wxt` from 0.20.26 to 0.20.27
- [Release notes](https://github.com/wxt-dev/wxt/releases)
- [Commits](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.26...wxt-v0.20.27)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 21.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 21.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@eslint-react/eslint-plugin"
  dependency-version: 5.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint
  dependency-version: 10.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: prettier
  dependency-version: 3.9.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.62.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: unplugin-dts
  dependency-version: 1.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: vite
  dependency-version: 8.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 1.22.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: motion
  dependency-version: 12.42.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.24.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: wxt
  dependency-version: 0.20.27
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 00:43:38 +00:00
Simon 187d5deccb Merge pull request #568 from itxaiohanglover/fix/chat-latest-reasoning-effort
fix(llms): skip reasoning_effort patch for chat-latest models
2026-06-26 01:07:30 +08:00
Simon fb221b4535 Merge pull request #573 from alibaba/feat/assets-repo
fix(assets): migrate img.alicdn.com URLs to page-agent.github.io
2026-06-26 01:02:50 +08:00
Simon 5058c96a78 fix(assets): migrate img.alicdn.com URLs to page-agent.github.io 2026-06-26 00:56:37 +08:00
Simon 2b679d85cb fix(llms): omitting both reasoning_effort and temperature for chat-latest 2026-06-26 00:12:34 +08:00
artboy 57f64347ac fix(llms): skip reasoning_effort patch for *-chat-latest models
normalizeModelName reduces gpt-5.2-chat-latest to gpt-52-chat-latest,
which matches the gpt-52 prefix and gets reasoning_effort='none'.
But chat-latest models don't support reasoning_effort, causing
HTTP 400.

Fix: check for 'chat-latest' before applying reasoning_effort patches.

Fixes #563
2026-06-23 14:26:45 +08:00
Simon fb4b8fd067 Merge pull request #565 from alibaba/dependabot/github_actions/github-actions-640176b5ab
chore(deps): bump actions/checkout from 6 to 7 in the github-actions group
2026-06-23 14:24:52 +08:00
Simon 2bed20cd85 Merge pull request #566 from alibaba/dependabot/npm_and_yarn/development-dependencies-1075e1fe2e
chore(deps-dev): bump the development-dependencies group with 8 updates
2026-06-23 14:24:36 +08:00
Simon c48c7c524e Merge pull request #567 from alibaba/dependabot/npm_and_yarn/development-major-3bc154b33b
chore(deps-dev): bump @types/node from 25.9.3 to 26.0.0 in the development-major group
2026-06-23 14:24:20 +08:00
dependabot[bot] 32f27e73e3 chore(deps-dev): bump @types/node in the development-major group
Bumps the development-major group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 25.9.3 to 26.0.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: development-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 00:30:20 +00:00
dependabot[bot] 71738f1966 chore(deps-dev): bump the development-dependencies group with 8 updates
Bumps the development-dependencies group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@eslint-react/eslint-plugin](https://github.com/Rel1cx/eslint-react/tree/HEAD/plugins/eslint-plugin) | `5.9.0` | `5.9.2` |
| [globals](https://github.com/sindresorhus/globals) | `17.6.0` | `17.7.0` |
| [happy-dom](https://github.com/capricorn86/happy-dom) | `20.10.4` | `20.10.6` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `17.0.7` | `17.0.8` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.61.1` | `8.62.0` |
| [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) | `0.1.43` | `0.2.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.18.0` | `1.21.0` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.23.0` | `16.24.0` |


Updates `@eslint-react/eslint-plugin` from 5.9.0 to 5.9.2
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v5.9.2/plugins/eslint-plugin)

Updates `globals` from 17.6.0 to 17.7.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.6.0...v17.7.0)

Updates `happy-dom` from 20.10.4 to 20.10.6
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.10.4...v20.10.6)

Updates `lint-staged` from 17.0.7 to 17.0.8
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v17.0.7...v17.0.8)

Updates `typescript-eslint` from 8.61.1 to 8.62.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.0/packages/typescript-eslint)

Updates `@types/chrome` from 0.1.43 to 0.2.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome)

Updates `lucide-react` from 1.18.0 to 1.21.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.21.0/packages/lucide-react)

Updates `simple-icons` from 16.23.0 to 16.24.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.23.0...16.24.0)

---
updated-dependencies:
- dependency-name: "@eslint-react/eslint-plugin"
  dependency-version: 5.9.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: globals
  dependency-version: 17.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: happy-dom
  dependency-version: 20.10.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: lint-staged
  dependency-version: 17.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@types/chrome"
  dependency-version: 0.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 1.21.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.24.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 00:29:32 +00:00
dependabot[bot] e44455ba80 chore(deps): bump actions/checkout in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 00:27:34 +00:00
Simon d2ee260d82 Merge pull request #560 from alibaba/dependabot/npm_and_yarn/development-dependencies-d854c95bb8
chore(deps-dev): bump the development-dependencies group across 1 directory with 17 updates
2026-06-17 20:53:39 +08:00
dependabot[bot] 1f738bc41c chore(deps-dev): bump the development-dependencies group across 1 directory with 17 updates
Bumps the development-dependencies group with 16 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@eslint-react/eslint-plugin](https://github.com/Rel1cx/eslint-react/tree/HEAD/plugins/eslint-plugin) | `5.8.16` | `5.9.0` |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.58.8` | `7.58.9` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.0` | `4.3.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.2` | `25.9.3` |
| [eslint](https://github.com/eslint/eslint) | `10.4.1` | `10.5.0` |
| [happy-dom](https://github.com/capricorn86/happy-dom) | `20.10.2` | `20.10.4` |
| [prettier](https://github.com/prettier/prettier) | `3.8.3` | `3.8.4` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.61.0` | `8.61.1` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.8` | `4.1.9` |
| [@radix-ui/react-hover-card](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/hover-card) | `1.1.16` | `1.1.17` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.9` | `2.1.10` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.9` | `1.1.10` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.2.5` | `1.3.0` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.0` | `1.3.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.17.0` | `1.18.0` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.9` | `1.2.10` |



Updates `@eslint-react/eslint-plugin` from 5.8.16 to 5.9.0
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v5.9.0/plugins/eslint-plugin)

Updates `@microsoft/api-extractor` from 7.58.8 to 7.58.9
- [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md)
- [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-extractor_v7.58.9/apps/api-extractor)

Updates `@tailwindcss/vite` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-vite)

Updates `@types/node` from 25.9.2 to 25.9.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.4.1 to 10.5.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.4.1...v10.5.0)

Updates `happy-dom` from 20.10.2 to 20.10.4
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.10.2...v20.10.4)

Updates `prettier` from 3.8.3 to 3.8.4
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.3...3.8.4)

Updates `typescript-eslint` from 8.61.0 to 8.61.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.1/packages/typescript-eslint)

Updates `vitest` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/HEAD/packages/vitest)

Updates `@radix-ui/react-hover-card` from 1.1.16 to 1.1.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/hover-card/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/hover-card)

Updates `@radix-ui/react-label` from 2.1.9 to 2.1.10
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-separator` from 1.1.9 to 1.1.10
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slot` from 1.2.5 to 1.3.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-switch` from 1.3.0 to 1.3.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `lucide-react` from 1.17.0 to 1.18.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.18.0/packages/lucide-react)

Updates `tailwindcss` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/tailwindcss)

Updates `@radix-ui/react-tooltip` from 1.2.9 to 1.2.10
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

---
updated-dependencies:
- dependency-name: "@eslint-react/eslint-plugin"
  dependency-version: 5.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.58.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint
  dependency-version: 10.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: happy-dom
  dependency-version: 20.10.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: prettier
  dependency-version: 3.8.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.61.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: vitest
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-hover-card"
  dependency-version: 1.1.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 1.18.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 05:00:12 +00:00
Simon 2802ab3dea Merge pull request #559 from alibaba/fix/hoist-happy-dom
fix(deps): hoist happy-dom to root devDependencies
2026-06-16 12:33:51 +08:00
Simon e6a2e07640 fix(deps): hoist happy-dom to root devDependencies 2026-06-16 12:30:55 +08:00
Simon 235fa1bc26 chore: fix lint error caused CI fail 2026-06-15 22:59:34 +08:00
Simon c04c02723d chore(version): bump version to 1.10.0 2026-06-15 17:31:54 +08:00
Simon 77468e4ab4 chore(version): bump version to 1.9.1 2026-06-15 17:22:10 +08:00
Simon 7f9cb61fc2 Merge pull request #557 from alibaba/feat/remove-nav-back-instructions
feat: remove instructions about nav back
2026-06-15 16:55:21 +08:00
Simon 76fea5b285 feat: remove instructions about nav back 2026-06-15 16:41:02 +08:00
Simon a796453c6e chore: rm go_back todo 2026-06-12 19:10:18 +08:00
Simon a1b0dd90eb Merge pull request #511 from linked-danis/fix/report-actual-wait-time
Report actual wait duration
2026-06-12 17:49:43 +08:00
Simon fb8c42daa4 feat: simplify waiting response to reduce LLM cognitive load 2026-06-12 17:44:21 +08:00
Simon 8eaa784a8a fix: github demo video broken 2026-06-11 21:40:01 +08:00
Simon 7dffaa4718 Merge pull request #512 from linked-danis/fix/scroll-predicate-types
Fix scroll predicate types (boolean | null -> boolean)
2026-06-11 21:22:31 +08:00
Simon 0f3f639497 Merge pull request #552 from alibaba/test/page-controller-happy-dom
test(page-controller): add happy-dom env and basic tests
2026-06-11 21:14:28 +08:00
Simon 67b1874836 Merge pull request #551 from alibaba/fix/stale-current-tab-id-mask-flash
fix(extension): clear currentTabId in storage on TabsController.init
2026-06-11 21:13:31 +08:00
Simon 56c09a9ae9 test(page-controller): add happy-dom env and basic tests 2026-06-11 21:10:05 +08:00
Simon e65c7c9601 Merge pull request #545 from alibaba/feat/execute-js-abort-signal
feat(core): make execute_javascript honor AbortSignal
2026-06-11 20:45:53 +08:00
Simon a646da80f5 Merge pull request #549 from alibaba/refactor/agent-lifecycle-state-machine
refactor(core)!: rework agent run lifecycle and status semantics
2026-06-11 20:37:37 +08:00
Simon d3797109bf fix(extension): clear currentTabId in storage on TabsController.init
Reset currentTabId via updateCurrentTabId(null) so the storage projection
stays consistent with isAgentRunning. Prevents the previous run's tab from
briefly flashing the mask when a new run starts within the heartbeat window.

Closes #550
2026-06-11 20:37:19 +08:00
Simon 4e881f7eb0 chore: add notes for a pending design decision 2026-06-11 20:31:01 +08:00
Simon 768b07e28c feat: disable ScriptExecutionTool for MultiPageAgent 2026-06-11 20:16:09 +08:00
Simon 4828f9f726 chore: update tests 2026-06-11 20:06:35 +08:00
Simon edb769b826 fix(ext): handle stopped lifecycle state 2026-06-11 19:53:30 +08:00
Simon e270ba15b5 fix(ext): clear stale activity on any non-running status
The side panel kept showing the last activity card when a run settled as
`stopped`; clear it for every terminal status instead of an allowlist.
2026-06-11 19:15:58 +08:00
Simon c19891926b refactor(ext): drive heartbeat and running flag from statuschange
Project agent status into chrome.storage via a statuschange listener
instead of pairing setup/teardown across lifecycle hooks. A throwing hook
can no longer leak the heartbeat or strand isAgentRunning, and rejected
concurrent execute() calls never touch the active run's state.
2026-06-11 19:15:48 +08:00
Simon 0438bf6265 fix(core): harden run settlement edge cases from review
- install #running before the `running` statuschange fires, so a listener
  calling stop() immediately awaits the current run
- await async mask/highlight cleanup before settling: once settled, the
  agent must be safely reusable
- make the inter-step delay abortable so stop() settles promptly; abort
  during the delay is classified as `stopped`
2026-06-11 19:15:27 +08:00
Simon 255e8fc861 chore: update docs 2026-06-11 17:29:54 +08:00
Simon 4690aefec5 refactor(core): decouple run settling from terminal status transition
Resolve #running before the terminal statuschange so the settle signal can
never be lost to re-entrant listeners. Hooks keep middleware semantics:
a throwing hook fails the run; integrations that don't want this should
suppress errors in their own hooks. Also make suppress() async-aware so
rejected promises (e.g. showMask) are actually caught.
2026-06-11 17:21:11 +08:00
Simon 8f9a637bdb refactor(core): suppress mask/highlight errors instead of failing the run
Visual feedback failures (showMask, hideMask, cleanUpHighlights) are
non-critical; log them instead of aborting the task or masking the
original error during teardown.
2026-06-11 16:26:42 +08:00
Simon c2d6a864f8 refactor(core): settle terminal status only after run cleanup completes
Defer the terminal statuschange to the outer finally via settleRun, closing
the window where a listener could re-enter execute() during teardown. Also
check abort at step start so aborts during stepDelay settle as `stopped`.
2026-06-11 16:26:05 +08:00
Simon 1deaa2147e fix: mke sure taskResult and onAfterStep wont get lost 2026-06-11 15:39:35 +08:00
Simon 052a302a08 refactor(core)!: rework agent run lifecycle and status semantics
BREAKING CHANGE: stop() is now async and resolves after the run fully
settles; status decouples from task outcome (new 'stopped' state, LLM
self-reported failure now ends as 'completed'). Lifecycle hooks re-throw
instead of being folded into the result; agent errors go to history.
Adds agent.lastResult.
2026-06-11 14:33:12 +08:00
Simon 9ed0a09194 feat(core): make execute_javascript honor AbortSignal
Expose the task AbortSignal as `signal` in the script scope so cooperative
code can cancel promptly, and re-check signal.throwIfAborted() after the
script settles to discard stale results.

Closes #537.
2026-06-09 21:30:27 +08:00
Simon 73810b3ed8 Merge pull request #544 from alibaba/feat/refine-dark-mode-check
feat(page-controller): refine dark mode detection heuristics
2026-06-09 20:43:15 +08:00
Simon 01c9cc9ea7 feat(page-controller): refine dark mode detection heuristics 2026-06-09 20:33:14 +08:00
Simon a7beabbbe2 Merge pull request #543 from alibaba/test/page-agent-core-lifecycle
test(core): add PageAgentCore lifecycle tests
2026-06-09 20:32:23 +08:00
Simon b3ff0a1a25 Merge pull request #373 from tageniu/feat/dark-mode-detect
Implement TODO: enhance dark mode detection
2026-06-09 20:18:33 +08:00
Simon daae28db34 test(core): add PageAgentCore lifecycle tests 2026-06-09 17:07:10 +08:00
Simon 797c238862 Merge pull request #534 from 64johnlee/chore/remove-debug-console-logs
chore(page-controller): remove accidental debug console.log calls
2026-06-09 15:07:19 +08:00
Simon 270791bb54 feat: add .codex to .gitignore 2026-06-09 14:50:21 +08:00
Simon 8ec460f431 Merge pull request #542 from alibaba/dependabot/npm_and_yarn/development-dependencies-a28b716e5c 2026-06-09 04:03:38 +08:00
dependabot[bot] 8c3a5f8188 chore(deps-dev): bump the development-dependencies group with 13 updates
Bumps the development-dependencies group with 13 updates:

| Package | From | To |
| --- | --- | --- |
| [@eslint-react/eslint-plugin](https://github.com/Rel1cx/eslint-react/tree/HEAD/plugins/eslint-plugin) | `5.8.10` | `5.8.16` |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.58.7` | `7.58.8` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.1` | `25.9.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.60.1` | `8.61.0` |
| [@radix-ui/react-hover-card](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/hover-card) | `1.1.15` | `1.1.16` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.8` | `2.1.9` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.8` | `1.1.9` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.2.4` | `1.2.5` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.2.6` | `1.3.0` |
| [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) | `0.1.42` | `0.1.43` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.16` | `19.2.17` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.22.0` | `16.23.0` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.8` | `1.2.9` |


Updates `@eslint-react/eslint-plugin` from 5.8.10 to 5.8.16
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v5.8.16/plugins/eslint-plugin)

Updates `@microsoft/api-extractor` from 7.58.7 to 7.58.8
- [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md)
- [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-extractor_v7.58.8/apps/api-extractor)

Updates `@types/node` from 25.9.1 to 25.9.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `typescript-eslint` from 8.60.1 to 8.61.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.0/packages/typescript-eslint)

Updates `@radix-ui/react-hover-card` from 1.1.15 to 1.1.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/hover-card/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/hover-card)

Updates `@radix-ui/react-label` from 2.1.8 to 2.1.9
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-separator` from 1.1.8 to 1.1.9
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slot` from 1.2.4 to 1.2.5
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-switch` from 1.2.6 to 1.3.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@types/chrome` from 0.1.42 to 0.1.43
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome)

Updates `@types/react` from 19.2.16 to 19.2.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `simple-icons` from 16.22.0 to 16.23.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.22.0...16.23.0)

Updates `@radix-ui/react-tooltip` from 1.2.8 to 1.2.9
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

---
updated-dependencies:
- dependency-name: "@eslint-react/eslint-plugin"
  dependency-version: 5.8.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.58.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.9.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-hover-card"
  dependency-version: 1.1.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@types/chrome"
  dependency-version: 0.1.43
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.23.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 19:34:16 +00:00
Simon 4ee773bef9 chore(changelog): update for version 1.9.0 release 2026-06-08 22:58:56 +08:00
Simon 3a2f0a41ec chore(version): bump version to 1.9.0 2026-06-08 22:42:46 +08:00
Simon 64bb1f1a56 Merge pull request #541 from alibaba/docs/update-model-list
docs: update model list
2026-06-08 22:37:23 +08:00
Simon 2afcf13fbb feat: update model list 2026-06-08 22:34:33 +08:00
Simon f617728fc3 Merge pull request #540 from alibaba/feat/claude-opus-4-8
feat(llms): add Claude Opus 4.8 support
2026-06-08 22:27:53 +08:00
Simon 9376abfe2a Merge pull request #539 from alibaba/docs/abort-and-concurrency
docs(website): document ctx.signal abort contract and execute() concurrency
2026-06-08 22:26:19 +08:00
Simon 2f343bcc56 feat(llms): add Claude Opus 4.8 support
Strip temperature for claude-opus-4-8, add tests, list model on website.
2026-06-08 22:23:56 +08:00
Simon 4108f67079 docs(website): document ctx.signal abort contract and execute() concurrency 2026-06-08 22:19:25 +08:00
Simon 0ba3bbd67f Merge pull request #536 from alibaba/fix/prevent-concurrent-execute
fix(core): prevent concurrent execute() calls
2026-06-08 22:04:19 +08:00
Simon 552987eb56 fix(core): prevent concurrent execute() calls 2026-06-08 21:58:16 +08:00
Simon 99ef08ac79 Merge pull request #532 from alibaba/feat/rubost-abort
feat: robust aborting
2026-06-08 21:53:05 +08:00
Simon 4445bec08a fix: abort may affect onAfterTask; clean up while loop 2026-06-08 19:46:19 +08:00
Simon 6530f0ef40 chore: cleanup; rm get abortSignal 2026-06-08 17:40:44 +08:00
Simon f8676a5cc2 fix: throw unhonored AbortError & rm detection code for it 2026-06-08 17:27:05 +08:00
64JohnLee 7b623c5b22 chore(page-controller): remove accidental debug console.log calls
Two debug log statements were left in production code:

- `isScrollableElement()` logged `scrollData!!!` for every scrollable
  element found during DOM tree construction. Because the DOM is rebuilt
  on every agent action, this fired repeatedly and also triggered
  unnecessary JSON serialisation of scroll metrics on the hot path.

- `SimulatorMask.dispose()` logged 'dispose SimulatorMask' every time
  the highlight overlay was torn down.

Neither had a structured prefix or was gated behind a debug flag.
Removing both silences console noise for end users and removes the
serialisation overhead in the scroll-detection hot path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 16:47:04 +08:00
Simon 029f64e4bc Merge branch 'feat/rubost-abort' of https://github.com/alibaba/page-agent into feat/rubost-abort 2026-06-05 21:25:07 +08:00
Simon 78b6e2ad3c feat: all sync tools should respect aborting 2026-06-05 21:23:18 +08:00
Simon 112f9fff39 docs: planning 2026-06-05 20:27:30 +08:00
Simon 1c82030a21 Merge pull request #531 from alibaba/chore/vitest-llms
chore(llms): add vitest unit tests
2026-06-04 21:05:19 +08:00
Simon f378972ef8 docs: fix test workflow docs 2026-06-04 20:43:09 +08:00
Simon 68ae73d4e6 chore(llms): add vitest unit tests for llms package 2026-06-04 20:27:33 +08:00
Simon 874b302860 Merge pull request #530 from alibaba/fix/dts
fix: dts bug hotifx
2026-06-04 17:51:00 +08:00
Simon 90ff96ca15 fix: dts global declarations should not be bundled 2026-06-04 17:44:59 +08:00
Simon 3c0d55fa33 Merge pull request #527 from alibaba/fix/abort-error-contract
improve error handling
2026-06-03 23:40:48 +08:00
Simon 9891e02215 fix(llms): handle AbortError during .json() 2026-06-03 23:31:01 +08:00
Simon 823195ad94 fix(llms): add error handling for invalid JSON responses and schema validation 2026-06-03 23:14:12 +08:00
Simon fd12fb9f1b refactor(llms): split AbortError out of InvokeError 2026-06-03 23:00:33 +08:00
Simon fb8de08c39 fix(llms): fix abort handling and clean up retry logic 2026-06-03 22:44:54 +08:00
Simon 738a62e96c Merge pull request #526 from alibaba/chore/typo
chore: typo
2026-06-03 20:22:17 +08:00
Simon e7b93efda0 chore: typo
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 20:20:37 +08:00
Simon 0b6f0a6068 Merge pull request #524 from alibaba/dependabot/npm_and_yarn/development-major-a28a93d02a
chore(deps-dev): bump concurrently from 9.2.1 to 10.0.3 in the development-major group
2026-06-03 15:12:41 +08:00
Simon cc16d39e00 Merge pull request #525 from alibaba/fix/wxt-audit-overrides
fix: temporarily patch WXT audit vulnerabilities
2026-06-03 15:11:51 +08:00
Simon 7eceff26ef fix: patch WXT audit vulnerabilities 2026-06-03 15:05:34 +08:00
dependabot[bot] 61a791f759 chore(deps-dev): bump concurrently in the development-major group
Bumps the development-major group with 1 update: [concurrently](https://github.com/open-cli-tools/concurrently).


Updates `concurrently` from 9.2.1 to 10.0.3
- [Release notes](https://github.com/open-cli-tools/concurrently/releases)
- [Commits](https://github.com/open-cli-tools/concurrently/compare/v9.2.1...v10.0.3)

---
updated-dependencies:
- dependency-name: concurrently
  dependency-version: 10.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: development-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 06:05:01 +00:00
Simon 6ecd6e61f6 Merge pull request #506 from tolerious/fix/fix-bug-505
fix(website): normalize trailing slash in sidebar location comparison
2026-06-02 17:56:30 +08:00
Simon 1e13385f1f Merge pull request #520 from alibaba/chore/upgrade-deps
chore: upgrade deps
2026-06-02 17:16:54 +08:00
Simon dec9fe8b3d chore: update node version and lockfile 2026-06-02 17:11:18 +08:00
Simon ebd8ec5ae7 chore: upgrade deps 2026-05-28 16:06:15 +08:00
linked-danis e525e4c6dd fix: report actual wait duration 2026-05-18 15:01:06 +02:00
linked-danis 903c2a736c fix: return booleans from scroll predicates 2026-05-18 15:00:52 +02:00
Toly 482190777c fix(website): normalize trailing slash in sidebar location comparison 2026-05-16 21:03:51 +08:00
Simon ebc785d1fe chore(version): bump version to 1.8.2 2026-05-11 19:40:30 +08:00
Simon ba932283f2 Merge pull request #501 from alibaba/feat/iife-cdn-without-auto-init
feat: add autoInit switch for demo CDN script
2026-05-11 19:39:19 +08:00
Simon f05bf68f61 docs: ?autoInit=false docs 2026-05-11 19:28:49 +08:00
Simon 35ab12a20a feat: add autoInit switch for IIFE demo 2026-05-11 19:20:52 +08:00
Simon fd7c54a695 Merge pull request #500 from alibaba/fix/demo-showpanel-type
fix(page-agent): keep demo showPanel out of config type
2026-05-11 19:06:35 +08:00
Simon eafd834daf fix(page-agent): keep demo showPanel out of config type 2026-05-11 19:05:27 +08:00
Feffery f575ae0fa4 feat: add showPanel switch to IIFE demo 2026-05-11 19:01:00 +08:00
Simon 797c9f8575 Merge pull request #499 from alibaba/chore/upgrade-dependencies
chore: upgrade dependencies
2026-05-11 17:23:36 +08:00
Simon fb3ac4d454 chore: upgrade dependencies 2026-05-11 17:19:23 +08:00
Simon c629d706ec chore: remove vite-bundle-analyzer 2026-04-28 16:46:05 +08:00
Simon 7896786311 Merge pull request #491 from alibaba/feat/vite8
Upgrade vite 8
2026-04-28 16:29:42 +08:00
Simon 820a2654ff fix: set minimal npm version to avoid peer: true 2026-04-28 16:24:50 +08:00
Simon 8d45ab8436 chore(deps): update package-lock.json 2026-04-28 16:15:16 +08:00
Simon 855020636b Merge branch 'main' into feat/vite8 2026-04-28 16:03:01 +08:00
Simon ff82603839 Merge pull request #490 from alibaba/fix/ts-value-space
fix(ts): `InvokeErrorType` separate confusing value/type space
2026-04-28 16:02:35 +08:00
Simon 90270fb76c fix(ts): InvokeErrorType separate confusing value/type space 2026-04-28 15:57:51 +08:00
Simon c9edeaea44 refactor(setup): upgrade vite8 2026-04-28 15:33:11 +08:00
Simon 757fe07b4d Merge pull request #489 from alibaba/fix/website-manual-chunks
fix(website): make manualChunks work
2026-04-28 14:44:29 +08:00
Simon f04fa706f7 fix(website): make manualChunks work 2026-04-28 14:42:34 +08:00
Simon 9daf914a89 Merge pull request #486 from alibaba/dependabot/npm_and_yarn/development-dependencies-82874c61ad
chore(deps-dev): bump the development-dependencies group with 7 updates
2026-04-28 14:21:07 +08:00
dependabot[bot] c8e5ff3010 chore(deps-dev): bump the development-dependencies group with 7 updates
Bumps the development-dependencies group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.5.0` | `20.5.2` |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.58.6` | `7.58.7` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.2.3` | `4.2.4` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.59.0` | `8.59.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.8.0` | `1.11.0` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.17.0` | `16.18.0` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.2.3` | `4.2.4` |


Updates `@commitlint/cli` from 20.5.0 to 20.5.2
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.5.2/@commitlint/cli)

Updates `@microsoft/api-extractor` from 7.58.6 to 7.58.7
- [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md)
- [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-extractor_v7.58.7/apps/api-extractor)

Updates `@tailwindcss/vite` from 4.2.3 to 4.2.4
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.4/packages/@tailwindcss-vite)

Updates `typescript-eslint` from 8.59.0 to 8.59.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.1/packages/typescript-eslint)

Updates `lucide-react` from 1.8.0 to 1.11.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.11.0/packages/lucide-react)

Updates `simple-icons` from 16.17.0 to 16.18.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.17.0...16.18.0)

Updates `tailwindcss` from 4.2.3 to 4.2.4
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.4/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.58.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.59.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 1.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.18.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 22:52:43 +00:00
Simon f5591dd3ea chore(version): bump version to 1.8.1 2026-04-27 23:32:01 +08:00
Simon 62a0d988ce Merge pull request #484 from alibaba/feat/gpt-54-models
feat(models): support gpt-5.4 and update model list
2026-04-27 21:43:28 +08:00
Simon dc41969503 feat(models): support gpt-5.4 and refresh recommendations 2026-04-27 21:40:19 +08:00
Simon 9d9a4b9698 Merge pull request #483 from alibaba/docs/website-deepseek-v4-tested-models
docs(website): add DeepSeek v4 flash and pro to tested models
2026-04-27 20:49:41 +08:00
Simon b171bf210f docs(models): update DeepSeek model recommendation 2026-04-27 20:49:17 +08:00
Simon c39678dece docs(website): add DeepSeek v4 flash and pro to tested models 2026-04-27 20:43:59 +08:00
Simon 6c1f552606 chore: update skills 2026-04-27 20:36:17 +08:00
Simon dcad67d4b4 Merge pull request #482 from alibaba/fix/llms-deepseek-tool-choice
fix(llms): remove tool_choice for deepseek models
2026-04-27 20:35:37 +08:00
Simon 15f3af406c fix(llms): remove tool_choice for deepseek models 2026-04-27 20:32:53 +08:00
Simon b50abcc4ec Merge pull request #476 from fuyua9/fix/mcp-server-package-version
fix(mcp): advertise the package version
2026-04-27 20:22:09 +08:00
Simon 20f1b1f51e docs(website): add prompt caching guidance (#481) 2026-04-27 20:14:01 +08:00
Simon cd035cd30f docs(website): add prompt caching guidance 2026-04-27 20:12:25 +08:00
zzy-life a7cc935453 feat(llms): add transformRequestBody hook and improve prompt assembly (#480)
* feat(llms): add transformRequestBody hook and refine prompt handling

* docs(website): document transformRequestBody usage

* refactor(extension): keep function-valued config handling consistent in useAgent

* feat: simplify `transformRequestBody`

---------

Co-authored-by: Simon <10131203+gaomeng1900@users.noreply.github.com>
2026-04-27 19:46:46 +08:00
Simon 349609614b Merge pull request #473 from akinshaywai/chore/remove-debug-console-logs
chore: remove debug console.log statements from production code
2026-04-27 17:30:13 +08:00
Simon ad680cee99 Merge pull request #472 from akinshaywai/fix/security-hub-link-rel
fix: add rel="noopener noreferrer" to hub link in ConfigPanel
2026-04-27 17:26:50 +08:00
fuyua9 e95b2d6a28 fix(mcp): advertise package version 2026-04-25 10:38:57 +08:00
akinshaywai f55c1b984b chore: remove debug console.log statements from production code
Three debug logs left in production paths:

- useAgent.ts: logs task string on every agent execution
- HeroSection.tsx: logs getPageInstructions url/hint on every page visit
- HeroSection.tsx: logs the full execute() result to the console

None of these are error-level events and they leak internal details
to the browser console in production builds.
2026-04-24 01:49:22 +01:00
akinshaywai 5ab34a2f50 fix: add rel="noopener noreferrer" to hub link in ConfigPanel
The 'Manage Page Agent Hub' link uses target="_blank" without
rel="noopener noreferrer". This exposes the opener browsing context
to the target page via window.opener. All other external links in
ConfigPanel already include this attribute.
2026-04-24 01:48:37 +01:00
Simon de24d97a22 Merge pull request #465 from alibaba/dependabot/npm_and_yarn/development-dependencies-c41b1a6d79
chore(deps-dev): bump the development-dependencies group with 4 updates
2026-04-21 14:57:57 +08:00
Simon c6f09375f8 fix(controller): isMainContentDark too aggressive 2026-04-20 20:31:50 -04:00
tageniu cc33297cc4 fix(controller): check html and body data attrs independently
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-20 20:31:45 -04:00
Shijie Sun 8556745645 feat(page-controller): enhance dark mode detection
Resolve the @TODO in checkDarkMode.ts by adding 3 new detection
strategies and expanding data-attribute coverage:

- Check data-color-mode, data-bs-theme, data-color-scheme attributes
- Read CSS color-scheme property and <meta name="color-scheme"> tag
- Inspect background of SPA containers (#app, #root, #__next, etc.)
- Detect light text color as a dark-theme signal
2026-04-20 20:14:07 -04:00
dependabot[bot] 317b2a1f10 chore(deps-dev): bump the development-dependencies group with 4 updates
Bumps the development-dependencies group with 4 updates: [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor), [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite), [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) and [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss).


Updates `@microsoft/api-extractor` from 7.58.5 to 7.58.6
- [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md)
- [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-extractor_v7.58.6/apps/api-extractor)

Updates `@tailwindcss/vite` from 4.2.2 to 4.2.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.3/packages/@tailwindcss-vite)

Updates `typescript-eslint` from 8.58.2 to 8.59.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.0/packages/typescript-eslint)

Updates `tailwindcss` from 4.2.2 to 4.2.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.3/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.58.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.59.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 21:17:15 +00:00
Simon 7bc2fa4d59 Merge pull request #464 from alibaba/docs/website-model-list-qwen36
docs(website): add qwen3.6-max/flash to model list
2026-04-20 21:39:48 +08:00
Simon 9d5c5a8969 fix: rebuild package lock 2026-04-20 21:38:28 +08:00
Simon 052d302f16 chore: upgrade wxt to version 0.20.25 2026-04-20 21:33:18 +08:00
Simon bb4da7fd86 docs(website): add qwen3.6-max/flash to model list 2026-04-20 21:28:49 +08:00
Simon 359e6eadc8 Merge pull request #463 from alibaba/fix/ci
fix: upgrade wxt to fix npm ci
2026-04-18 01:20:55 +08:00
Simon e41a2609b3 fix: upgrade wxt to fix ci 2026-04-18 01:19:14 +08:00
Simon b23c909953 chore: dep vulnerability 2026-04-16 20:13:44 +08:00
Simon 79bf9e2aee Merge pull request #459 from alibaba/docs/cleanup-ai-aliases
chore: support claude code and antigravity
2026-04-16 20:07:46 +08:00
Simon d9d8d558a9 docs: clean up AI assistant alias config
Unignore CLAUDE.md and .agent, add CLAUDE.md alias file,
and simplify the developer guide alias section.
2026-04-16 20:05:08 +08:00
Simon 6afd3dcbdf Merge pull request #458 from alibaba/refactor/ci-and-prettier
refactor(setup): consolidate prettier config and streamline CI
2026-04-16 17:05:47 +08:00
Simon 5809fe3249 refactor(setup): consolidate prettier config and streamline CI
- Replace scattered .prettierignore files with a single root config
- Add scripts/ci.js to orchestrate lint, format, typecheck, commitlint, and build
- Simplify ci.yml to use ci.js and npm ci
- Apply prettier formatting to docs, locales, and HTML files
2026-04-16 17:04:23 +08:00
Simon e626e0b8ce Merge pull request #456 from akinshaywai/design/history-ui-improvements
design(ui): improve HistoryList loading/empty states and add button tooltips
2026-04-16 16:02:16 +08:00
Simon 284098ff0d chore: catch session loading errors 2026-04-16 15:57:04 +08:00
akinshaywai 90e1c297f4 style(ui): improve HistoryList loading/empty states and add button tooltips
- Replace plain 'Loading...' text in HistoryList with animated skeleton
  placeholder rows that match the shape of real history items
- Replace plain 'No history yet' text with a centred History icon +
  label for a more polished empty state
- Add aria-label and title to the Back button in HistoryList header
- Add aria-label and title to History and Settings buttons in main header
- Add aria-label and title to Send and Stop task buttons in footer
2026-04-16 15:56:57 +08:00
Simon 23adbea744 Merge pull request #455 from alibaba/chore/parallel-build-scripts
chore(scripts): add parallel build scripts
2026-04-15 17:45:52 +08:00
Simon 9af3a3f73e chore(scripts): add parallel build scripts
Replace sequential `npm run build --workspaces --if-present` with
concurrent execution via a reusable `parallelTask` utility. Full
`npm run build` now runs cleanup then all 7 build tasks in parallel.
2026-04-15 17:44:37 +08:00
Simon cc27ff9305 docs: update the dev guide 2026-04-15 17:05:09 +08:00
Olabode Felix Akinyemi 8a75c15557 Merge pull request #454 from akinshaywai/fix/a11y-config-panel-labels
fix(a11y): add accessible labels to ConfigPanel inputs and icon buttons
2026-04-15 16:31:24 +08:00
Simon e9eaf44bdd docs(ext): no more need to build libs before ext 2026-04-15 04:02:16 +08:00
Simon 2ad2d06d01 chore(version): bump version to 1.8.0 2026-04-15 03:11:14 +08:00
Simon 8bf7abfe76 chore: wxt version 2026-04-15 03:09:15 +08:00
Simon 099a1251cd Merge branch 'main' of https://github.com/alibaba/page-agent 2026-04-14 15:54:25 +08:00
Simon 354397a1b4 Merge pull request #449 from alibaba/dependabot/npm_and_yarn/development-dependencies-d34cd256e0
chore(deps-dev): bump the development-dependencies group with 11 updates
2026-04-14 15:53:22 +08:00
Simon a1d9e11c05 Merge pull request #448 from alibaba/dependabot/github_actions/github-actions-903567a242
chore(deps): bump actions/upload-pages-artifact from 4 to 5 in the github-actions group
2026-04-14 15:52:48 +08:00
Simon 33538cca05 chore(dependabot): set labels 2026-04-14 15:51:55 +08:00
Simon 50f4866142 Merge pull request #447 from alibaba/fix/iife-keepnames-redeclaration
fix(vite): remove global helper functions in built assets
2026-04-14 15:45:47 +08:00
dependabot[bot] 28a2ebb817 chore(deps-dev): bump the development-dependencies group with 11 updates
Bumps the development-dependencies group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.58.1` | `7.58.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.5.2` | `25.6.0` |
| [dotenv](https://github.com/motdotla/dotenv) | `17.4.1` | `17.4.2` |
| [globals](https://github.com/sindresorhus/globals) | `17.4.0` | `17.5.0` |
| [prettier](https://github.com/prettier/prettier) | `3.8.1` | `3.8.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.58.1` | `8.58.2` |
| [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) | `0.1.39` | `0.1.40` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.7.0` | `1.8.0` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.4` | `19.2.5` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.4` | `19.2.5` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.15.0` | `16.16.0` |


Updates `@microsoft/api-extractor` from 7.58.1 to 7.58.2
- [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md)
- [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-extractor_v7.58.2/apps/api-extractor)

Updates `@types/node` from 25.5.2 to 25.6.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `dotenv` from 17.4.1 to 17.4.2
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v17.4.1...v17.4.2)

Updates `globals` from 17.4.0 to 17.5.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.4.0...v17.5.0)

Updates `prettier` from 3.8.1 to 3.8.2
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.1...3.8.2)

Updates `typescript-eslint` from 8.58.1 to 8.58.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.58.2/packages/typescript-eslint)

Updates `@types/chrome` from 0.1.39 to 0.1.40
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome)

Updates `lucide-react` from 1.7.0 to 1.8.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.8.0/packages/lucide-react)

Updates `react` from 19.2.4 to 19.2.5
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.5/packages/react)

Updates `react-dom` from 19.2.4 to 19.2.5
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.5/packages/react-dom)

Updates `simple-icons` from 16.15.0 to 16.16.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.15.0...16.16.0)

---
updated-dependencies:
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.58.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: dotenv
  dependency-version: 17.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: globals
  dependency-version: 17.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: prettier
  dependency-version: 3.8.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.58.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/chrome"
  dependency-version: 0.1.40
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 1.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: react
  dependency-version: 19.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: react-dom
  dependency-version: 19.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.16.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 20:13:25 +00:00
dependabot[bot] 7a08022c72 chore(deps): bump actions/upload-pages-artifact
Bumps the github-actions group with 1 update: [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact).


Updates `actions/upload-pages-artifact` from 4 to 5
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 20:10:33 +00:00
Simon 78c829d955 fix: global helper functions redeclaration 2026-04-14 02:57:13 +08:00
Simon 5008345ce8 Merge pull request #425 from XePope/fix/autofixer-wait-fallback-format
fix(core): correct autoFixer wait fallback action format
2026-04-13 20:33:08 +08:00
Simon a1b7684bf2 Merge pull request #418 from lgy2020/fix/simulator-mask-raf-leak
fix(mask): dispose 后 requestAnimationFrame 循环未停止导致内存泄漏
2026-04-13 20:14:32 +08:00
Simon da67f3b07e chore(controller): .disposed guard 2026-04-13 20:13:41 +08:00
Simon c72ff95670 docs: update links in troubleshooting page 2026-04-13 17:21:26 +08:00
Simon d7a38abdbd chore: update tsconfig 2026-04-13 16:07:24 +08:00
Simon 6fed95b365 chore: enable workspace TypeScript SDK prompt 2026-04-13 15:23:35 +08:00
Simon 0099c1ca8b chore: upgrade wxt 2026-04-13 15:19:05 +08:00
Simon c5c7e8af55 chore: set ts SDK path 2026-04-12 04:29:26 +08:00
Simon 83476da97f docs: update agents.md 2026-04-12 03:39:15 +08:00
Simon 4b6b591849 Merge pull request #423 from alibaba/chore/update-dev-guide-and-pr-skill
chore: update dev guide and PR skill
2026-04-12 03:29:38 +08:00
Simon b5ec9c4406 Merge pull request #422 from alibaba/refactor/typescript-6
refactor: upgrade to TypeScript 6 with source-first monorepo resolution
2026-04-12 03:28:46 +08:00
Simon ca197d14e9 fix: dom typing 2026-04-12 03:16:39 +08:00
Simon acfb336a9a fix: pre-publish and post-publish hooks 2026-04-12 03:16:13 +08:00
Simon 4d27d49752 refactor(setup): upgrade to TypeScript 6 with source-first monorepo resolution 2026-04-12 02:04:21 +08:00
XePope b46693f525 fix(core): correct autoFixer wait fallback action format 2026-04-09 16:49:16 +08:00
Simon c3720ce6b3 chore(repo): update dev guide and PR skill 2026-04-09 03:51:13 +08:00
Simon f68c73c5e9 feat: add maintainer's skills; rm npmrc 2026-04-08 22:37:03 +08:00
Simon 58d992d875 Merge pull request #420 from alibaba/refactor/node-and-eslint-upgrade
refactor: upgrade Node and ESLint
2026-04-08 22:05:06 +08:00
Simon e9784a16d0 chore: upgrade deps 2026-04-08 22:00:29 +08:00
Simon ebe3a3f7c3 chore: rm eslint-config-prettier and eslint-plugin-react-refresh 2026-04-08 21:58:03 +08:00
Simon 9ebb629aca fix: wxt vite-plugin error 2026-04-08 21:16:29 +08:00
Simon 4f80ec1459 refactor: upgrade ESLint 9→10 and simplify React lint toolchain
- Upgrade eslint and @eslint/js to v10
- Replace eslint-plugin-react-x + eslint-plugin-react-dom + eslint-plugin-react-hooks
  with unified @eslint-react/eslint-plugin
- Raise dev Node.js requirement to ^22.13.0 || >=24 (runtime packages unaffected)
- Add .npmrc with engine-strict=true
- Move all @eslint-react rule overrides to eslint.config.js,
  eliminating plugin-specific inline eslint-disable comments
- Fix real issues caught by new rules: useless assignments,
  leaked setTimeout, ref naming, useState setter naming
2026-04-08 20:31:31 +08:00
liuguiyuan 9104064e8c fix(mask): stop requestAnimationFrame loop on dispose to prevent memory leak
The #moveCursorToTarget() method recursively schedules itself via
requestAnimationFrame, creating a continuous animation loop for the
AI cursor. However, dispose() only removes the DOM wrapper element
without stopping this loop, causing:

- CPU waste: rAF callback continues executing every frame (~60fps)
  after the mask is disposed, performing unnecessary calculations
  on a detached cursor element.
- Resource leak: Each SimulatorMask instance creates an unrecoverable
  animation loop that persists for the lifetime of the page.
- Console noise: style assignments to removed DOM nodes may produce
  browser warnings.

Fix: Add a #disposed boolean flag, checked at the top of
#moveCursorToTarget() to short-circuit the recursion. Set the flag
to true in dispose() before removing DOM elements.

Changes:
- Add #disposed field (default false)
- Guard #moveCursorToTarget() with early return when #disposed
- Set #disposed = true in dispose() before cleanup
2026-04-08 18:53:42 +08:00
Simon a43e653a74 chore: simplify issue config 2026-04-08 18:52:53 +08:00
Simon 3bffd76626 chore: update github templates 2026-04-08 18:49:16 +08:00
Simon 015101d6ae Merge pull request #416 from alibaba/dependabot/npm_and_yarn/development-dependencies-ffd7d36be5
chore(deps-dev): bump typescript-eslint from 8.58.0 to 8.58.1 in the development-dependencies group
2026-04-08 18:45:14 +08:00
dependabot[bot] 8904e602dd chore(deps-dev): bump typescript-eslint
Bumps the development-dependencies group with 1 update: [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `typescript-eslint` from 8.58.0 to 8.58.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.58.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.58.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-08 09:36:39 +00:00
Simon d384967e41 chore: dependabot config 2026-04-08 17:34:17 +08:00
Simon 531b59f538 chore: add git-cleanup skill 2026-04-08 17:28:11 +08:00
Simon bef26c3feb Merge pull request #415 from alibaba/fix/security-deps-hono-vite-path-to-regexp
fix(deps): update hono, path-to-regexp and vite for security patches
2026-04-08 17:21:17 +08:00
Simon c48dd538a0 fix(deps): update hono, @hono/node-server, path-to-regexp and vite for security patches
- hono 4.12.8 → 4.12.12 (cookie bypass, middleware bypass, path traversal, IP matching)
- @hono/node-server 1.19.11 → 1.19.13 (middleware bypass)
- path-to-regexp 8.3.0 → 8.4.2 (ReDoS)
- vite ^7.3.1 → ^7.3.2 (arbitrary file read, fs.deny bypass, path traversal)
2026-04-08 17:20:14 +08:00
Simon 950767c1bf Merge pull request #414 from alibaba/fix/panel-history-max-height-responsive
fix(ui): make panel history max-height responsive to viewport
2026-04-08 17:07:09 +08:00
Simon 12b6001d53 fix(ui): make panel history max-height responsive to viewport 2026-04-08 17:05:10 +08:00
Simon 2c1972ed89 Merge pull request #413 from alibaba/fix/ext-result-card-font-size
fix(ext): increase result card font size and use foreground color
2026-04-08 16:52:39 +08:00
Simon 362bf822f7 fix(ext): increase result card font size and use foreground color
Fixes conflicting text-xs/text-[11px] classes and muted color on the
ResultCard component, making the result text more readable.

Closes #360
2026-04-08 16:50:09 +08:00
dependabot[bot] 4734e2df26 chore(deps-dev): bump the development-dependencies group with 6 updates (#405) 2026-04-07 15:39:11 +08:00
Simon c2648c1749 chore(setup): update TypeScript SDK path in settings 2026-04-04 03:44:31 +08:00
Simon 7071a6c26a fix(mcp): bind HTTP+WS server to localhost only (#399) 2026-04-04 03:15:20 +08:00
Simon 303ece84fd feat: add update-changelog skill documentation 2026-04-04 03:14:37 +08:00
Simon 8a0266ce8d docs: update changelog for version 1.7.1 2026-04-04 03:14:23 +08:00
Simon cb55daf47d chore(version): bump version to 1.7.1 2026-04-04 01:37:49 +08:00
Simon 43415d3139 feat(website): sticky header 2026-04-04 01:34:54 +08:00
Simon 9cc9edf17e docs: lm studio (#398)
* docs(page): lm studio doc
* docs: update documentation and improve model section
2026-04-04 01:15:41 +08:00
Simon e290e2f67a Merge pull request #396 from alibaba/fix/interactive-aria-attrs-detection
fix(controller): detect elements with ARIA state attrs as interactive
2026-04-03 20:22:09 +08:00
Simon 4272939217 fix(controller): treat interactive with aria as distinct 2026-04-03 20:21:11 +08:00
Lubrsy 148bdb6839 fix: recognize role="listitem" as interactive element (#203)
* fix: recognize role="listitem" as interactive element
* fix: DISTINCT_INTERACTIVE_ROLES

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Simon <10131203+gaomeng1900@users.noreply.github.com>
2026-04-03 20:09:48 +08:00
Simon 68cee4cc64 Merge pull request #395 from alibaba/feat/controller-keep-semantic-tags
feat(controller): add experimental `keepSemanticTags` config
2026-04-03 19:07:36 +08:00
Simon 7e9027167d feat(controller): add keepSemanticTags config to keep the semantic structure of the page 2026-04-03 19:01:54 +08:00
Simon 6630774dc9 Merge pull request #390 from alibaba/fix/controller-improve-scrolling
feat(controller): improve scroll container detection and tool guidance
2026-04-03 17:29:38 +08:00
Simon 0ca1c8de0b feat(controller): consider hasScrollbarSignal when detecting scrollable 2026-04-03 16:09:35 +08:00
Simon 85a33ac1a4 feat(controller): improve scroll action 2026-04-02 22:05:47 +08:00
Simon b8fd1aaebc Merge pull request #389 from alibaba/fix/ext-postmessage-source-guard 2026-04-02 19:06:06 +08:00
Simon 13d952dc9d fix(ext): guard postMessage listeners against iframe sources
Add `e.source !== window` check to both content script and main-world
script message handlers, preventing iframes from injecting or
intercepting extension bridge messages.
2026-04-02 18:53:59 +08:00
Simon 1f2f5a44d3 Merge pull request #332 from mvanhorn/fix/scroll-direction-pixels
fix(page-controller): apply scroll direction to pixels parameter
2026-04-02 18:36:42 +08:00
Simon 8159aa58a6 Merge branch 'main' into fix/scroll-direction-pixels 2026-04-02 18:31:56 +08:00
Simon 2bdb3be81c Merge pull request #356 from lgy2020/fix/aria-attribute-detection
fix(isInteractiveCandidate): use hasAttribute with known aria list to detect aria- attributes
2026-04-02 17:41:36 +08:00
Simon bde630f55d chore(controller): add @edit mark 2026-04-02 17:38:02 +08:00
Simon 3efef0ec42 fix(controller): clean up INTERACTIVE_ARIA_ATTRS 2026-04-02 17:33:27 +08:00
Simon 1141287733 Merge pull request #386 from alibaba/feat/ext-execute-system-instruction
feat(ext): add `systemInstruction` to ExecuteConfig
2026-04-02 16:37:29 +08:00
Simon ec08b05da1 feat(ext): add systemInstruction to ExecuteConfig
Expose a serializable `systemInstruction` string field on the
page-facing ExecuteConfig, mapped to `instructions.system` when
creating MultiPageAgent. Functions cannot cross the postMessage
boundary, so this flat string field replaces the object form.

Closes #359
2026-04-02 16:34:57 +08:00
Simon 5ea2edd13c Merge pull request #384 from Anyexyz/main
fix: extract currentScript outside setTimeout to avoid null pointer
2026-04-02 16:20:17 +08:00
Anyexyz 1999bbeda9 fix: extract currentScript outside setTimeout to avoid null pointer 2026-04-02 16:17:46 +08:00
Simon b3ec571341 Merge pull request #385 from alibaba/docs/website-models-qwen3.6-plus
docs(website): add qwen3.6-plus to models page
2026-04-02 15:33:23 +08:00
Simon d0de8ad838 docs(website): add qwen3.6-plus to models page 2026-04-02 15:30:58 +08:00
Simon 0402a6be03 fix(tabs): error message typo 2026-03-31 21:04:53 +08:00
Simon 6823e04ca9 chore(version): bump version to 1.7.0 2026-03-31 20:45:12 +08:00
Simon 9ae1527201 Merge pull request #378 from alibaba/fix/controller-click-action-robust 2026-03-31 20:41:27 +08:00
Simon 2b20b48dff chore(controller): reuse pointer xy 2026-03-31 20:27:04 +08:00
Simon 296459924a feat(controller): enhance click action with elementFromPoint 2026-03-31 20:02:39 +08:00
Simon 8eee3b27e2 feat(controller): fix SimulatorMast mem leak; add passthrough events 2026-03-31 19:59:57 +08:00
Simon 32d6f0c74b fix(controller): click action robust 2026-03-31 17:41:49 +08:00
Simon 6e4094565a Merge pull request #369 from alibaba/dependabot/github_actions/github-actions-e5be0e1c7a
chore(deps): bump the github-actions group with 2 updates
2026-03-31 15:11:09 +08:00
Simon da2b7b5a8f Merge pull request #370 from alibaba/dependabot/npm_and_yarn/production-dependencies-90c90c5d9d
chore(deps): bump @modelcontextprotocol/sdk from 1.27.1 to 1.29.0 in the production-dependencies group
2026-03-31 15:09:37 +08:00
Simon 3207c3ffc1 Merge pull request #371 from alibaba/dependabot/npm_and_yarn/development-dependencies-003fb0370a 2026-03-31 12:22:35 +08:00
dependabot[bot] 784627a7c1 chore(deps-dev): bump the development-dependencies group with 3 updates
Bumps the development-dependencies group with 3 updates: [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint), [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `typescript-eslint` from 8.57.2 to 8.58.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.58.0/packages/typescript-eslint)

Updates `lucide-react` from 1.0.1 to 1.7.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.7.0/packages/lucide-react)

Updates `simple-icons` from 16.13.0 to 16.14.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.13.0...16.14.0)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.58.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 1.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.14.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 20:22:53 +00:00
dependabot[bot] 46d6c02c95 chore(deps): bump @modelcontextprotocol/sdk
Bumps the production-dependencies group with 1 update: [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk).


Updates `@modelcontextprotocol/sdk` from 1.27.1 to 1.29.0
- [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases)
- [Commits](https://github.com/modelcontextprotocol/typescript-sdk/compare/v1.27.1...v1.29.0)

---
updated-dependencies:
- dependency-name: "@modelcontextprotocol/sdk"
  dependency-version: 1.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 20:21:25 +00:00
dependabot[bot] 6d46719fe8 chore(deps): bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [actions/configure-pages](https://github.com/actions/configure-pages) and [actions/deploy-pages](https://github.com/actions/deploy-pages).


Updates `actions/configure-pages` from 5 to 6
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v5...v6)

Updates `actions/deploy-pages` from 4 to 5
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/deploy-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 20:21:07 +00:00
liuguiyuan f2b6c9dfd2 fix: use hasAttribute with known aria list for interactive candidate detection
Replace broken el.hasAttribute("aria-") with a curated list of 27
aria attributes checked via hasAttribute. Each check is O(1).

WAI-ARIA 1.2 defines ~50 aria attributes total per MDN.
Of these ~27 appear on interactive elements such as buttons,
inputs, sliders, and dialogs. The remaining ~20 are structural
container attributes like aria-live, aria-colcount, and
aria-rowspan that only appear on non-interactive containers.
Checking them would not change results.
2026-03-31 00:57:44 +08:00
Simon c465fd8aa4 chore(version): bump version to 1.6.3 2026-03-30 22:29:03 +08:00
Simon 1eef785a61 fix(ext): tabs deduplication 2026-03-30 22:18:55 +08:00
Simon 0b4eb6b49a docs(ext): experimentalIncludeAllTabs 2026-03-30 22:08:09 +08:00
Simon cba54967b8 Merge pull request #365 from alibaba/docs/simplify-contributing
docs: simplify docs
2026-03-30 22:02:11 +08:00
Simon 23dc3b7f7e Merge pull request #363 from alibaba/feat/ext-controll-all-tabs
fix(ext): MultiPageAgent inside content script can not detect new tabs
feat(ext): experimentalIncludeAllTabs - control all window tabs
2026-03-30 21:59:56 +08:00
Simon 49b137981c fix(ext): tab events do not work for content scripts 2026-03-30 21:40:16 +08:00
Simon cad033d63b feat(ext): add experimentalIncludeAllTabs UI 2026-03-30 20:25:04 +08:00
Simon 312952ec41 fix(ext): multi window errors 2026-03-30 20:24:24 +08:00
Simon 52edd78cd4 chore(ext): improve debug logging 2026-03-30 19:48:52 +08:00
Simon a7c9999d83 chore: mv security file 2026-03-28 19:21:20 +08:00
Simon 7f8ddde748 docs: move setup details to dev-guide; simplify readme and contributing 2026-03-28 19:11:54 +08:00
Simon e1fede1194 feat(ext): option to control all tabs 2026-03-27 20:18:13 +08:00
Simon 2322d6c46b docs: add awesome section 2026-03-27 19:32:04 +08:00
Simon ac72f55731 docs: simplify readme 2026-03-27 19:23:31 +08:00
Simon 5485e852bd Merge pull request #362 from 1245040330/fix-typing-animation
feat(extension): 禁用 EmptyState 组件的自动开始动画
2026-03-27 15:50:09 +08:00
1245040330 48fb65d00b feat(extension): 禁用 EmptyState 组件的自动开始动画
- 在 EmptyState 组件的动画配置中添加了 startOnView=false 属性
- 防止动画在视图加载时自动开始播放
- 优化了用户体验,避免不必要的动画干扰
2026-03-27 15:26:13 +08:00
Simon 75bc69c0c0 chore(version): bump version to 1.6.2 2026-03-25 01:05:20 +08:00
Simon 0d9b9e4b9a Merge pull request #341 from alibaba/dependabot/npm_and_yarn/development-dependencies-adf3994b6c
chore(deps-dev): bump the development-dependencies group with 7 updates
2026-03-25 00:56:05 +08:00
Simon 78a31ef5d1 fix: tslint 2026-03-25 00:50:29 +08:00
dependabot[bot] 98005e323a chore(deps-dev): bump the development-dependencies group with 7 updates
Bumps the development-dependencies group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.2.1` | `4.2.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.57.1` | `8.57.2` |
| [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) | `0.1.37` | `0.1.38` |
| [motion](https://github.com/motiondivision/motion) | `12.37.0` | `12.38.0` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.12.0` | `16.13.0` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.2.1` | `4.2.2` |
| [wxt](https://github.com/wxt-dev/wxt) | `0.20.19` | `0.20.20` |


Updates `@tailwindcss/vite` from 4.2.1 to 4.2.2
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.2/packages/@tailwindcss-vite)

Updates `typescript-eslint` from 8.57.1 to 8.57.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.57.2/packages/typescript-eslint)

Updates `@types/chrome` from 0.1.37 to 0.1.38
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome)

Updates `motion` from 12.37.0 to 12.38.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.37.0...v12.38.0)

Updates `simple-icons` from 16.12.0 to 16.13.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.12.0...16.13.0)

Updates `tailwindcss` from 4.2.1 to 4.2.2
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.2/packages/tailwindcss)

Updates `wxt` from 0.20.19 to 0.20.20
- [Release notes](https://github.com/wxt-dev/wxt/releases)
- [Commits](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.19...wxt-v0.20.20)

---
updated-dependencies:
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.57.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/chrome"
  dependency-version: 0.1.38
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: motion
  dependency-version: 12.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: wxt
  dependency-version: 0.20.20
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-24 16:28:24 +00:00
Simon f3c253c044 Merge pull request #340 from alibaba/dependabot/npm_and_yarn/production-dependencies-699df778d8
chore(deps): bump ws from 8.19.0 to 8.20.0 in the production-dependencies group
2026-03-25 00:25:53 +08:00
Simon 02f63ec04d Merge pull request #342 from alibaba/dependabot/npm_and_yarn/lucide-react-1.0.1
chore(deps-dev): bump lucide-react from 0.577.0 to 1.0.1
2026-03-25 00:25:41 +08:00
Ryanba 583f757f0a fix(ui): set task input max length to 1000 (#292) 2026-03-25 00:23:59 +08:00
Simon 53c9149d06 docs: add maintainer note 2026-03-24 17:42:56 +08:00
dependabot[bot] 64a90e3b3a chore(deps-dev): bump lucide-react from 0.577.0 to 1.0.1
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.577.0 to 1.0.1.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.0.1/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 19:17:32 +00:00
dependabot[bot] abfa903c9c chore(deps): bump ws in the production-dependencies group
Bumps the production-dependencies group with 1 update: [ws](https://github.com/websockets/ws).


Updates `ws` from 8.19.0 to 8.20.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.19.0...8.20.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 19:15:07 +00:00
Simon 6dd0ceab73 chore: README.md 2026-03-22 12:49:10 +08:00
Simon eede819ea8 chore(version): bump version to 1.6.1 2026-03-22 03:27:51 +08:00
Simon ce3047428a chore(mcp): opt descriptions 2026-03-22 03:27:17 +08:00
Simon 3456f45590 docs: MCP 2026-03-22 03:21:33 +08:00
Simon 4f71f065df Merge pull request #310 from zfangqijun/chore-export-actions
chore(page-controller): export actions as internal methods
2026-03-22 01:02:56 +08:00
Matt Van Horn 005bc8ab44 fix(page-controller): apply scroll direction to pixels parameter
Two bugs in the scroll direction logic:

1. Vertical scroll with `pixels` ignores the `down` boolean because the
   `??` operator bypasses the direction multiplier when pixels is provided.
   Fix: move the direction multiplier outside the `??` so it applies to
   both the pixels and numPages paths.

2. Horizontal scroll with `pixels` applies direction twice - once in
   PageController.ts and again in actions.ts - causing a double negation
   that reverses the intended direction. Fix: remove the redundant
   direction logic from actions.ts since PageController already signs
   the scroll amount.

Also removes the now-unused `down` and `right` parameters from the
scrollVertically() and scrollHorizontally() action functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 08:38:32 -07:00
zfangqijun cd2d33a9f6 chore(page-controller): export actions as internal methods 2026-03-21 20:17:01 +08:00
Simon 80e96d0b9e docs: limitations doc; homepage details 2026-03-21 02:53:00 +08:00
Simon 2c3b27e337 docs: changelog 2026-03-21 02:33:50 +08:00
Simon b6691b4732 chore(version): bump version to 1.6.0 2026-03-21 02:24:42 +08:00
Simon a557a85d28 Merge pull request #325 from alibaba/fix/action-into-iframes
fix(PageController): same-origin iframe actions
2026-03-21 02:20:13 +08:00
Simon eeb5b6a5af fix(clickElement): reorder iframe scroll and click actions 2026-03-21 02:18:42 +08:00
Simon ad31e2b750 chore: clarify current type handling in isHTMLElement 2026-03-21 02:16:51 +08:00
Simon e98d80b6a0 fix(PageController): same-origin iframe actions 2026-03-21 01:46:09 +08:00
Simon 93b1e52376 Merge pull request #322 from alibaba/feat/optional-toolchoice 2026-03-20 17:43:44 +08:00
Simon 66a85c0dd3 feat: option to disable named tool choice 2026-03-20 17:40:16 +08:00
Simon 50ce56a4f6 Merge pull request #321 from alibaba:chore/ext-rm-keydown
chore(ext): rm keydown event on history
2026-03-20 17:14:38 +08:00
Simon 5aeb40abac chore(ext): rm keydown event on history 2026-03-20 17:13:42 +08:00
Simon 8c06cdc898 Merge pull request #313 from Adonis0123/feature/239-history-export
feat(extension): export history sessions as json
2026-03-20 17:01:59 +08:00
Simon dfde89aa70 Merge branch 'main' into pr/Adonis0123/313 2026-03-20 16:58:13 +08:00
Simon 49b3316a5f Merge pull request #314 from Adonis0123/feat/issue-304-rerun-history-task
feat(ext): rerun tasks from history
2026-03-20 16:39:09 +08:00
Simon c35f367b39 feat(ext): style adjust; rm rerunDisabled 2026-03-20 16:36:23 +08:00
Simon 3459836a14 fix(PageController): lint error 2026-03-20 16:10:43 +08:00
Simon b7b5b6db30 Merge pull request #319 from alibaba/fix/missing-pointerout
fix(PageController): add `mouseleave` event
2026-03-20 16:05:37 +08:00
Simon 05d16313c7 fix(PageController): add mouseleave event 2026-03-20 15:54:36 +08:00
Simon a3a96d85d5 Merge pull request #210 from voidborne-d/fix/contenteditable-fallback
fix: add execCommand fallback for contenteditable input
2026-03-20 15:38:06 +08:00
Simon c89042f142 chore: wording 2026-03-20 15:36:10 +08:00
voidborne-d 2e18bd862d refactor: use const for planASucceeded, clarify LinkedIn comment 2026-03-20 07:08:45 +00:00
adonis 0bc47a997d feat(ext): rerun tasks from history 2026-03-19 23:54:08 +08:00
adonis bcc7dfea2d feat(extension): export history sessions as json 2026-03-19 23:46:58 +08:00
Simon ba242d3a1b Merge pull request #311 from alibaba/feat/llms-ak-always-optional
feat: optional AK
2026-03-19 20:08:15 +08:00
Simon fe82b11285 feat: AK optional 2026-03-19 19:50:05 +08:00
Simon bdf79b7c10 chore(version): bump version to 1.5.11 2026-03-18 20:45:19 +08:00
d 🔹 2f92a9cb32 fix: add execCommand fallback for contenteditable input (#168)
When typing into contenteditable elements (e.g. LinkedIn post editor),
the synthetic event approach (Plan A) may fail silently — the events
fire but the editor's internal state doesn't update, leaving the
element empty.

This adds an automatic fallback: after Plan A, we verify the text was
actually inserted by checking element.innerText. If it wasn't, we
fall back to execCommand('insertText') which integrates natively with
most rich-text editors including LinkedIn, Quill, and Slate.js.

The fallback uses proper Selection/Range API to select-all before
replacing, and preserves the undo stack since execCommand is handled
by the browser natively.

Fixes #168
2026-03-11 17:07:22 +00:00
162 changed files with 8695 additions and 6239 deletions
+64
View File
@@ -0,0 +1,64 @@
---
name: git-cleanup
description: Clean up local git branches and remotes accumulated from PR reviews. Use when the user asks to clean branches, remove stale remotes, or tidy up the local git state.
---
# Git Cleanup
Clean local branches (except `main` and current branch) and non-origin remotes.
Useful after reviewing multiple PRs that leave behind tracking branches and contributor remotes.
## Workflow
### 1. Survey
Run these in parallel:
```bash
git branch
git remote
git status --short
git stash list
```
Present a summary of how many branches and remotes will be removed.
### 2. Safety Checks — Ask Before Proceeding
**Stop and ask for confirmation if any of these are true:**
- Current branch is NOT `main`
- Working tree has uncommitted changes or stashes
- Any branch is ahead of its upstream
List unmerged branches separately and let the user decide.
### 3. Delete Branches
```bash
# Safe delete first (fails on unmerged branches)
git branch | grep -v '^\*' | grep -v '^\s*main$' | xargs git branch -d 2>&1
# Force-delete only with explicit user approval
git branch -D <branch>
```
### 4. Remove Remotes
```bash
git remote | grep -v '^origin$' | xargs -I{} git remote remove {}
```
### 5. Confirm
```bash
git branch && echo "---" && git remote
```
Report what was cleaned up.
## Key Rules
- **NEVER** delete `main` or the current checked-out branch
- **NEVER** force-delete unmerged branches without user confirmation
- If in doubt, ask
+104
View File
@@ -0,0 +1,104 @@
---
name: pre-impl-discussion
description: "Conduct a thorough pre-implementation discussion before making significant changes. Use when the user wants to discuss, plan, or evaluate a change before implementing it — especially when they say words like 'discuss', 'evaluate', 'plan', or 'let's talk about'."
argument-hint: 'Describe the change to evaluate'
---
# Pre-Implementation Discussion
Facilitate a structured, collaborative discussion to evaluate a proposed change **before any implementation begins**.
## Golden Rule
**Do NOT implement anything until the user explicitly confirms the final plan and says to proceed.** Not even "let me try on a branch" — that's implementation. The user will tell you when discussion is over.
## Interaction Style
- **Concise during discussion.** Each intermediate response should be short and focused on the current question. Do NOT repeat the full plan in every response.
- **Complete when finalizing.** Once the plan is mature and the user asks for it, present a single comprehensive (but terse) summary for final review.
- **Ask, don't assume.** When uncertain about project context, constraints, or preferences — ask. The user prefers collaborative discussion over receiving a pre-baked answer.
- **Challenge the premise.** Question whether the proposed change is the right one. Suggest simpler alternatives if they exist.
- **Match the user's language.** Reply in the same language the user writes in.
## Procedure
### 1. Understand the Current State
Before forming any opinion:
- **Read the relevant source files** — configs, code, docs that relate to the change
- **Understand the project structure** — what's published vs private, what environments things target, existing constraints
- **Map the impact surface** — which files, packages, or systems would be affected
- **Estimate implementation cost** — how many files to touch, how much config to rewrite, what could break
Do NOT skip this step. Do NOT rely on assumptions about what "most projects" do.
### 2. Research Thoroughly
- **Consult official sources** — docs, changelogs, migration guides, GitHub issues
- **Verify every claim with evidence.** Don't say "X supports Y" without a source. If unsure, say so.
- **Search in parallel** to save time — batch independent queries
Common pitfalls:
- Assuming compatibility without checking actual version constraints
- Confusing roadmap/aspirations with actual released state
- Missing transitive constraints (a dependency of a dependency)
### 3. Present Findings (Concise)
Share a **brief** assessment. Tables work well for comparisons. Highlight **blockers** and **unknowns** prominently — don't bury them in paragraphs.
### 4. Identify Key Decisions
Surface the decisions the user needs to make. Present them as clear choices with trade-offs, not as a recommendation monologue.
For each decision point:
- What are the options? (2-3 max)
- What does each option cost or give up?
- What's your lean and why? (one sentence)
### 5. Iterate
The user will ask follow-up questions, raise concerns, or challenge assumptions. For each round:
- **Research if needed** — don't answer from instinct when facts are available
- **Answer only what was asked** — don't re-present the entire plan
- **Update your mental model** based on user feedback
Common mistakes in this phase:
- Repeating the full plan after every small clarification
- Answering a narrow question with a broad redesign
- Treating user questions as confirmation to proceed
### 6. Finalize the Plan
When the user signals the discussion is converging, present the **complete plan once**:
- **What changes** — concrete list of actions (table or bullet points)
- **Impact surface** — what files/systems are affected, estimated effort
- **What does NOT change** — explicitly state scope boundaries
- **Risks or open items** — anything that needs testing or further verification
Keep it terse. Tables over paragraphs. No explanations the user already heard during discussion.
### 7. Wait for Confirmation
After presenting the final plan, **stop and wait**. The user will either:
- Confirm → then (and only then) proceed to implementation
- Ask more questions → go back to step 5
- Modify scope → update the plan and re-present
## Anti-Patterns
| Don't | Do Instead |
| --------------------------------------------------- | ---------------------------------------- |
| Start implementation "to test" without confirmation | Present findings and wait |
| Repeat the full plan in every response | Answer the specific question asked |
| Say "X should work" without checking | Say "I need to verify X" and research it |
| Assume project structure or constraints | Read the actual files |
| Present one recommendation as the only option | Present 2-3 options with trade-offs |
| Write long paragraphs explaining trade-offs | Use tables and bullet points |
@@ -0,0 +1,102 @@
---
name: submit-pr-from-current-changes
description: 'Create a branch, commit existing local changes, push them, and open a pull request. Use when submitting current work as a PR.'
argument-hint: 'Short summary of the current changes to submit'
user-invocable: true
---
# Submit PR From Current Changes
Turn a working tree diff into a clean branch, commit, and pull request.
## Hard Rules
1. **Follow the PR template exactly.** The template is at `.github/PULL_REQUEST_TEMPLATE.md`. Read it and copy its full structure into the PR body. Do NOT remove, reorder, or skip any section or checkbox.
2. **Never check "Requirements / 要求" checkboxes.** These are human-only declarations — the Code of Conduct acknowledgment and the AI authorship declaration can only be truthfully made by the human submitter. They MUST remain unchecked (`- [ ]`) in the PR body you generate. No exceptions, no workarounds, even if the user explicitly asks you to check them. The user goes to GitHub and checks them in person after verifying each statement is true.
3. **Only check Testing checkboxes you actually verified.** The "`npm run ci` passes" checkbox MAY be checked only if you ran `npm run ci` in this session and it passed. All other Testing checkboxes (browser tested, no console errors, types/doc added) require manual verification that only a human can perform — they MUST remain unchecked. You MAY add a supplementary note below the Testing section listing what automated validation you actually ran and the results.
4. **Never fabricate information.** Do not claim tests passed, commands ran, or checks succeeded unless you actually executed them and observed the output in this session. If you did not run it, do not mention it.
5. **PR output must be concise.** PR title: one line. "What" section: 12 sentences max. No walls of text, no redundant explanations. Let the diff speak.
## Prerequisites
Before attempting to push or open a PR, verify that the necessary tools are available:
- Check that `gh` CLI is installed and authenticated (`gh auth status`). If not available, stop and ask the user to install and authenticate GitHub CLI first.
- If the workflow uses MCP tools for GitHub operations, verify the MCP server is accessible.
## Procedure
1. **Read contribution rules.** Read `CONTRIBUTING.md` and any package-level instructions (e.g. `packages/*/AGENTS.md`) relevant to the changed files.
2. **Inspect the diff.** Review changed files, scope, and impact. Understand what the change does before writing anything.
3. **Inspect repo conventions.** Check recent branch names (`git branch -r --sort=-committerdate | head -20`), recent commit messages (`git log --oneline -20`), and the PR template.
4. **Pre-submission health checks** (run all before creating the PR):
- **Unified theme**: All changes should serve one purpose. If unrelated changes are mixed in, ask the user to split or confirm.
- **Commit hygiene**: Every new commit must follow the repo's conventional commit style. Squash or reword if needed.
- **Author identity**: Verify `git config user.name` and `git config user.email` are set and the email looks real (not empty, not `noreply` unless intentional). Warn the user if not.
- **No leftover artifacts**: Check for debug logs, `.only` in tests, conflict markers, or temp files in the diff.
- **CI**: Run `npm run ci` and make sure it passes. Record results honestly.
5. **Warn if the PR looks too hasty.** If any of these are true, pause and warn the user before proceeding:
- Large diff with no description of intent provided
- Changes touch core lib or extension (where vibe coding is prohibited per `CONTRIBUTING.md`)
- Multiple unrelated concerns in one diff
- No validation has been run at all
- Remind the user: _"This project does not accept low-quality or AI-generated PRs without meaningful human review. Please review your changes carefully."_
6. **Branch.** Check the current branch first:
- If already on a non-main feature branch with a valid name (matching `type/topic` convention), reuse it.
- If the branch name does not follow repo conventions (e.g. missing prefix, unclear topic), ask the user whether to rename or create a new one.
- If on `main` or a default branch, create a new branch from it with a short kebab-case name: prefix (`fix/`, `feat/`, `docs/`, `refactor/`, `chore/`) + concrete topic words.
7. **Stage and commit.** Stage only the intended files. Use `type(scope): subject` format. Keep the subject specific and compact.
8. **Push.** Push with `-u origin HEAD`.
9. **Open PR.** Use `gh pr create` with the full PR template structure. Fill in "What" and "Type" sections based on the actual diff. Check "`npm run ci` passes" only if it actually passed in this session; leave all other "Testing" and all "Requirements" checkboxes unchecked.
10. **Report results.** Tell the user: branch name, commit hash, PR link, and what validation actually ran (with pass/fail).
## Post-Submission Reminder
After successfully opening the PR, ALWAYS give a brief reminder in the user's language. Keep it concise and natural, but make sure it clearly tells the user:
1. They need to test the changes themselves in the browser.
2. They need to go to the PR page and check the Testing and Requirements checkboxes only after verifying each item.
3. The PR will not enter review until those checkboxes are checked.
4. The project does not accept autonomously AI-generated PRs, so they should only check the AI declaration if it is truthful.
## Branch Naming
- Prefer short kebab-case names with a repo-consistent prefix such as `fix/`, `feat/`, `docs/`, `refactor/`, or `chore/`.
- Match the change type first, then the smallest useful topic.
- Prefer concrete topic words over issue text dumps.
## Commit Style
- Use the repository's prevailing commit style (conventional commits).
- Use `type(scope): subject` when scope is clear from the changed area.
- Keep the subject specific and compact.
- If multiple commits exist on the branch, each one must independently follow conventions.
## Validation Strategy
- Default to enough validation to defend the PR, not the absolute minimum.
- Always run `npm run ci` (build, lint, typecheck, tests) before opening the PR. It must pass.
- Escalate to broader validation when the diff crosses packages, changes shared code, or affects release behavior.
- Never claim checks that did not actually run.
- You MAY note what you ran and the results below the Testing section in the PR body, but do NOT check the template checkboxes.
## Decision Points
- No local changes → stop, say so.
- `gh` CLI not available → stop, ask user to install it.
- Unrelated files mixed → stage only the intended subset or ask whether the work should be split.
- If the repo has area-specific instructions, read them before naming the branch or writing the PR.
- Change is small but high-risk → prefer broader validation.
- Change is narrowly scoped and low-risk → run the most relevant checks, not arbitrary unrelated ones.
- Template has declarations you cannot truthfully check → leave unchecked, tell the user why.
## Completion Checks
- The branch exists remotely and tracks upstream.
- The commit message(s) match repo style.
- All commit authors have proper name and email configured.
- The PR title matches the commit intent.
- The PR body follows the template structure completely.
- All "Requirements" checkboxes and all human-only "Testing" checkboxes are unchecked (`- [ ]`) in the PR body. The only checkbox you may check is "`npm run ci` passes", and only if it truly passed.
- The reported validation is accurate — nothing fabricated.
- The post-submission reminder was delivered in the user's language, concisely and accurately.
+109
View File
@@ -0,0 +1,109 @@
---
name: update-changelog
description: 'Update docs/CHANGELOG.md from git history, GitHub releases, and code diffs. Use when: writing release notes, syncing the latest changelog entry, summarizing a new tag, or keeping changelog wording concise and consistent.'
argument-hint: 'Describe the target, for example: latest version only'
---
# Update Changelog
Update `docs/CHANGELOG.md` from repository evidence instead of guesswork.
## When to Use
- Add the newest release entry to `docs/CHANGELOG.md`
- Sync changelog text with GitHub Releases
- Summarize the latest tag from git history or code diffs
## Defaults
- Keep the wording brief
- Only add the latest missing version unless the user explicitly asks to backfill older releases
- Prefer GitHub sources first, especially Releases and tag metadata
- Skip `Features` / `Improvements` / `Bug Fixes` headings when the release only has a few clear items
## Procedure
### 1. Read the local style
- Open `docs/CHANGELOG.md`
- Match the existing tone, bullet style, section ordering, and date format
### 2. Determine the target release
- Read the root `package.json` version and compare it with the top changelog entry
- Find the previous tag for the target version
- If the latest version is already documented, stop and report that no changelog update is needed
### 3. Gather evidence
Prefer these sources in order:
1. GitHub release notes
```bash
GH_PAGER=cat gh release view v<version> --repo <owner>/<repo> --json tagName,name,publishedAt,body
```
2. Tag date
```bash
git log -1 --format=%cs v<version>
```
3. Commit history between tags
```bash
git --no-pager log --format='%h %s' --no-merges v<previous>..v<version>
```
4. Diff scope when commit subjects are vague
```bash
git --no-pager diff --name-only v<previous>..v<version>
git --no-pager diff --stat v<previous>..v<version>
```
5. Read touched files directly only when the user-visible change is still unclear
### 4. Distill what belongs in the changelog
Include:
- User-visible features
- Important behavior changes
- Bug fixes that improve reliability, compatibility, or developer experience
- Small docs updates only when they materially change supported setups or onboarding
Exclude unless explicitly requested:
- Pure version bumps
- Routine dependency updates
- Internal refactors with no visible impact
- Mechanical formatting noise
### 5. Choose the structure
- If the release has 1 to 3 clear points, write flat bullets directly under the version heading
- If the release has several distinct items, use short headings such as `### Features`, `### Improvements`, and `### Bug Fixes`
- Do not force categories when they make the entry longer or noisier
### 6. Write the entry
- Add or update only the requested release entry
- Keep bullets short and concrete
- Reuse project terminology from nearby entries
- Avoid marketing language
- Do not copy noisy GitHub release text verbatim
### 7. Verify
- Check dates, version numbers, and tag boundaries
- Ensure Markdown structure matches nearby entries
- Confirm no intermediate versions were added unless requested
## Completion Checks
- The newest requested version is documented
- The wording is concise and consistent with the surrounding changelog
- The entry is backed by GitHub releases, git history, or code diff evidence
- Low-signal internal changes were left out
+24 -26
View File
@@ -1,61 +1,59 @@
name: Bug Report
description: Report a bug
title: '[Bug] '
labels: ['bug']
type: Bug
body:
- type: markdown
attributes:
value: |
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
感谢您对改进项目的兴趣!提交前请阅读我们的指南
Thanks for your interest in improving the project! Before submitting, please search for existing issues.
感谢参与我们的社区!请先搜索现有 Issue,避免重复提交
- [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)
> 为了让更多人参与讨论,请尽量使用英文提交 Issue,感谢理解 🙏
- type: textarea
id: description
attributes:
label: What happened?
placeholder: Describe the bug and expected behavior
label: What happened? / 问题描述
description: Describe what you expected vs what actually happened.
placeholder: 'A clear description of the bug.'
validations:
required: true
- type: textarea
id: code
id: reproduction
attributes:
label: Code
render: typescript
placeholder: Minimal reproduction code
label: How to reproduce / 如何复现
description: Provide steps, URL, config or code snippet to reproduce the bug.
placeholder: '1. Open URL: https://...'
validations:
required: false
required: true
- type: input
id: version
attributes:
label: Version
placeholder: '0.0.0'
- 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
placeholder: 'e.g. Chrome 130'
- type: checkboxes
id: community
id: checklist
attributes:
label: Community Communication / 社区沟通
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
label: Before submitting
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
- label: I have searched existing issues and this is not a duplicate.
required: true
validations:
required: true
-6
View File
@@ -1,11 +1,5 @@
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. 如何进行贡献与提交代码。
+9 -14
View File
@@ -1,32 +1,28 @@
name: Feature Request
description: Suggest a feature
title: '[Feature] '
labels: ['enhancement']
type: Feature
body:
- type: markdown
attributes:
value: |
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
感谢您对改进项目的兴趣!提交前请阅读我们的指南
Thanks for your interest in improving the project! Before submitting, please search for existing issues.
感谢参与我们的社区!请先搜索现有 Issue,避免重复提交
- [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)
> 为了让更多人参与讨论,请尽量使用英文提交 Issue,感谢理解 🙏
- type: textarea
id: description
attributes:
label: Feature Description / 功能描述
description: Describe the problem, solution, and any API changes. / 描述问题、解决方案以及 API 变更。
description: Describe the problem, solution, and any API changes.
placeholder: |
**Problem**:
What problem does this solve?
**Solution**:
How should this work?
**Proposed API**:
```typescript
// code here
```
validations:
required: true
@@ -34,14 +30,13 @@ body:
- type: checkboxes
id: community
attributes:
label: Community Communication / 社区沟通
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
label: Before submitting
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
- label: I have searched existing issues and this is not a duplicate.
required: true
validations:
required: true
+6 -8
View File
@@ -2,24 +2,22 @@
Brief description of changes.
Closes #(issue)
## Type
- [ ] Breaking change
- [ ] Bug fix
- [ ] Feature / Improvement
- [ ] Refactor
- [ ] Documentation
- [ ] Website
- [ ] Demo / Testing
- [ ] Breaking change
- [ ] Refactor / Chores
- [ ] Documentation / Website / Demo / Testing
## Testing
- [ ] `npm run ci` passes
- [ ] 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) . / 我已阅读并遵守行为准则。
+11 -4
View File
@@ -4,28 +4,35 @@ updates:
directory: '/'
schedule:
interval: 'weekly'
labels:
- 'dependencies'
groups:
# 生产依赖 - 小版本更新
production-dependencies:
dependency-type: 'production'
update-types:
- 'minor'
- 'patch'
# 开发依赖 - 小版本更新
development-dependencies:
dependency-type: 'development'
update-types:
- 'minor'
- 'patch'
# Major 更新单独处理(不分组,需要人工审查)
# 安全更新也不分组,Dependabot 会自动优先创建独立 PR
development-major:
dependency-type: 'development'
update-types:
- 'major'
# Production major updates are intentionally ungrouped for individual review.
# Security updates are also ungrouped — Dependabot prioritizes them automatically.
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
labels:
- 'dependencies'
groups:
github-actions:
patterns:
+9 -23
View File
@@ -1,41 +1,27 @@
name: CI
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
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Node.js ${{ matrix.node-version }}
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
node-version: 24
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
run: npm ci
- name: Lint
run: npx eslint . && npx prettier --check **/*.ts
- name: Build
run: npm run build
- name: CI checks
run: node scripts/ci.js
@@ -1,19 +1,20 @@
name: Deploy Demo
name: Deploy Website
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
runs-on: page-agent-trusted
permissions:
contents: read
pages: write
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -24,17 +25,17 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Build demo
- name: Build website
run: npm run build:website
- name: Setup Pages
uses: actions/configure-pages@v5
uses: actions/configure-pages@v6
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@v5
with:
path: './packages/website/dist'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
+30
View File
@@ -0,0 +1,30 @@
# Runs only for code already on main, on the self-hosted runner.
# PR-triggered CI must stay on GitHub-hosted runners (see ci.yml).
name: Main CI
permissions:
contents: read
on:
push:
branches: [main]
workflow_dispatch:
jobs:
test:
runs-on: page-agent-trusted
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: CI checks
run: node scripts/ci.js
+3 -1
View File
@@ -11,9 +11,11 @@ permissions:
jobs:
release:
# Only the repo owner may trigger a release, even if tag protection is bypassed
if: github.actor == 'gaomeng1900'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v6
+4 -2
View File
@@ -25,6 +25,9 @@ dist-ssr
*.sw?
.qoder
# publish backup
package.json.bak
# env files
.env
.env.*
@@ -34,8 +37,7 @@ dist-ssr
.wxt
# AI
.agent
.claude
.cursor
.gemini
CLAUDE.md
.codex
+8
View File
@@ -0,0 +1,8 @@
# Prompt templates (formatted manually for LLM readability)
*prompt.md
# Generated
packages/extension/.wxt
# Vendored
**/components/ui
+82
View File
@@ -0,0 +1,82 @@
{
"version": "0.2",
"language": "en",
"globRoot": "..",
"ignorePaths": [
"**/node_modules/**",
"**/dist/**",
"**/dist-ssr/**",
"**/.output/**",
"**/.wxt/**",
"**/*.svg"
],
"words": [
"activedescendant",
"adex",
"agentactivity",
"agentic",
"akldabonmimlicnjlflnapfeklbfemhj",
"aliyun",
"Bailian",
"Bento",
"BYOK",
"Chromewebstore",
"contenteditable",
"deepseek",
"executionresult",
"fieldsets",
"gaomeng",
"Googlechrome",
"Grabbable",
"Gregor",
"haspopup",
"highspeed",
"historicalevent",
"historychange",
"HITL",
"hrefs",
"IDBP",
"innerhtml",
"instructionsconfig",
"kimi",
"languagedetector",
"Lian",
"llms",
"magicui",
"maxlength",
"menuitemcheckbox",
"menuitemradio",
"modelcontextprotocol",
"multipageagent",
"nesw",
"npmmirror",
"Ollama",
"onwarn",
"opensource",
"openrouter",
"pageagent",
"pageagentcore",
"pageagentext",
"postpublish",
"qwen",
"reactid",
"reactroot",
"retryable",
"shadcn",
"sidepanel",
"spinbutton",
"staticity",
"statuschange",
"Supabase",
"toggleable",
"trivago",
"Uncapitalize",
"unparseable",
"valuenow",
"valuetext",
"WCAG",
"webgl",
"wouter",
"Zunic"
]
}
+4 -34
View File
@@ -1,39 +1,9 @@
{
"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
}
"editor.defaultFormatter": "esbenp.prettier-vscode",
"js/ts.tsdk.path": "node_modules/typescript/lib",
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
+24 -8
View File
@@ -5,7 +5,7 @@
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
- **Extension** (`packages/extension/`) - Browser extension (WXT + React)
- **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**
Internal packages:
@@ -18,18 +18,20 @@ Internal packages:
## 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
npm start # Start website dev server
npm run build # Build all packages
npm run build:libs # Build all libraries
npm run build:ext # Build and zip the extension package
npm run typecheck # Typecheck all packages
npm run test # Run unit tests across all workspaces
npm run lint # ESLint
```
## Architecture
### Monorepo Structure
Simple monorepo solution: TypeScript references + Vite aliases. Update tsconfig and vite config when adding/removing packages.
Source-first monorepo: library `package.json` exports point to `src/*.ts` during development. At publish time, `scripts/pre-publish.js` promotes `publishConfig` fields to top-level (swapping to `dist/`), and `scripts/post-publish.js` restores the originals.
```
packages/
@@ -37,7 +39,7 @@ packages/
├── 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)
├── extension/ # Browser extension
├── page-controller/ # @page-agent/page-controller
└── ui/ # @page-agent/ui
```
@@ -124,6 +126,20 @@ const pageInfo = await this.pageController.getPageInfo()
2. Expose via async method in `PageController.ts`
3. Export from `packages/page-controller/src/index.ts`
## Testing
- **Framework**: Vitest (unit tests only for now; future E2E goes to `packages/e2e/` with Playwright)
- **Location**: co-located, `src/foo.test.ts` next to `src/foo.ts`
- **Coverage today**: `packages/llms` only — other packages will follow incrementally
- **Adding tests to a new package**: create `vitest.config.ts` in the package and add a `"test": "vitest run"` script. Root `npm test` and `node scripts/ci.js` pick it up through npm workspaces.
- **Template**: See @page-agent/llms
```bash
npm test # all packages with a test script
npm test -w @page-agent/llms # single package
cd packages/llms && npx vitest # watch mode in one package
```
## Code Standards
- Explicit typing for exported/public APIs
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+13 -171
View File
@@ -2,42 +2,13 @@
♥️ 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.
For local development workflows, setup, local LLM config, extension development, testing on other websites, and more details, see [docs/developer-guide.md](docs/developer-guide.md).
## 🤝 How to Contribute
### Reporting Issues
> **[Maintainer's Note](https://github.com/alibaba/page-agent/issues/349)**
### Opening Issues
- Use the GitHub issue tracker to report bugs or request features
- Search existing issues before creating new ones
@@ -46,147 +17,24 @@ This is a **monorepo** with npm workspaces containing **4 main packages**:
### 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
1. Follow existing code style and patterns
2. Update documentation as needed
3. Add JSDoc for public APIs
4. Build and lint everything
5. Test in our demo website, and on other websites if applicable
6. Include screenshots for UI changes
### Vibe Coding with AI
> [Vibe coding](https://en.wikipedia.org/wiki/Vibe_coding)
- Vibe coding is **NOT** allowed for the core lib or the extension!!!
- 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.
- Make sure your AI references `AGENTS.md` and `website/AGENTS.md` for better quality.
- 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
@@ -194,12 +42,6 @@ Ask an AI to help you add documentation to the `website/` package. Follow the ex
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! 🎉
+39 -32
View File
@@ -1,19 +1,30 @@
# 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">
<source media="(prefers-color-scheme: dark)" srcset="https://page-agent.github.io/assets/readme/banner-dark.png">
<img alt="Page Agent Banner" src="https://page-agent.github.io/assets/readme/banner-light.png">
</picture>
[![License: MIT](https://img.shields.io/badge/License-MIT-auto.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
[![CI](https://img.shields.io/github/actions/workflow/status/alibaba/page-agent/main-ci.yml?branch=main&style=flat-square&label=ci)](https://github.com/alibaba/page-agent/actions/workflows/main-ci.yml)
[![npm](https://img.shields.io/npm/v/page-agent?style=flat-square&label=npm)](https://www.npmjs.com/package/page-agent)
[![downloads](https://img.shields.io/npm/dt/page-agent?style=flat-square)](https://www.npmjs.com/package/page-agent)
[![size](https://img.shields.io/bundlephobia/minzip/page-agent?style=flat-square&label=size)](https://bundlephobia.com/package/page-agent)
[![license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://opensource.org/licenses/MIT)
[![typescript](https://img.shields.io/badge/%3C%2F%3E-typescript-blue?style=flat-square)](http://www.typescriptlang.org/)
[![Chrome Web Store Rating](https://img.shields.io/chrome-web-store/rating/akldabonmimlicnjlflnapfeklbfemhj?style=flat-square&label=chrome%20rating)](https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj)
[![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
The GUI Agent Living in Your Webpage. One script gives any web page its own AI agent.
<a href="https://trendshift.io/repositories/22551?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-22551" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/22551" alt="alibaba%2Fpage-agent | Trendshift" width="180"/></a>
🌐 **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>
<!-- demo video -->
[![Watch the demo](https://page-agent.github.io/assets/readme/poster.jpg)](https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2)
---
@@ -25,15 +36,17 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
- **📖 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**
- Works with most mainstream models, including locally deployed ones. See [supported models](https://alibaba.github.io/page-agent/docs/features/models).
- **🐙 Optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension) for multi-page tasks.**
- And an [MCP Server (Beta)](https://alibaba.github.io/page-agent/docs/features/mcp-server) to control it from outside
## 💡 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).
- **Multi-page Agent** — Extend your own web agent's reach across browser tabs via the [Chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension).
- **MCP** - Allow your agent clients to control your browser.
## 🚀 Quick Start
@@ -42,15 +55,20 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
Fastest way to try PageAgent with our free Demo LLM:
```html
<script src="{URL}" crossorigin="true"></script>
<script
src="https://cdn.jsdelivr.net/npm/page-agent@1.12.0/dist/iife/page-agent.demo.js"
crossorigin="anonymous"
></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.5.10/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.5.10/files/dist/iife/page-agent.demo.js |
| Mirrors | URL |
| ------- | ----------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.12.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.12.0/files/dist/iife/page-agent.demo.js |
Add `?autoInit=false` to load the script without creating the demo agent automatically. You can then instantiate it with `new window.PageAgent(...)`.
### NPM Installation
@@ -75,11 +93,17 @@ For more programmatic usage, see [📖 Documentations](https://alibaba.github.io
## 🤝 Contributing
We welcome contributions from the community! Follow our instructions in [CONTRIBUTING.md](CONTRIBUTING.md) for setup and guidelines.
We welcome contributions from the community! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and [docs/developer-guide.md](docs/developer-guide.md) for local development workflows.
Please read [Code of Conduct](docs/CODE_OF_CONDUCT.md) before contributing.
Built something cool with PageAgent? Share it in [Show and Tell](https://github.com/alibaba/page-agent/discussions/categories/show-and-tell). 🙌
Contributions generated entirely by bots or agents without substantial human involvement will not be accepted, and bot accounts may be blocked.
Please read the [maintainer's note](https://github.com/alibaba/page-agent/issues/349) on principles and current state.
Contributions generated entirely by **bots or AI** without substantial human involvement will **not be accepted**.
## ⚖️ License
[MIT License](LICENSE)
## 👏 Acknowledgments
@@ -97,23 +121,6 @@ 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>
+191 -36
View File
@@ -5,6 +5,197 @@ 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.12.0] - 2026-07-09
- **Stateless extension tab sync** - TabsController pulls tab state on demand instead of long-lived ports, so the MV3 service worker stays stateless and survives idle kills without stalling the side panel.
- **Tab status in agent context** - Tab summaries now include each tab's loading status for the LLM.
## [1.11.0] - 2026-07-03
### Features
- **LLM model patching rework** - Rewrote per-model request patching for GPT, Claude, Qwen, Gemini, and DeepSeek, and refreshed the recommended model list.
### Improvements
- **Deprecate `LLMConfig.temperature`** - Warn when set; use `transformRequestBody` for models that still accept it.
- **Extension limitations docs** - Added a limitations section to extension documentation.
### Bug Fixes
- **Multi-window extension tabs** - Resolved the active tab/window from the caller's window context instead of the global active tab.
- **chat-latest model compatibility** - Skip `reasoning_effort` and `temperature` patches for `*-chat-latest` models.
- **OpenRouter defaults** - Do not enable reasoning by default on OpenRouter.
- **Asset URLs** - Migrated `img.alicdn.com` links to `page-agent.github.io`.
## [1.10.0] - 2026-06-15
### Breaking Changes
- **Agent run lifecycle rework** - `stop()` is now async and resolves only after the run fully settles. Run status is decoupled from task outcome: a new `stopped` state was added, and LLM self-reported failures now end as `completed`. Lifecycle hooks re-throw instead of folding errors into the result, agent errors are recorded in history, and `agent.lastResult` was added.
### Features
- **Abortable JavaScript execution** - `execute_javascript` now honors the `AbortSignal`.
- **Leaner agent prompts** - Simplified the waiting-response flow and removed navigation-back instructions to reduce LLM cognitive load.
- **MultiPageAgent safety** - Disabled `ScriptExecutionTool` for `MultiPageAgent`.
### Improvements
- **Dark mode detection** - Refined detection heuristics and made `isMainContentDark` less aggressive by checking `html` and `body` data attributes independently.
- **Extension lifecycle robustness** - Drove heartbeat and running state from status changes, cleared stale activity on any non-running status, handled the stopped lifecycle state, and cleared `currentTabId` on `TabsController.init`.
### Bug Fixes
- **Accurate wait reporting** - Wait steps now report the actual wait duration.
- **Scroll predicates** - Scroll predicates now return booleans.
- **Docs** - Fixed the broken demo video on GitHub.
## [1.9.0] - 2026-06-08
### Features
- **Robust abort handling** - Rewrote the aborting system; sync tools, loop execution, and LLM clients now correctly respect task abort signals (`ctx.signal`). Also decoupled `AbortError` from `InvokeError` in `@page-agent/llms`.
- **Claude Opus 4.8 support** - Added support for Claude Opus 4.8 model.
### Improvements
- **Concurrency guard** - Prevented concurrent `execute()` calls on a single PageAgent/Core instance to avoid race conditions.
- **Model recommendations refresh** - Updated default and tested model list recommendations.
- **Test coverage** - Added comprehensive Vitest unit tests for the `@page-agent/llms` package.
- **Improved documentation** - Added website documentation for the `ctx.signal` abort contract and `execute()` concurrency rules.
### Bug Fixes
- **DTS bundle fix** - Fixed a packaging bug where global type declarations were incorrectly bundled into `.d.ts` outputs.
- **Website sidebar fix** - Normalized trailing slashes in the website's sidebar location comparison.
## [1.8.2] - 2026-05-11
### Features
- **IIFE demo control** - Added `showPanel` and `autoInit` switches to the IIFE CDN script to control whether the UI panel automatically displays or initializes on load.
### Improvements
- **Build toolchain modernization** - Upgraded build infrastructure to Vite 8.
### Bug Fixes
- **TypeScript `InvokeErrorType` fix** - Separated the value and type space for `InvokeErrorType` to resolve TypeScript compilation issues.
- **Website chunking fix** - Restored working code-splitting with `manualChunks` for the documentation website.
## [1.8.1] - 2026-04-27
### Features
- **GPT-5.4 & Qwen 3.6 support** - Added support for `gpt-5.4` and `qwen3.6-max/flash` in the recommended LLM list.
- **Custom LLM request hook** - Added a `transformRequestBody` hook to allow custom modification of payloads before sending requests to LLM providers.
### Improvements
- **Accessibility (a11y) enhancements** - Added descriptive accessible labels to `ConfigPanel` input fields and icon buttons in compliance with WCAG 4.1.2.
- **UI polish** - Improved `HistoryList` loading and empty states, and added helpful tooltips for actions.
- **Prompt caching guidance** - Added website documentation for prompt caching optimization.
- **Build speedups** - Added parallel build scripts to accelerate local development compilation.
### Bug Fixes
- **DeepSeek tool choice fix** - Disabled explicit `tool_choice` for DeepSeek models to avoid API compatibility errors.
- **MCP version advertising** - MCP server now correctly advertises its package version.
## [1.8.0] - 2026-04-15
### Breaking Changes
- **TypeScript 6 & ESLint 10 upgrade** - Major toolchain modernization. Upgraded the entire monorepo to TypeScript 6 and ESLint 10 with source-first monorepo resolution (library exports resolve to source files directly during local development).
### Improvements
- **MCP security hardening** - Bound the MCP HTTP + WebSocket server to `localhost` only.
- **Extension UI refinement** - Made the history panel height responsive to the viewport and improved the result card readability by increasing font size.
### Bug Fixes
- **SimulatorMask memory leak** - Fixed a memory leak by ensuring the `requestAnimationFrame` loop is cancelled when disposing the SimulatorMask.
- **Autofixer format fix** - Corrected the fallback action format for `autoFixer` when waiting.
- **IIFE scope protection** - Fixed a name collision in IIFE builds by preventing global helper function re-declarations.
## [1.7.1] - 2026-04-04
### Features
- **Optional `keepSemanticTags`** - Added an experimental `keepSemanticTags` config to preserve semantic structure in PageController output
- **Per-task extension system instructions** - Extension `ExecuteConfig` now supports `systemInstruction`
### Improvements
- **Smarter scroll handling** - Scroll container detection and scroll direction handling are more reliable
- **Better accessibility-aware element detection** - Interactive candidates with supported ARIA attributes and `role="listitem"` are recognized more accurately
### Bug Fixes
- Fixed iframe-origin filtering for extension `postMessage` listeners
- Avoided a `currentScript` null pointer during deferred initialization
## [1.7.0] - 2026-03-31
- **More reliable click actions** - Click handling now reuses pointer coordinates, verifies targets with `elementFromPoint`, and behaves better on layered layouts
- **Better mask event handling** - `SimulatorMask` now supports passthrough events when automation should not fully swallow input
- Fixed a `SimulatorMask` memory leak
## [1.6.3] - 2026-03-30
### Features
- **Experimental all-tabs control** - Extension can include and control all browser tabs via `experimentalIncludeAllTabs`
### Improvements
- **Calmer empty state motion** - Disabled the EmptyState auto-start animation in the extension UI
- **Cleaner extension docs** - Simplified setup and tab-control documentation across the README and developer guide
### Bug Fixes
- Fixed new-tab detection from content scripts
- Fixed tab deduplication and multi-window handling in the extension
## [1.6.2] - 2026-03-25
- **Longer task input** - The UI task input now accepts up to 1000 characters
- **Contributor docs refresh** - Added a maintainer note and refreshed contributor-facing documentation
- Fixed lint issues in the release pipeline
## [1.6.1] - 2026-03-22
- **Internal PageController action exports** - PageController actions are now exposed as internal methods for easier reuse across packages
- **Expanded docs** - Added MCP docs and clarified project limitations and homepage details
## [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
@@ -153,42 +344,6 @@ PageAgent is now ready for production use. The API is stable and breaking change
- **Ask User Tool** - Agent can ask users for clarification
- **i18n Support** - English and Chinese localization
### Configuration
```typescript
// Version 1.0.0
interface PageAgentConfig {
// LLM Configuration (required)
baseURL: string
apiKey: string
model: string
temperature?: number
maxRetries?: number
customFetch?: typeof fetch
// Agent Configuration
language?: 'en-US' | 'zh-CN'
maxSteps?: number // default: 20
customTools?: Record<string, PageAgentTool> // experimental
instructions?: InstructionsConfig
transformPageContent?: (content: string) => string | Promise<string>
experimentalScriptExecutionTool?: boolean // default: false
// Lifecycle Hooks (experimental)
onBeforeTask?: (agent, result) => void
onAfterTask?: (agent, result) => void
onBeforeStep?: (agent, stepCount) => void
onAfterStep?: (agent, history) => void
onDispose?: (agent, reason?) => void
// Page Controller Configuration
enableMask?: boolean // default: true
viewportExpansion?: number
interactiveBlacklist?: Element[]
interactiveWhitelist?: Element[]
}
```
### Packages
| Package | Description |
+23 -23
View File
@@ -16,22 +16,22 @@ appearance, race, religion, or sexual identity and orientation.
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
- 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
- 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
@@ -85,19 +85,19 @@ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.ht
有助于创造正面环境的行为包括但不限于:
* 使用友好和包容性语言
* 尊重不同的观点和经历
* 耐心地接受建设性批评
* 关注对社区最有利的事情
* 友善对待其他社区成员
- 使用友好和包容性语言
- 尊重不同的观点和经历
- 耐心地接受建设性批评
- 关注对社区最有利的事情
- 友善对待其他社区成员
身为参与者不能接受的行为包括但不限于:
* 使用与性有关的言语或是图像,以及不受欢迎的性骚扰
* 捣乱/煽动/造谣的行为或进行侮辱/贬损的评论,人身攻击及政治攻击
* 公开或私下的骚扰
* 未经许可地发布他人的个人资料,例如住址或是电子地址
* 其他可以被合理地认定为不恰当或者违反职业操守的行为
- 使用与性有关的言语或是图像,以及不受欢迎的性骚扰
- 捣乱/煽动/造谣的行为或进行侮辱/贬损的评论,人身攻击及政治攻击
- 公开或私下的骚扰
- 未经许可地发布他人的个人资料,例如住址或是电子地址
- 其他可以被合理地认定为不恰当或者违反职业操守的行为
## 我们的责任
+42 -34
View File
@@ -1,39 +1,51 @@
# 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">
<source media="(prefers-color-scheme: dark)" srcset="https://page-agent.github.io/assets/readme/banner-dark.png">
<img alt="Page Agent Banner" src="https://page-agent.github.io/assets/readme/banner-light.png">
</picture>
[![License: MIT](https://img.shields.io/badge/License-MIT-auto.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
[![CI](https://img.shields.io/github/actions/workflow/status/alibaba/page-agent/main-ci.yml?branch=main&style=flat-square&label=ci)](https://github.com/alibaba/page-agent/actions/workflows/main-ci.yml)
[![npm](https://img.shields.io/npm/v/page-agent?style=flat-square&label=npm)](https://www.npmjs.com/package/page-agent)
[![downloads](https://img.shields.io/npm/dt/page-agent?style=flat-square)](https://www.npmjs.com/package/page-agent)
[![size](https://img.shields.io/bundlephobia/minzip/page-agent?style=flat-square&label=size)](https://bundlephobia.com/package/page-agent)
[![license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://opensource.org/licenses/MIT)
[![typescript](https://img.shields.io/badge/%3C%2F%3E-typescript-blue?style=flat-square)](http://www.typescriptlang.org/)
[![Chrome Web Store Rating](https://img.shields.io/chrome-web-store/rating/akldabonmimlicnjlflnapfeklbfemhj?style=flat-square&label=chrome%20rating)](https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj)
[![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
<a href="https://trendshift.io/repositories/22551?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-22551" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/22551" alt="alibaba%2Fpage-agent | Trendshift" width="180"/></a>
🌐 [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>
<!-- demo video -->
[![Watch the demo](https://page-agent.github.io/assets/readme/poster.jpg)](https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2)
---
## ✨ Features
- **🎯 轻松集成**
- 无需 `浏览器插件` / `Python` / `无头浏览器`
- 纯页面内 JavaScript,一切都在你的网页中完成。
- 无需 `浏览器插件` / `Python` / `无头浏览器`,纯页面内 JavaScript
- **📖 基于文本的 DOM 操作**
- 无需截图,无需多模态模型或特殊权限
- **🧠 用你自己的 LLM**
- **🎨 精美 UI,支持人机协同**
- **🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),支持跨页面任务。**
- 无需截图,无需多模态模型或特殊权限
- **🧠 自备 LLM**
- 支持多数主流模型,包括本地部署模型。参见[支持的模型](https://alibaba.github.io/page-agent/docs/features/models)。
- 🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),支持跨页面任务
- [MCP Server (Beta)](https://alibaba.github.io/page-agent/docs/features/mcp-server)
## 💡 应用场景
- **SaaS AI 副驾驶** — 几行代码为你的产品加上 AI 副驾驶,无需重写后端。
- **SaaS AI Copilot** — 几行代码为你的产品加上 AI 副驾驶,无需重写后端。
- **智能表单填写** — 把 20 次点击变成一句话。ERP、CRM、管理后台的最佳拍档。
- **无障碍增强** — 用自然语言让任何网页无障碍。语音指令、屏幕阅读器,零门槛。
- **跨页面 Agent** — 通过可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),让你自己的 Agent 跨标签页工作。
- **跨页面 Agent** — 通过可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),让你自己的 Web Agent 跨标签页工作。
- 通过 MCP 为现有 Agent 加入浏览器控制能力。
## 🚀 快速开始
@@ -42,15 +54,20 @@
通过我们免费的 Demo LLM 快速体验 PageAgent
```html
<script src="{URL}" crossorigin="true"></script>
<script
src="https://cdn.jsdelivr.net/npm/page-agent@1.12.0/dist/iife/page-agent.demo.js"
crossorigin="anonymous"
></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.5.10/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.5.10/files/dist/iife/page-agent.demo.js |
| Mirrors | URL |
| ------- | ----------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.12.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.12.0/files/dist/iife/page-agent.demo.js |
在 URL 后添加 `?autoInit=false` 可只加载脚本,不自动创建 Demo Agent;之后可通过 `new window.PageAgent(...)` 手动初始化。
### NPM 安装
@@ -75,11 +92,15 @@ await agent.execute('点击登录按钮')
## 🤝 贡献
欢迎社区贡献!请参阅 [CONTRIBUTING.md](../CONTRIBUTING.md) 了解安装与贡献指南。请在贡献前阅读[行为准则](CODE_OF_CONDUCT.md)。
欢迎社区贡献!请参阅 [CONTRIBUTING.md](../CONTRIBUTING.md) 了解安装与贡献指南。
我们不接受未经实质性人类参与、完全由 Bot 或 Agent 自动生成的代码,机器人账号可能被禁止参与互动
提交 issue 或 PR 之前,请先阅读[作者声明](https://github.com/alibaba/page-agent/issues/349)和[行为准则](CODE_OF_CONDUCT.md)
## 👏 致谢
我们不接受未经实质性人类参与、完全由 Bot 或 Agent 自动生成的代码。
用 PageAgent 做了有趣的东西?欢迎到 [Show and Tell](https://github.com/alibaba/page-agent/discussions/categories/show-and-tell) 分享。🙌
## 👏 声明与致谢
本项目基于 **[`browser-use`](https://github.com/browser-use/browser-use)** 的优秀工作构建。
@@ -95,23 +116,10 @@ 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>
View File
+105
View File
@@ -0,0 +1,105 @@
# Developer Guide
This file is for local development workflows.
For contribution rules and expectations, see [../CONTRIBUTING.md](../CONTRIBUTING.md).
## 🚀 Quick Start
### Development Setup
1. **Prerequisites**
- `macOS` / `Linux` / `WSL`
- `node.js ^22.13 || >=24` with `npm >= 11`
- 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 # Or `npm ci` if you don't want to change the lockfile
npm start # Start website dev server
npm run build # Build everything
```
## 📦 Project Structure
This is a **monorepo** with npm workspaces.
Published packages:
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel (npm: `page-agent`)
- **MCP** (`packages/mcp/`) - MCP server for browser control via Page Agent extension (npm: `@page-agent/mcp`)
- **Core** (`packages/core/`) - Core agent logic 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, independent of LLM
- **UI** (`packages/ui/`) - Panel and i18n, decoupled from PageAgent
Applications:
- **Extension** (`packages/extension/`) - Browser extension (WXT + React)
- **Website** (`packages/website/`) - React docs, landing page, and dev playground (private)
> Source-first monorepo with `npm workspaces + ts references + vite alias`. Library `package.json` exports point to `src/*.ts` during development, and point to `dist/*.js` when published. `workspaces` in root `package.json` must be in topological order.
## 🤖 AGENTS.md Alias
If your AI assistant does not support [AGENTS.md](https://agents.md/). Add an alias for it.
## 🔧 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](./terms-and-privacy.md).
### Extension Development
```bash
npm run dev:ext
npm run build: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?lang=en-US&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.
+7 -29
View File
@@ -1,8 +1,5 @@
import eslintReact from '@eslint-react/eslint-plugin'
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'
@@ -15,44 +12,24 @@ export default defineConfig([
'**/.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,
eslintReact.configs['recommended-typescript'],
],
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',
@@ -72,14 +49,15 @@ export default defineConfig([
'@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',
'@typescript-eslint/no-deprecated': 'off',
'@eslint-react/dom-no-missing-button-type': 'off',
'@eslint-react/no-nested-component-definitions': 'off',
'@eslint-react/no-array-index-key': 'off',
'@eslint-react/dom-no-dangerously-set-innerhtml': 'off',
},
},
])
+3325 -3709
View File
File diff suppressed because it is too large Load Diff
+37 -32
View File
@@ -1,7 +1,7 @@
{
"name": "root",
"private": true,
"version": "1.5.10",
"version": "1.12.0",
"type": "module",
"workspaces": [
"packages/page-controller",
@@ -22,52 +22,54 @@
},
"homepage": "https://alibaba.github.io/page-agent/",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
"node": "^22.22.1 || >=24",
"npm": "^11.6.3"
},
"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": "node scripts/build.js",
"build:libs": "node scripts/build-libs.js",
"build:website": "npm run build:website --workspace=@page-agent/website",
"build:ext": "npm run build:libs && npm run zip -w @page-agent/ext",
"build:ext": "npm run zip -w @page-agent/ext",
"version": "node scripts/sync-version.js",
"postpublish": "npm run postpublish --workspaces --if-present",
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json && tsc --noEmit -p packages/extension/tsconfig.json",
"test": "npm test --workspaces --if-present",
"lint": "eslint .",
"cleanup": "rm -rf packages/*/dist",
"prepare": "husky"
"ci": "node scripts/ci.js",
"cleanup": "rm -rf packages/*/dist && rm -rf packages/*/.output",
"prepare": "husky || true"
},
"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",
"@commitlint/cli": "^21.2.0",
"@commitlint/config-conventional": "^21.2.0",
"@eslint-react/eslint-plugin": "^5.11.2",
"@eslint/js": "^10.0.1",
"@microsoft/api-extractor": "^7.58.9",
"@tailwindcss/vite": "^4.3.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.5.0",
"@vitejs/plugin-react-swc": "^4.3.0",
"@types/node": "^26.1.0",
"@vitejs/plugin-react": "^6.0.3",
"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",
"concurrently": "^10.0.3",
"dotenv": "^17.4.2",
"eslint": "^10.6.0",
"globals": "^17.7.0",
"happy-dom": "^20.10.6",
"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"
"lint-staged": "^17.0.8",
"prettier": "^3.9.4",
"typescript": "^6.0.3",
"typescript-eslint": "^8.63.0",
"unplugin-dts": "^1.0.3",
"vite": "^8.1.3",
"vite-plugin-css-injected-by-js": "^5.0.1",
"vitest": "^4.1.10"
},
"overrides": {
"typescript": "^5.9.3"
"typescript": "^6.0.3"
},
"lint-staged": {
"*.{js,ts,cjs,cts,mjs,mts}": [
@@ -80,6 +82,9 @@
],
"*.css": [
"npx prettier --write --ignore-unknown"
],
"*.md": [
"npx prettier --write --ignore-unknown"
]
},
"commitlint": {
+23 -12
View File
@@ -1,16 +1,26 @@
{
"name": "@page-agent/core",
"private": false,
"version": "1.5.10",
"version": "1.12.0",
"type": "module",
"main": "./dist/esm/page-agent-core.js",
"module": "./dist/esm/page-agent-core.js",
"types": "./dist/esm/PageAgentCore.d.ts",
"main": "./src/PageAgentCore.ts",
"types": "./src/PageAgentCore.ts",
"exports": {
".": {
"types": "./dist/esm/PageAgentCore.d.ts",
"import": "./dist/esm/page-agent-core.js",
"default": "./dist/esm/page-agent-core.js"
"types": "./src/PageAgentCore.ts",
"default": "./src/PageAgentCore.ts"
}
},
"publishConfig": {
"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": [
@@ -38,19 +48,20 @@
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"test": "vitest run",
"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{}})\""
"prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node ../../scripts/post-publish.js"
},
"dependencies": {
"chalk": "^5.6.2",
"@page-agent/llms": "1.5.10",
"@page-agent/page-controller": "1.5.10"
"@page-agent/llms": "1.12.0",
"@page-agent/page-controller": "1.12.0"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"zod": "^4.3.5"
"zod": "^4.4.3"
}
}
+365
View File
@@ -0,0 +1,365 @@
import type { BrowserState, PageController } from '@page-agent/page-controller'
import { describe, expect, it, vi } from 'vitest'
import * as z from 'zod/v4'
import { PageAgentCore, tool } from './PageAgentCore'
import type { ExecutionResult } from './types'
type TestFetch = (...args: Parameters<typeof globalThis.fetch>) => Promise<Response>
function agentResponse(args: unknown): Response {
return new Response(
JSON.stringify({
choices: [
{
finish_reason: 'tool_calls',
message: {
tool_calls: [
{
function: {
name: 'AgentOutput',
arguments: JSON.stringify(args),
},
},
],
},
},
],
usage: {},
})
)
}
function createPageController(): PageController {
const browserState: BrowserState = {
url: 'https://example.test/',
title: 'Test page',
header: '',
content: '',
footer: '',
}
return {
showMask: vi.fn(async () => {}),
hideMask: vi.fn(),
cleanUpHighlights: vi.fn(),
getLastUpdateTime: vi.fn(() => Date.now()),
getBrowserState: vi.fn(async () => browserState),
dispose: vi.fn(),
} as unknown as PageController
}
function createAgent(
customFetch: TestFetch,
options: Partial<ConstructorParameters<typeof PageAgentCore>[0]> = {}
): PageAgentCore {
return new PageAgentCore({
baseURL: 'https://llm.test',
model: 'test-model',
maxRetries: 0,
stepDelay: 0,
customFetch,
customSystemPrompt: 'test',
pageController: createPageController(),
...options,
})
}
function createFetchMock() {
return vi.fn<TestFetch>()
}
function onceActivity(
agent: PageAgentCore,
predicate: (detail: unknown) => boolean
): Promise<void> {
return new Promise((resolve) => {
const onActivity = (event: Event) => {
if (!predicate((event as CustomEvent).detail)) return
agent.removeEventListener('activity', onActivity)
resolve()
}
agent.addEventListener('activity', onActivity)
})
}
function isExecutingTool(detail: unknown, toolName: string): boolean {
return (
typeof detail === 'object' &&
detail !== null &&
'type' in detail &&
'tool' in detail &&
detail.type === 'executing' &&
detail.tool === toolName
)
}
function doneResponse(text: string, success = true): Response {
return agentResponse({ action: { done: { text, success } } })
}
function waitResponse(seconds = 10): Response {
return agentResponse({ action: { wait: { seconds } } })
}
/**
* Start a task that blocks on `wait`, returning once the tool is executing.
* The running promise is wrapped so awaiting this helper does not await the task.
*/
async function startBlockedTask(
agent: PageAgentCore,
task = 'first'
): Promise<{ result: Promise<ExecutionResult> }> {
const waitStarted = onceActivity(agent, (detail) => isExecutingTool(detail, 'wait'))
const result = agent.execute(task)
await waitStarted
return { result }
}
describe.concurrent('PageAgentCore lifecycle', () => {
describe('normal execution', () => {
it('runs a task to natural completion', async () => {
const fetchMock = createFetchMock().mockResolvedValueOnce(doneResponse('all done'))
const agent = createAgent(fetchMock)
const result = await agent.execute('do something')
expect(result).toMatchObject({ success: true, data: 'all done' })
expect(agent.status).toBe('completed')
expect(fetchMock).toHaveBeenCalledTimes(1)
})
it('completes (not errors) when the LLM reports task failure', async () => {
const fetchMock = createFetchMock().mockResolvedValueOnce(doneResponse('gave up', false))
const agent = createAgent(fetchMock)
const result = await agent.execute('do something')
expect(result).toMatchObject({ success: false, data: 'gave up' })
expect(agent.status).toBe('completed')
})
it('throws when a task is already running', async () => {
const fetchMock = createFetchMock().mockResolvedValueOnce(waitResponse())
const agent = createAgent(fetchMock)
const { result } = await startBlockedTask(agent)
await expect(agent.execute('second')).rejects.toThrow('A task is already running.')
await agent.stop()
await result
})
})
describe('stop', () => {
it('aborts the running task and keeps the agent reusable', async () => {
const fetchMock = createFetchMock()
.mockResolvedValueOnce(waitResponse())
.mockResolvedValueOnce(doneResponse('second task'))
const agent = createAgent(fetchMock)
const { result: firstTask } = await startBlockedTask(agent)
await agent.stop()
expect(agent.status).toBe('stopped')
await expect(firstTask).resolves.toMatchObject({ success: false, data: 'Task aborted' })
const secondTask = await agent.execute('second')
expect(secondTask).toMatchObject({ success: true, data: 'second task' })
expect(agent.status).toBe('completed')
})
it('resolves only after the run has fully settled', async () => {
const fetchMock = createFetchMock().mockResolvedValueOnce(waitResponse())
const agent = createAgent(fetchMock)
const { result } = await startBlockedTask(agent)
await agent.stop()
expect(agent.status).toBe('stopped')
await expect(result).resolves.toMatchObject({ success: false })
})
it('is a no-op when no task is running', async () => {
const agent = createAgent(createFetchMock())
await expect(agent.stop()).resolves.toBeUndefined()
await expect(agent.stop()).resolves.toBeUndefined()
expect(agent.status).toBe('idle')
})
})
describe('dispose', () => {
it('aborts the running task and blocks further execution', async () => {
const fetchMock = createFetchMock().mockResolvedValueOnce(waitResponse())
const agent = createAgent(fetchMock)
const { result: task } = await startBlockedTask(agent)
agent.dispose()
await expect(task).resolves.toMatchObject({ success: false, data: 'Task aborted' })
expect(agent.disposed).toBe(true)
await expect(agent.execute('again')).rejects.toThrow('has been disposed')
})
it('is idempotent', () => {
const agent = createAgent(createFetchMock())
expect(() => {
agent.dispose()
agent.dispose()
}).not.toThrow()
expect(agent.disposed).toBe(true)
})
})
describe('error handling', () => {
it('fails the task when the network request rejects', async () => {
const fetchMock = createFetchMock().mockRejectedValue(new Error('network down'))
const agent = createAgent(fetchMock)
const result = await agent.execute('do something')
expect(result.success).toBe(false)
expect(agent.status).toBe('error')
})
it('fails the task when a tool throws', async () => {
const fetchMock = createFetchMock().mockResolvedValue(agentResponse({ action: { boom: {} } }))
const agent = createAgent(fetchMock, {
customTools: {
boom: tool({
description: 'Always throws.',
inputSchema: z.object({}),
execute: async () => {
throw new Error('tool exploded')
},
}),
},
})
const result = await agent.execute('trigger tool error')
expect(result.success).toBe(false)
expect(agent.status).toBe('error')
})
it('re-throws and sets error status when onBeforeTask throws', async () => {
const agent = createAgent(createFetchMock(), {
onBeforeTask: async () => {
throw new Error('setup failed')
},
})
await expect(agent.execute('do something')).rejects.toThrow('setup failed')
expect(agent.status).toBe('error')
expect(agent.history.some((e) => e.type === 'error')).toBe(false)
})
it('re-throws and sets error status when onAfterTask throws', async () => {
const fetchMock = createFetchMock().mockResolvedValueOnce(doneResponse('all done'))
const agent = createAgent(fetchMock, {
onAfterTask: async () => {
throw new Error('teardown failed')
},
})
await expect(agent.execute('do something')).rejects.toThrow('teardown failed')
expect(agent.status).toBe('error')
})
it('stays reusable after onBeforeTask throws', async () => {
const fetchMock = createFetchMock().mockResolvedValueOnce(doneResponse('second'))
let failOnce = true
const agent = createAgent(fetchMock, {
onBeforeTask: async () => {
if (failOnce) {
failOnce = false
throw new Error('setup failed')
}
},
})
await expect(agent.execute('first')).rejects.toThrow('setup failed')
const result = await agent.execute('second')
expect(result).toMatchObject({ success: true, data: 'second' })
})
it('re-throws and sets error status when onBeforeStep throws', async () => {
const agent = createAgent(createFetchMock(), {
onBeforeStep: async () => {
throw new Error('before step failed')
},
})
await expect(agent.execute('do something')).rejects.toThrow('before step failed')
expect(agent.status).toBe('error')
expect(agent.history.some((e) => e.type === 'error')).toBe(false)
})
it('re-throws and sets error status when onAfterStep throws', async () => {
const fetchMock = createFetchMock().mockResolvedValueOnce(doneResponse('all done'))
const agent = createAgent(fetchMock, {
onAfterStep: async () => {
throw new Error('after step failed')
},
})
await expect(agent.execute('do something')).rejects.toThrow('after step failed')
expect(agent.status).toBe('error')
expect(agent.history.some((e) => e.type === 'error')).toBe(false)
})
})
describe('cancellation edge cases', () => {
it('rejects a new task while a stop is still settling', async () => {
const fetchMock = createFetchMock().mockResolvedValueOnce(waitResponse())
const agent = createAgent(fetchMock)
const { result: firstTask } = await startBlockedTask(agent)
const stopped = agent.stop()
await expect(agent.execute('too early')).rejects.toThrow('A task is already running.')
await stopped
await expect(firstTask).resolves.toMatchObject({ success: false, data: 'Task aborted' })
expect(fetchMock).toHaveBeenCalledTimes(1)
})
it('discards a custom tool result that resolves after stop', async () => {
let resolveTool!: () => void
let notifyToolStarted!: () => void
const toolFinished = new Promise<void>((resolve) => {
resolveTool = resolve
})
const toolStarted = new Promise<void>((resolve) => {
notifyToolStarted = resolve
})
const fetchMock = createFetchMock().mockResolvedValue(
agentResponse({ action: { slow_tool: {} } })
)
const agent = createAgent(fetchMock, {
customTools: {
slow_tool: tool({
description: 'A tool that deliberately ignores cancellation.',
inputSchema: z.object({}),
execute: async () => {
notifyToolStarted()
await toolFinished
return 'ignored stop'
},
}),
},
})
const task = agent.execute('run slow tool')
await toolStarted
const stopped = agent.stop()
resolveTool()
await stopped
await expect(task).resolves.toMatchObject({ success: false, data: 'Task aborted' })
expect(agent.status).toBe('stopped')
})
})
})
+174 -154
View File
@@ -21,7 +21,7 @@ import type {
MacroToolInput,
MacroToolResult,
} from './types'
import { assert, fetchLlmsTxt, normalizeResponse, uid, waitFor } from './utils'
import { assert, fetchLlmsTxt, normalizeResponse, suppress, uid, waitFor } from './utils'
export { tool, type PageAgentTool } from './tools'
export type * from './types'
@@ -42,7 +42,7 @@ export type PageAgentCoreConfig = AgentConfig & { pageController: PageController
* - loop
*
* ## Event System
* - `statuschange` - Agent status transitions (idle → running → completed/error)
* - `statuschange` - Agent status transitions (idle → running → completed/error/stopped)
* - `historychange` - History events updated (persistent, part of agent memory)
* - `activity` - Real-time activity feedback (transient, for UI only)
* - `dispose` - Agent cleanup triggered
@@ -73,17 +73,28 @@ export class PageAgentCore extends EventTarget {
disposed = false
/**
* Callback for when agent needs user input (ask_user tool)
* If not set, ask_user tool will be disabled
* Called when the agent needs to ask the user questions.
* If unset, the `ask_user` tool will be disabled.
* Implementations should reject the promise when `signal` aborts.
* @example onAskUser: (q) => window.prompt(q) || ''
*/
onAskUser?: (question: string) => Promise<string>
onAskUser?: (question: string, options?: { signal: AbortSignal }) => Promise<string>
#status: AgentStatus = 'idle'
#llm: LLM
/**
* Task cancellation primitive: its signal reaches the LLM fetch, tools
* (via `ctx.signal`) and async callbacks. Aborted only by `stop`/`dispose`
* (during a task) or task setup, always WITHOUT a reason so `signal.reason`
* stays a standard `AbortError`.
*/
#abortController = new AbortController()
#observations: string[] = []
/** Resolves when the current run has fully settled. Awaited by `stop()`. */
#running: Promise<void> = Promise.resolve()
#lastResult: ExecutionResult | null = null
/** internal states during a single task execution */
#states = {
/** Accumulated wait time in seconds */
@@ -103,11 +114,14 @@ export class PageAgentCore extends EventTarget {
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
const { attempt, maxAttempts, lastError } = (e as CustomEvent).detail
this.#emitActivity({ type: 'retrying', attempt, maxAttempts })
// Also push to history for panel rendering
this.history.push({
type: 'error',
message: String(lastError),
rawResponse: (lastError as InvokeError).rawResponse,
})
this.history.push({
type: 'retry',
message: `LLM retry attempt ${attempt} of ${maxAttempts}`,
@@ -116,19 +130,6 @@ export class PageAgentCore extends EventTarget {
})
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)) {
@@ -150,13 +151,19 @@ export class PageAgentCore extends EventTarget {
return this.#status
}
/** Result of the most recent run, or `null` before the first run completes. */
get lastResult(): ExecutionResult | null {
return this.#lastResult
}
/** Emit statuschange event */
#emitStatusChange(): void {
this.dispatchEvent(new Event('statuschange'))
}
/** Emit historychange event */
#emitHistoryChange(): void {
#emitHistoryChange(pushHistoricalEvent?: HistoricalEvent): void {
if (pushHistoricalEvent) this.history.push(pushHistoricalEvent)
this.dispatchEvent(new Event('historychange'))
}
@@ -186,165 +193,184 @@ export class PageAgentCore extends EventTarget {
this.#observations.push(content)
}
/** Stop the current task. Agent remains reusable. */
stop() {
this.pageController.cleanUpHighlights()
this.pageController.hideMask()
/**
* Stop the current task and wait until the run has fully settled (including lifecycle hooks).
* @note never await .stop() in a lifecycle hook.
*/
async stop(): Promise<void> {
if (this.#status !== 'running') return
this.#abortController.abort()
await this.#running
}
/**
* external errors (pre-checks/config/hooks) will threw;
* agent errors will be caught and added to history, and return a failed result
*/
async execute(task: string): Promise<ExecutionResult> {
// pre-checks
if (this.disposed) throw new Error('PageAgent has been disposed. Create a new instance.')
if (this.#status === 'running') throw new Error('A task is already running.')
if (!task) throw new Error('Task is required')
this.task = task
this.taskId = uid()
this.history = []
this.#observations = []
this.#states = { totalWaitTime: 0, lastURL: '', browserState: null }
this.#abortController = new AbortController()
const signal = this.#abortController.signal
let resolveRunning!: () => void
this.#running = new Promise<void>((r) => (resolveRunning = r))
this.#setStatus('running')
this.#emitHistoryChange()
// Disable ask_user tool if onAskUser is not set
if (!this.onAskUser) {
this.tools.delete('ask_user')
}
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 }
const stepDelay = this.config.stepDelay ?? 0.4
const maxSteps = this.config.maxSteps
let step = 0
let taskResult: ExecutionResult
let finalStatus: AgentStatus = 'error'
while (true) {
try {
console.group(`step: ${step}`)
await suppress(() => this.pageController.showMask())
// graceful exit
try {
await onBeforeTask?.(this)
while (true) {
await onBeforeStep?.(this, step)
// observe
// handle internal agent errors
try {
console.group(`step: ${step}`)
console.log(chalk.blue.bold('👀 Observing...'))
// @note It's convenient to treat stepDelay as part of the next step.
// Maybe move it to a dedicated try block for better semantics?
if (step > 0) await waitFor(stepDelay, signal)
this.#states.browserState = await this.pageController.getBrowserState()
await this.#handleObservations(step)
signal.throwIfAborted()
// assemble prompts
// observe
const messages = [
{ role: 'system' as const, content: this.#getSystemPrompt() },
{ role: 'user' as const, content: await this.#assembleUserPrompt() },
]
console.log(chalk.blue.bold('👀 Observing...'))
const macroTool = { AgentOutput: this.#packMacroTool() }
this.#states.browserState = await this.pageController.getBrowserState()
await this.#handleObservations(step)
// invoke LLM
// assemble prompts
console.log(chalk.blue.bold('🧠 Thinking...'))
this.#emitActivity({ type: 'thinking' })
const messages = [
{ role: 'system' as const, content: this.#getSystemPrompt() },
{ role: 'user' as const, content: await this.#assembleUserPrompt() },
]
const result = await this.#llm.invoke(messages, macroTool, this.#abortController.signal, {
toolChoiceName: 'AgentOutput',
normalizeResponse: (res) => normalizeResponse(res, this.tools),
})
const macroTool = { AgentOutput: this.#packMacroTool() }
// assemble history
// invoke LLM
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,
}
console.log(chalk.blue.bold('🧠 Thinking...'))
this.#emitActivity({ type: 'thinking' })
this.history.push({
type: 'step',
stepIndex: step,
reflection,
action,
usage: result.usage,
rawResponse: result.rawResponse,
rawRequest: result.rawRequest,
} as AgentStepEvent)
this.#emitHistoryChange()
const result = await this.#llm.invoke(messages, macroTool, signal, {
toolChoiceName: 'AgentOutput',
normalizeResponse: (res) => normalizeResponse(res, this.tools),
})
//
// assemble history
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,
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,
}
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
}
this.#emitHistoryChange({
type: 'step',
stepIndex: step,
reflection,
action,
usage: result.usage,
rawResponse: result.rawResponse,
rawRequest: result.rawRequest,
})
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
}
if (actionName === 'done') {
const success = action.input?.success ?? false
const data = action.input?.text || 'no text provided'
console.log(chalk.green.bold('Task completed'), success, data)
taskResult = { success, data, history: this.history }
this.#lastResult = taskResult
finalStatus = 'completed'
break
}
} catch (error: unknown) {
// catch block must not throw error. otherwise the error may be overridden if finally block also throws error.
await waitFor(this.config.stepDelay ?? 0.4)
const isAbortError = (error as any)?.name === 'AbortError'
if (!isAbortError) console.error('Task failed', error)
const message = isAbortError ? 'Task aborted' : String(error)
this.#emitActivity({ type: 'error', message: message })
this.#emitHistoryChange({ type: 'error', message: message, rawResponse: error })
taskResult = { success: false, data: message, history: this.history }
this.#lastResult = taskResult
finalStatus = isAbortError ? 'stopped' : 'error'
break
} finally {
// finally block runs before the break above.
console.groupEnd()
// @note hook may throw error.
// which will override the `break` above and be handled as an external error.
// as expected.
await onAfterStep?.(this, this.history)
}
step++
if (step > maxSteps) {
const message = 'Step count exceeded maximum limit'
console.error(message)
this.#emitActivity({ type: 'error', message: message })
this.#emitHistoryChange({ type: 'error', message: message })
taskResult = { success: false, data: message, history: this.history }
this.#lastResult = taskResult
finalStatus = 'error'
break
}
} // while
await onAfterTask?.(this, taskResult)
return taskResult
} catch (error) {
this.#emitActivity({ type: 'error', message: String(error) })
finalStatus = 'error'
throw error
} finally {
await suppress(() => this.pageController.cleanUpHighlights())
await suppress(() => this.pageController.hideMask())
this.#abortController.abort()
resolveRunning()
this.#setStatus(finalStatus)
}
}
@@ -378,8 +404,8 @@ export class PageAgentCore extends EventTarget {
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')
const signal = this.#abortController.signal
signal.throwIfAborted()
console.log(chalk.blue.bold('MacroTool input'), input)
const action = input.action
@@ -411,8 +437,9 @@ export class PageAgentCore extends EventTarget {
const startTime = Date.now()
// Execute tool, bind `this` to PageAgent
const result = await tool.execute.bind(this)(toolInput)
const result = await tool.execute.bind(this)(toolInput, { signal })
// Enforce abort even if the tool ignored the signal and resolved normally.
signal.throwIfAborted()
const duration = Date.now() - startTime
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result)
@@ -619,13 +646,6 @@ export class PageAgentCore extends EventTarget {
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
@@ -1 +0,0 @@
system_prompt.md
+1 -2
View File
@@ -37,7 +37,7 @@ and system messages wrapped in <sys> tag.
<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 user request is very specific - then carefully follow each step and don't skip or hallucinate steps.
- If the task is open ended you can plan yourself how to get it done.
</user_request>
@@ -70,7 +70,6 @@ Strictly follow these rules while using the browser and navigating the web:
- 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.
+26 -13
View File
@@ -7,6 +7,14 @@ import * as z from 'zod/v4'
import type { PageAgentCore } from '../PageAgentCore'
import { waitFor } from '../utils'
/**
* Per-invocation context passed to every tool execution.
* Tools MUST honor `signal` to support cooperative cancellation.
*/
export interface ToolContext {
signal: AbortSignal
}
/**
* Internal tool definition that has access to PageAgent `this` context
*/
@@ -14,7 +22,7 @@ export interface PageAgentTool<TParams = any> {
// name: string
description: string
inputSchema: z.ZodType<TParams>
execute: (this: PageAgentCore, args: TParams) => Promise<string>
execute: (this: PageAgentCore, args: TParams, ctx: ToolContext) => Promise<string>
}
export function tool<TParams>(options: PageAgentTool<TParams>): PageAgentTool<TParams> {
@@ -50,14 +58,16 @@ tools.set(
inputSchema: z.object({
seconds: z.number().min(1).max(10).default(1),
}),
execute: async function (this: PageAgentCore, input) {
execute: async function (this: PageAgentCore, input, { signal }) {
// 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)
const secondsSinceLastUpdate = (Date.now() - lastTimeUpdate) / 1000
const actualWaitTime = Math.max(0, input.seconds - secondsSinceLastUpdate)
console.log(`actualWaitTime: ${actualWaitTime} seconds`)
await waitFor(actualWaitTime)
await waitFor(actualWaitTime, signal)
return `✅ Waited for ${input.seconds} seconds.`
const waitedSeconds = (secondsSinceLastUpdate + actualWaitTime).toFixed(2)
return `✅ Waited for ${waitedSeconds} seconds.`
},
})
)
@@ -70,11 +80,11 @@ tools.set(
inputSchema: z.object({
question: z.string(),
}),
execute: async function (this: PageAgentCore, input) {
execute: async function (this: PageAgentCore, input, { signal }) {
if (!this.onAskUser) {
throw new Error('ask_user tool requires onAskUser callback to be set')
}
const answer = await this.onAskUser(input.question)
const answer = await this.onAskUser(input.question, { signal })
return `User answered: ${answer}`
},
})
@@ -131,7 +141,8 @@ tools.set(
tools.set(
'scroll',
tool({
description: 'Scroll the page vertically. Use index for scroll elements (dropdowns/custom UI).',
description:
'Scroll vertically. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.',
inputSchema: z.object({
down: z.boolean().default(true),
num_pages: z.number().min(0).max(10).optional().default(0.1),
@@ -155,7 +166,7 @@ tools.set(
'scroll_horizontally',
tool({
description:
'Scroll the page horizontally, or within a specific element by index. Useful for wide tables.',
'Scroll horizontally. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.',
inputSchema: z.object({
right: z.boolean().default(true),
pixels: z.number().int().min(0),
@@ -172,12 +183,15 @@ tools.set(
'execute_javascript',
tool({
description:
'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution!',
'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution! ' +
'An `AbortSignal` named `signal` is available in scope: long-running async code MUST honor it ' +
'(e.g. `await fetch(url, { signal })`, or `signal.throwIfAborted()` in loops)',
inputSchema: z.object({
script: z.string(),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.executeJavascript(input.script)
execute: async function (this: PageAgentCore, input, { signal }) {
const result = await this.pageController.executeJavascript(input.script, signal)
signal.throwIfAborted()
return result.message
},
})
@@ -185,5 +199,4 @@ tools.set(
// @todo send_keys
// @todo upload_file
// @todo go_back
// @todo extract_structured_data
+3 -7
View File
@@ -255,16 +255,12 @@ export interface AgentErrorEvent {
* Union type for all history events
*/
export type HistoricalEvent =
| AgentStepEvent
| ObservationEvent
| UserTakeoverEvent
| RetryEvent
| AgentErrorEvent
AgentStepEvent | ObservationEvent | UserTakeoverEvent | RetryEvent | AgentErrorEvent
/**
* Agent execution status
* Agent lifecycle status.
*/
export type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
export type AgentStatus = 'idle' | 'running' | 'completed' | 'error' | 'stopped'
/**
* Agent activity - transient state for immediate UI feedback.
+5 -5
View File
@@ -1,4 +1,4 @@
import { InvokeError, InvokeErrorType } from '@page-agent/llms'
import { InvokeError, InvokeErrorTypes } from '@page-agent/llms'
import chalk from 'chalk'
import * as z from 'zod/v4'
@@ -19,7 +19,7 @@ const log = console.log.bind(console, chalk.yellow('[autoFixer]'))
* - etc.
*/
export function normalizeResponse(response: any, tools?: Map<string, PageAgentTool>): any {
let resolvedArguments = null as any
let resolvedArguments: any
const choice = (response as { choices?: Choice[] }).choices?.[0]
if (!choice) throw new Error('No choices in response')
@@ -93,7 +93,7 @@ export function normalizeResponse(response: any, tools?: Map<string, PageAgentTo
// fix incomplete formats
if (!resolvedArguments.action) {
log(`#5: fixing tool_call`)
resolvedArguments.action = { name: 'wait', input: { seconds: 1 } }
resolvedArguments.action = { wait: { seconds: 1 } }
}
// pack back to standard format
@@ -137,7 +137,7 @@ function validateAction(action: any, tools: Map<string, PageAgentTool>): any {
if (!tool) {
const available = Array.from(tools.keys()).join(', ')
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
InvokeErrorTypes.INVALID_TOOL_ARGS,
`Unknown action "${toolName}". Available: ${available}`
)
}
@@ -159,7 +159,7 @@ function validateAction(action: any, tools: Map<string, PageAgentTool>): any {
const result = schema.safeParse(value)
if (!result.success) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
InvokeErrorTypes.INVALID_TOOL_ARGS,
`Invalid input for action "${toolName}": ${z.prettifyError(result.error)}`
)
}
+34 -2
View File
@@ -2,8 +2,28 @@ import chalk from 'chalk'
export * from './autoFixer'
export async function waitFor(seconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
/**
* Wait for `seconds`. If a `signal` is provided, the wait is cancellable:
* aborting rejects with the signal's reason (an `AbortError`).
*/
export async function waitFor(seconds: number, signal?: AbortSignal): Promise<void> {
if (!signal) {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
return
}
signal.throwIfAborted()
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
signal.removeEventListener('abort', onAbort)
resolve()
}, seconds * 1000)
const onAbort = () => {
clearTimeout(timer)
// reason is a DOMException AbortError.
reject(signal.reason as DOMException)
}
signal.addEventListener('abort', onAbort, { once: true })
})
}
//
@@ -109,3 +129,15 @@ export function assert(condition: unknown, message?: string, silent?: boolean):
throw new Error(errorMessage)
}
}
/**
* Suppress errors from a function.
*/
export async function suppress<T>(fn: () => T | Promise<T>): Promise<Awaited<T> | undefined> {
try {
return await fn()
} catch (error) {
console.error(error)
return undefined
}
}
-9
View File
@@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+3 -17
View File
@@ -1,22 +1,8 @@
{
"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"]
}
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"],
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" }
]
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"]
}
+11 -4
View File
@@ -11,13 +11,20 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
export default defineConfig({
clearScreen: false,
plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
dts({
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts'],
bundleTypes: true,
compilerOptions: {
composite: true,
noEmit: false,
emitDeclarationOnly: true,
declaration: true,
},
}),
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
],
publicDir: false,
esbuild: {
keepNames: true,
},
build: {
lib: {
entry: resolve(__dirname, 'src/PageAgentCore.ts'),
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
name: 'core',
include: ['src/**/*.test.ts'],
silent: 'passed-only',
},
})
-2
View File
@@ -1,2 +0,0 @@
.wxt
src/components/ui
+83 -81
View File
@@ -42,33 +42,28 @@ localStorage.setItem('PageAgentExtUserAuthToken', 'your-token')
## Quick Start
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
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
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)
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)
}
```
@@ -90,10 +85,10 @@ Execute one agent task.
Parameters:
| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `task` | `string` | Yes | Task description |
| `config` | `ExecuteConfig` | Yes | LLM settings, options, and callbacks |
| Name | Type | Required | Description |
| -------- | --------------- | -------- | ------------------------------------ |
| `task` | `string` | Yes | Task description |
| `config` | `ExecuteConfig` | Yes | LLM settings, options, and callbacks |
Returns: `Promise<ExecutionResult>`
@@ -101,29 +96,37 @@ Returns: `Promise<ExecutionResult>`
Stop the current task.
## Limitations
- **Normal browser windows only.** This extension relies on tab group API which does not work for pop-up window or PWA App window.
## Types
Install `@page-agent/core` for complete types:
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'
export interface ExecuteConfig {
baseURL: string
apiKey: string
model: string
baseURL: string
model: string
apiKey?: string
// Include the initial tab where page JS starts. Default: true.
includeInitialTab?: boolean
// Global system-level instructions for the agent.
// Equivalent to AgentConfig.instructions.system.
systemInstruction?: string
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
// Include the initial tab where page JS starts. Default: true.
includeInitialTab?: boolean
// Control all unpinned tabs in the window instead of only the tab group.
// When enabled, agent sees and can switch to every non-pinned tab.
// Default: false. Experimental.
experimentalIncludeAllTabs?: boolean
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
}
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
@@ -132,38 +135,38 @@ export type Execute = (task: string, config: ExecuteConfig) => Promise<Execution
`AgentStatus`
```typescript
type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
type AgentStatus = 'idle' | 'running' | 'completed' | 'error' | 'stopped'
```
`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 }
| { 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 }
| { 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[]
success: boolean
data: string
history: HistoricalEvent[]
}
```
@@ -173,15 +176,15 @@ interface ExecutionResult {
```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),
}
'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),
}
)
```
@@ -196,31 +199,30 @@ window.PAGE_AGENT_EXT!.stop()
If you are not importing `@page-agent/core`, add:
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'
interface ExecuteConfig {
baseURL: string
apiKey: string
model: string
includeInitialTab?: boolean
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
baseURL: string
model: string
apiKey?: string
systemInstruction?: string
includeInitialTab?: boolean
experimentalIncludeAllTabs?: 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
interface Window {
PAGE_AGENT_EXT_VERSION?: string
PAGE_AGENT_EXT?: {
version: string
execute: Execute
stop: () => void
}
}
}
}
```
+22 -21
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/ext",
"private": true,
"version": "1.5.10",
"version": "1.12.0",
"type": "module",
"scripts": {
"dev": "wxt",
@@ -10,37 +10,38 @@
"postinstall": "wxt prepare"
},
"devDependencies": {
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-hover-card": "^1.1.18",
"@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",
"@radix-ui/react-label": "^2.1.11",
"@radix-ui/react-separator": "^1.1.11",
"@radix-ui/react-slot": "^1.3.0",
"@radix-ui/react-switch": "^1.3.2",
"@tailwindcss/vite": "^4.3.2",
"@types/chrome": "^0.2.2",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@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",
"lucide-react": "^1.23.0",
"motion": "^12.42.2",
"next-themes": "^0.4.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"rough-notation": "^0.5.1",
"simple-icons": "^16.12.0",
"simple-icons": "^16.25.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.14",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.2",
"tw-animate-css": "^1.4.0",
"wxt": "^0.20.19"
"wxt": "^0.20.27"
},
"dependencies": {
"@page-agent/core": "1.5.10",
"@page-agent/llms": "1.5.10",
"@page-agent/page-controller": "1.5.10",
"@page-agent/ui": "1.5.10",
"@page-agent/core": "1.12.0",
"@page-agent/llms": "1.12.0",
"@page-agent/page-controller": "1.12.0",
"@page-agent/ui": "1.12.0",
"ai-motion": "^0.4.8",
"chalk": "^5.6.2"
},
@@ -1,11 +1,11 @@
{
"extName": {
"message": "Page Agent Ext"
},
"extDescription": {
"message": "AI-powered browser automation assistant. Control web pages with natural language."
},
"extActionTitle": {
"message": "Open Page Agent"
}
"extName": {
"message": "Page Agent Ext"
},
"extDescription": {
"message": "AI-powered browser automation assistant. Control web pages with natural language."
},
"extActionTitle": {
"message": "Open Page Agent"
}
}
@@ -1,11 +1,11 @@
{
"extName": {
"message": "Page Agent Ext"
},
"extDescription": {
"message": "AI 驱动的浏览器自动化助手,用自然语言控制网页。"
},
"extActionTitle": {
"message": "打开 Page Agent"
}
"extName": {
"message": "Page Agent Ext"
},
"extDescription": {
"message": "AI 驱动的浏览器自动化助手,用自然语言控制网页。"
},
"extActionTitle": {
"message": "打开 Page Agent"
}
}
@@ -1 +0,0 @@
system_prompt.md
+34 -31
View File
@@ -11,13 +11,18 @@ function detectLanguage(): 'en-US' | 'zh-CN' {
return lang.startsWith('zh') ? 'zh-CN' : 'en-US'
}
interface MultiPageAgentConfig extends AgentConfig {
includeInitialTab?: boolean
experimentalIncludeAllTabs?: boolean
}
/**
* 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 }) {
constructor(config: MultiPageAgentConfig) {
// multi page controller
const tabsController = new TabsController()
const pageController = new RemotePageController(tabsController)
@@ -31,67 +36,65 @@ export class MultiPageAgent extends PageAgentCore {
`Default working language: **${targetLanguage}**`
)
// include initial tab for controlling
const includeInitialTab = config.includeInitialTab ?? true
const experimentalIncludeAllTabs = config.experimentalIncludeAllTabs ?? false
/**
* Project agent status into chrome.storage. The content script polls
* `isAgentRunning` + `agentHeartbeat` (eventually consistent by design).
*
* 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
let heartBeatInterval: number | null = null
super({
...config,
// Disabled: AbortSignal cannot cross contexts
experimentalScriptExecutionTool: false,
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,
})
await tabsController.init(agent.task, { includeInitialTab, experimentalIncludeAllTabs })
},
onBeforeStep: async (agent) => {
// pull latest tab state so that tabs changes can be observed
await tabsController.syncTabs()
if (!tabsController.currentTabId) return
// make sure the current tab is loaded before the step starts
await tabsController.waitUntilTabLoaded(tabsController.currentTabId!)
await tabsController.waitUntilTabLoaded(tabsController.currentTabId)
},
onDispose: () => {
if (heartBeatInterval) {
window.clearInterval(heartBeatInterval)
clearInterval(heartBeatInterval)
heartBeatInterval = null
}
chrome.storage.local.set({
isAgentRunning: false,
})
chrome.storage.local.set({ isAgentRunning: false }).catch(console.error)
tabsController.dispose()
},
})
this.addEventListener('statuschange', () => {
const running = this.status === 'running'
if (running && !heartBeatInterval) {
heartBeatInterval = window.setInterval(() => {
void chrome.storage.local.set({ agentHeartbeat: Date.now() })
}, 1_000)
} else if (!running && heartBeatInterval) {
clearInterval(heartBeatInterval)
heartBeatInterval = null
}
chrome.storage.local.set({ isAgentRunning: running }).catch(console.error)
})
}
}
@@ -10,9 +10,7 @@ export function handlePageControlMessage(
): true | undefined {
const PREFIX = '[RemotePageController.background]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
const debug = console.debug.bind(console, `\x1b[90m${PREFIX}\x1b[0m`)
const { action, payload, targetTabId } = message
@@ -19,7 +19,10 @@ export function initPageController() {
function getPC(): PageController {
if (!pageController) {
pageController = new PageController({ enableMask: false, viewportExpansion: 400 })
pageController = new PageController({
enableMask: false,
viewportExpansion: 400,
})
}
return pageController
}
@@ -4,9 +4,7 @@ import type { TabsController } from './TabsController'
const PREFIX = '[RemotePageController]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
const debug = console.debug.bind(console, `\x1b[90m${PREFIX}\x1b[0m`)
function sendMessage(message: {
type: 'PAGE_CONTROL'
@@ -58,7 +56,7 @@ export class RemotePageController {
}
async getBrowserState(): Promise<BrowserState> {
let browserState = {} as BrowserState
let browserState: BrowserState
debug('getBrowserState', this.currentTabId)
const currentUrl = await this.getCurrentUrl()
@@ -135,9 +133,7 @@ export class RemotePageController {
return this.remoteCallDomAction('scroll_horizontally', args)
}
async executeJavascript(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('execute_javascript', args)
}
// `execute_javascript` is intentionally not implemented: AbortSignal cannot cross context
/** @note Managed by content script via storage polling. */
async showMask(): Promise<void> {}
@@ -1,12 +1,47 @@
/**
* background logics for TabsController
*
* Keep this stateless: pure request/response handlers only, no in-memory
* state, no ports, no event pushing. MV3 SW should be killed and restarted at
* any time (idle timeout, extension update) without special handling.
*/
import type { TabAction } from './TabsController'
const PREFIX = '[TabsController.background]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
const debug = console.debug.bind(console, `\x1b[90m${PREFIX}\x1b[0m`)
/**
* Resolve active tab.
*
* - `tabs.query({ active: true })` does not work in multi-window scenarios.
* - Extension pages (side panel, hub tab) can resolve their own windowId.
* We just find the active tab within that window.
* - Content scripts (PAGE_AGENT_EXT) can't self-report a windowId.
* Chrome populates `sender.tab` for every content-script message,
* which is the tab hosting the script.
*/
async function resolveActiveTab(
payload: { windowId?: number } | undefined,
sender: chrome.runtime.MessageSender
): Promise<chrome.tabs.Tab> {
const windowId = payload?.windowId
if (windowId != null) {
debug('get_active_tab: resolving via caller-reported windowId', windowId)
const [tab] = await chrome.tabs.query({ active: true, windowId })
if (!tab) throw new Error(`No active tab found in window ${windowId}.`)
return tab
}
if (sender.tab) {
debug('get_active_tab: resolving via sender.tab (content script)', sender.tab.id)
return sender.tab
}
throw new Error(
'Cannot resolve active tab: caller reported no windowId and is not a content script (no sender.tab).'
)
}
export function handleTabControlMessage(
@@ -18,13 +53,11 @@ export function handleTabControlMessage(
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 })
debug('get_active_tab', payload)
resolveActiveTab(payload, sender)
.then((tab) => {
debug('get_active_tab: success', tab)
sendResponse({ success: true, tab })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
@@ -49,7 +82,7 @@ export function handleTabControlMessage(
case 'open_new_tab': {
debug('open_new_tab', payload)
chrome.tabs
.create({ url: payload.url, active: false })
.create({ url: payload.url, windowId: payload.windowId, active: false })
.then((newTab) => {
debug('open_new_tab: success', newTab)
sendResponse({ success: true, tabId: newTab.id })
@@ -63,7 +96,7 @@ export function handleTabControlMessage(
case 'create_tab_group': {
debug('create_tab_group', payload)
chrome.tabs
.group({ tabIds: payload.tabIds })
.group({ tabIds: payload.tabIds, createProperties: { windowId: payload.windowId } })
.then((groupId) => {
debug('create_tab_group: success', groupId)
sendResponse({ success: true, groupId })
@@ -114,47 +147,20 @@ export function handleTabControlMessage(
return true // async response
}
case 'get_window_tabs': {
chrome.tabs
.query({ windowId: payload.windowId })
.then((tabs) => {
sendResponse({ success: true, tabs })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true
}
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)
})
})
}
+198 -92
View File
@@ -2,9 +2,7 @@ import { isContentScriptAllowed } from './RemotePageController'
const PREFIX = '[TabsController]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
const debug = console.debug.bind(console, `\x1b[90m${PREFIX}\x1b[0m`)
function sendMessage(message: {
type: 'TAB_CONTROL'
@@ -17,51 +15,107 @@ function sendMessage(message: {
})
}
/**
* Resolve the window hosting this script's own context, when knowable.
*
* Extension pages (side panel, hub tab) have `chrome.windows` access and can
* identify their own window directly via `getCurrent()`.
* Content scripts have no `chrome.windows` access; they resolve `undefined`
* here and the background script falls back to `sender.tab` instead.
*/
async function getOwnWindowId(): Promise<number | undefined> {
if (typeof chrome.windows === 'undefined') return undefined
const win = await chrome.windows.getCurrent()
return win.id
}
/**
* Controller for managing browser tabs.
* - live in the agent env (extension page or content script)
* - no chrome apis. call sw for tab operations
* - store tabs states, pull tabs info and detect changes
*/
export class TabsController extends EventTarget {
export class TabsController {
currentTabId: number | null = null
private disposed = false
/* tracked window */
private windowId: number | null = null
/* tracked tabs */
private tabs: TabMeta[] = []
private initialTabId: number | null = null
private tabGroupId: number | null = null
private experimentalIncludeAllTabs = false
private task: string = ''
async init(task: string, includeInitialTab: boolean = true) {
debug('init', task, includeInitialTab)
async init(task: string, options: TabsInitOptions = {}) {
const { includeInitialTab = true, experimentalIncludeAllTabs = false } = options
debug('init', task, options)
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 (this.disposed) {
throw new Error('TabsController already disposed')
}
if (includeInitialTab) {
await this.updateCurrentTabId(null)
this.windowId = null
this.tabs = []
this.tabGroupId = null
this.initialTabId = null
this.experimentalIncludeAllTabs = experimentalIncludeAllTabs
this.task = task
const activeTabResult = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_active_tab',
payload: { windowId: await getOwnWindowId() },
})
this.initialTabId = activeTabResult.tab?.id
this.windowId = activeTabResult.tab?.windowId
if (!this.initialTabId || !this.windowId) {
if (activeTabResult.error) {
throw new Error(activeTabResult.error)
} else {
throw new Error('Failed to get active tab')
}
}
if (experimentalIncludeAllTabs) {
const allTabs = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_window_tabs',
payload: { windowId: this.windowId },
})
for (const tab of allTabs.tabs as chrome.tabs.Tab[]) {
if (tab.id && !tab.pinned && isContentScriptAllowed(tab.url)) {
this.addTab({
id: tab.id,
isInitial: tab.id === this.initialTabId,
url: tab.url,
title: tab.title,
status: tab.status,
})
}
}
if (this.tabs.find((t) => t.id === this.initialTabId)) {
this.currentTabId = this.initialTabId
await this.createTabGroup([this.initialTabId])
}
} else if (includeInitialTab) {
const info = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_tab_info',
payload: { tabId: this.initialTabId },
})
if (isContentScriptAllowed(info.url)) {
if (isContentScriptAllowed(info.url) && !info.pinned) {
this.currentTabId = this.initialTabId
this.tabs.push({
id: result.tabId,
this.addTab({
id: this.initialTabId,
isInitial: true,
url: info.url,
title: info.title,
@@ -73,52 +127,6 @@ export class TabsController extends EventTarget {
}
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> {
@@ -127,7 +135,7 @@ export class TabsController extends EventTarget {
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'open_new_tab',
payload: { url },
payload: { url, windowId: this.windowId },
})
if (!result.success) {
@@ -136,7 +144,7 @@ export class TabsController extends EventTarget {
const tabId = result.tabId as number
this.tabs.push({
this.addTab({
id: tabId,
isInitial: false,
})
@@ -209,7 +217,7 @@ export class TabsController extends EventTarget {
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'create_tab_group',
payload: { tabIds },
payload: { tabIds, windowId: this.windowId },
})
if (!result?.success) {
@@ -232,6 +240,11 @@ export class TabsController extends EventTarget {
})
}
private addTab(meta: TabMeta) {
if (this.tabs.find((t) => t.id === meta.id)) return
this.tabs.push(meta)
}
async updateCurrentTabId(tabId: number | null) {
debug('updateCurrentTabId', tabId)
@@ -263,11 +276,14 @@ export class TabsController extends EventTarget {
}
async summarizeTabs(): Promise<string> {
const summaries = [`| Tab ID | URL | Title | Current |`, `|-----|-----|-----|-----|`]
const summaries = [
`| Tab ID | URL | Title | Status | 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 ? '✅' : ''} |`
`| ${tab.id} | ${url} | ${title} | ${tab.status ?? '-'} | ${this.currentTabId === tab.id ? '✅' : ''} |`
)
}
if (!this.tabs.length) {
@@ -280,19 +296,104 @@ export class TabsController extends EventTarget {
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
// When a tracked tab is closed or untracked.
// The tab object will be removed from the tab list.
// Finding the latest tab object is the only way to know if it's closed.
debug('waitUntilTabLoaded', tabId)
await waitUntil(() => tab.status === 'complete', 4_000)
await waitUntil(async () => {
await this.syncTabs()
const latest = this.tabs.find((t) => t.id === tabId)
return !latest || latest.status !== 'loading'
}, 4_000)
const latest = this.tabs.find((t) => t.id === tabId)
if (latest?.status === 'unloaded') throw new Error(`Tab ID ${tabId} is unloaded.`)
}
/**
* Pull the window's tabs from the background.
* Pulling is better than pushing. Long-lived ports are stateful troublemakers.
*/
async syncTabs(): Promise<void> {
if (this.disposed || this.windowId == null) return
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_window_tabs',
payload: { windowId: this.windowId },
})
// sendMessage already logged the failure; keep the stale mirror
if (!result?.success) return
const liveTabs = (result.tabs as chrome.tabs.Tab[]).filter((t) => t.id != null)
const liveIds = new Set(liveTabs.map((t) => t.id!))
const closedIds = this.tabs.filter((t) => !liveIds.has(t.id)).map((t) => t.id)
if (closedIds.length) {
debug('syncTabs: tabs closed', closedIds)
this.tabs = this.tabs.filter((t) => liveIds.has(t.id))
}
const newTabs: TabMeta[] = []
for (const live of liveTabs) {
const tracked = this.tabs.find((t) => t.id === live.id)
if (tracked) {
tracked.url = live.url
tracked.title = live.title
tracked.status = live.status as TabMeta['status']
} else if (this.shouldTrack(live)) {
debug('syncTabs: new tab', live.id, live.url)
const meta: TabMeta = {
id: live.id!,
isInitial: false,
url: live.url,
title: live.title,
status: live.status,
}
this.addTab(meta)
newTabs.push(meta)
}
}
// Follow the page like a user would: focus the newest tab it opened.
// If the current tab is gone, fall back to the last tracked one.
if (newTabs.length) {
await this.switchToTab(newTabs[newTabs.length - 1].id)
} else if (this.currentTabId != null && !this.tabs.find((t) => t.id === this.currentTabId)) {
const fallback = this.tabs[this.tabs.length - 1]
if (fallback) {
await this.switchToTab(fallback.id)
} else {
debug('syncTabs: no fallback tab found, updating current tab to null')
await this.updateCurrentTabId(null)
}
}
}
private shouldTrack(tab: chrome.tabs.Tab): boolean {
if (this.tabGroupId != null && tab.groupId === this.tabGroupId) return true
return (
this.experimentalIncludeAllTabs &&
tab.windowId === this.windowId &&
!tab.pinned &&
isContentScriptAllowed(tab.url)
)
}
dispose() {
this.dispatchEvent(new Event('dispose'))
debug('dispose')
this.disposed = true
}
}
export interface TabsInitOptions {
includeInitialTab?: boolean
experimentalIncludeAllTabs?: boolean
}
export type TabAction =
| 'get_active_tab'
| 'get_tab_info'
@@ -302,6 +403,7 @@ export type TabAction =
| 'add_tab_to_group'
| 'close_tab'
| 'get_tab_title'
| 'get_window_tabs'
interface TabMeta {
id: number
@@ -321,29 +423,33 @@ function randomColor(): TabGroupColor {
/**
* 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
* @returns Returns when condition becomes true, false if timeout
* @param timeoutMS Timeout in milliseconds, default 1 minutes
* @param throwIfTimeout Reject on timeout instead of resolving with `false`
*/
export async function waitUntil(
async function waitUntil(
check: () => boolean | Promise<boolean>,
timeoutMS = 60_000,
error?: string
throwIfTimeout = false
): 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)
try {
if (await check()) return resolve(true)
if (Date.now() - start > timeoutMS) {
if (throwIfTimeout) {
return reject(new Error(`waitUntil timed out after ${timeoutMS}ms`))
} else {
return resolve(false)
}
}
setTimeout(poll, 100)
} catch (err) {
reject(err instanceof Error ? err : new Error(String(err)))
}
setTimeout(poll, 100)
}
setTimeout(poll, 100)
})
+2 -2
View File
@@ -3,12 +3,12 @@ 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_API_KEY = 'NA'
export const DEMO_CONFIG: LLMConfig = {
apiKey: DEMO_API_KEY,
baseURL: DEMO_BASE_URL,
model: DEMO_MODEL,
// apiKey: DEMO_API_KEY,
}
/** Legacy testing endpoints that should be auto-migrated to DEMO_BASE_URL */
@@ -37,7 +37,7 @@ and system messages wrapped in <sys> tag.
<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 user request is very specific - then carefully follow each step and don't skip or hallucinate steps.
- If the task is open ended you can plan yourself how to get it done.
</user_request>
@@ -72,7 +72,6 @@ Strictly follow these rules while using the browser and navigating the web:
- 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.
+12 -3
View File
@@ -21,6 +21,8 @@ export interface AdvancedConfig {
maxSteps?: number
systemInstruction?: string
experimentalLlmsTxt?: boolean
experimentalIncludeAllTabs?: boolean
disableNamedToolChoice?: boolean
}
export interface ExtConfig extends LLMConfig, AdvancedConfig {
@@ -78,7 +80,7 @@ export function useAgent(): UseAgentResult {
const handleStatusChange = (e: Event) => {
const newStatus = agent.status as AgentStatus
setStatus(newStatus)
if (newStatus === 'idle' || newStatus === 'completed' || newStatus === 'error') {
if (newStatus !== 'running') {
setActivity(null)
}
}
@@ -106,7 +108,6 @@ export function useAgent(): UseAgentResult {
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)
@@ -124,6 +125,8 @@ export function useAgent(): UseAgentResult {
maxSteps,
systemInstruction,
experimentalLlmsTxt,
experimentalIncludeAllTabs,
disableNamedToolChoice,
...llmConfig
}: ExtConfig) => {
await chrome.storage.local.set({ llmConfig })
@@ -132,7 +135,13 @@ export function useAgent(): UseAgentResult {
} else {
await chrome.storage.local.remove('language')
}
const advancedConfig: AdvancedConfig = { maxSteps, systemInstruction, experimentalLlmsTxt }
const advancedConfig: AdvancedConfig = {
maxSteps,
systemInstruction,
experimentalLlmsTxt,
experimentalIncludeAllTabs,
disableNamedToolChoice,
}
await chrome.storage.local.set({ advancedConfig })
setConfig({ ...llmConfig, ...advancedConfig, language })
},
@@ -14,7 +14,7 @@ import {
import { useEffect, useState } from 'react'
import { siGithub } from 'simple-icons'
import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL, isTestingEndpoint } from '@/agent/constants'
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'
@@ -27,31 +27,41 @@ interface ConfigPanelProps {
}
export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
const [apiKey, setApiKey] = useState(config?.apiKey || DEMO_API_KEY)
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 [maxSteps, setMaxSteps] = useState(config?.maxSteps)
const [systemInstruction, setSystemInstruction] = useState(config?.systemInstruction ?? '')
const [experimentalLlmsTxt, setExperimentalLlmsTxt] = useState(
config?.experimentalLlmsTxt ?? false
)
const [experimentalIncludeAllTabs, setExperimentalIncludeAllTabs] = useState(
config?.experimentalIncludeAllTabs ?? false
)
const [disableNamedToolChoice, setDisableNamedToolChoice] = useState(
config?.disableNamedToolChoice ?? false
)
const [advancedOpen, setAdvancedOpen] = useState(false)
const [saving, setSaving] = useState(false)
const [userAuthToken, setUserAuthToken] = useState<string>('')
const [userAuthToken, setUserAuthToken] = useState('')
const [copied, setCopied] = useState(false)
const [showToken, setShowToken] = useState(false)
const [showApiKey, setShowApiKey] = useState(false)
useEffect(() => {
setApiKey(config?.apiKey || DEMO_API_KEY)
const [prevConfig, setPrevConfig] = useState(config)
if (prevConfig !== config) {
setPrevConfig(config)
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)
}, [config])
setExperimentalIncludeAllTabs(config?.experimentalIncludeAllTabs ?? false)
setDisableNamedToolChoice(config?.disableNamedToolChoice ?? false)
}
// Poll for user auth token every second until found
useEffect(() => {
@@ -96,6 +106,8 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
maxSteps: maxSteps || undefined,
systemInstruction: systemInstruction || undefined,
experimentalLlmsTxt,
experimentalIncludeAllTabs,
disableNamedToolChoice,
})
} finally {
setSaving(false)
@@ -111,6 +123,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
size="icon-sm"
onClick={onClose}
className="absolute top-2 right-3 cursor-pointer"
aria-label="Back"
>
<CornerUpLeft className="size-3.5" />
</Button>
@@ -118,12 +131,15 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
{/* 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>
<label htmlFor="user-auth-token" 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
id="user-auth-token"
readOnly
value={
userAuthToken
@@ -140,6 +156,8 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
className="h-8 w-8 shrink-0 cursor-pointer"
onClick={() => setShowToken(!showToken)}
disabled={!userAuthToken}
aria-label={showToken ? 'Hide token' : 'Show token'}
aria-pressed={showToken}
>
{showToken ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
</Button>
@@ -149,9 +167,13 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
className="h-8 w-8 shrink-0 cursor-pointer"
onClick={handleCopyToken}
disabled={!userAuthToken}
aria-label="Copy token"
>
{copied ? <span className=""></span> : <Copy className="size-3" />}
</Button>
<span role="status" aria-live="polite" aria-atomic="true" className="sr-only">
{copied ? 'Token copied' : ''}
</span>
</div>
</div>
@@ -159,6 +181,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
<a
href="/hub.html"
target="_blank"
rel="noopener noreferrer"
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
@@ -166,8 +189,11 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
</a>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Base URL</label>
<label htmlFor="base-url" className="text-xs text-muted-foreground">
Base URL
</label>
<Input
id="base-url"
placeholder="https://api.openai.com/v1"
value={baseURL}
onChange={(e) => setBaseURL(e.target.value)}
@@ -192,9 +218,12 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
)}
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Model</label>
<label htmlFor="model" className="text-xs text-muted-foreground">
Model
</label>
<Input
placeholder="gpt-5.2"
id="model"
placeholder="gpt-5.1"
value={model}
onChange={(e) => setModel(e.target.value)}
className="text-xs h-8"
@@ -202,11 +231,14 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">API Key</label>
<label htmlFor="api-key" className="text-xs text-muted-foreground">
API Key
</label>
<div className="flex gap-2 items-center">
<Input
id="api-key"
type={showApiKey ? 'text' : 'password'}
placeholder="sk-..."
// placeholder="sk-..."
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="text-xs h-8"
@@ -216,6 +248,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
size="icon"
className="h-8 w-8 shrink-0 cursor-pointer"
onClick={() => setShowApiKey(!showApiKey)}
aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
>
{showApiKey ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
</Button>
@@ -248,8 +281,11 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
{advancedOpen && (
<>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Max Steps</label>
<label htmlFor="max-steps" className="text-xs text-muted-foreground">
Max Steps
</label>
<Input
id="max-steps"
type="number"
placeholder="40"
min={1}
@@ -271,10 +307,23 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
/>
</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>
<label className="flex items-center justify-between cursor-pointer">
<span className="text-xs text-muted-foreground">Experimental include all tabs</span>
<Switch
checked={experimentalIncludeAllTabs}
onCheckedChange={setExperimentalIncludeAllTabs}
/>
</label>
</>
)}
@@ -1,12 +1,20 @@
import { ArrowLeft } from 'lucide-react'
import { ArrowLeft, RotateCcw, Trash2 } from 'lucide-react'
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { type SessionRecord, getSession } from '@/lib/db'
import { type SessionRecord, deleteSession, getSession } from '@/lib/db'
import { EventCard } from './cards'
export function HistoryDetail({ sessionId, onBack }: { sessionId: string; onBack: () => void }) {
export function HistoryDetail({
sessionId,
onBack,
onRerun,
}: {
sessionId: string
onBack: () => void
onRerun: (task: string) => void
}) {
const [session, setSession] = useState<SessionRecord | null>(null)
useEffect(() => {
@@ -37,12 +45,32 @@ export function HistoryDetail({ sessionId, onBack }: { sessionId: string; onBack
<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>
@@ -1,8 +1,17 @@
import { ArrowLeft, CheckCircle, Trash2, XCircle } from 'lucide-react'
import {
ArrowDownToLine,
ArrowLeft,
CheckCircle,
History,
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)
@@ -18,20 +27,26 @@ function timeAgo(ts: number): string {
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)
try {
setSessions(await listSessions())
} catch (err) {
console.error('[HistoryList] Failed to load sessions:', err)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
load()
}, [load])
@@ -41,11 +56,28 @@ export function HistoryList({
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">
<Button
variant="ghost"
size="icon-sm"
onClick={onBack}
className="cursor-pointer"
aria-label="Back"
title="Back"
>
<ArrowLeft className="size-3.5" />
</Button>
<span className="text-sm font-medium flex-1">History</span>
@@ -68,14 +100,23 @@ export function HistoryList({
{/* 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 className="flex flex-col" aria-label="Loading history" aria-busy="true">
{[...Array(4)].map((_, i) => (
<div key={i} className="flex items-start gap-2 px-3 py-2.5 border-b">
<div className="size-3.5 mt-0.5 rounded-full bg-muted animate-pulse shrink-0" />
<div className="flex-1 space-y-1.5">
<div className="h-2.5 bg-muted animate-pulse rounded w-3/4" />
<div className="h-2 bg-muted animate-pulse rounded w-1/3" />
</div>
</div>
))}
</div>
)}
{!loading && sessions.length === 0 && (
<div className="flex items-center justify-center h-32 text-xs text-muted-foreground">
No history yet
<div className="flex flex-col items-center justify-center h-40 gap-2 text-muted-foreground">
<History className="size-8 opacity-30" />
<p className="text-xs">No history yet</p>
</div>
)}
@@ -85,7 +126,6 @@ export function HistoryList({
role="button"
tabIndex={0}
onClick={() => onSelect(session.id)}
onKeyDown={(e) => e.key === 'Enter' && 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 */}
@@ -98,19 +138,41 @@ export function HistoryList({
{/* Content */}
<div className="flex-1 min-w-0">
<p className="text-xs font-medium truncate">{session.task}</p>
<p className="text-[10px] text-muted-foreground mt-0.5">
{timeAgo(session.createdAt)} · {session.history.length} steps
</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>
{/* Delete */}
<button
type="button"
onClick={(e) => handleDelete(e, session.id)}
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:text-destructive cursor-pointer shrink-0"
>
<Trash2 className="size-3" />
</button>
</div>
))}
</div>
+2 -1
View File
@@ -54,7 +54,7 @@ function ResultCard({
Result: {success ? 'Success' : 'Failed'}
</span>
</div>
<p className="text-xs text-[11px] text-muted-foreground pl-5 whitespace-pre-wrap">{text}</p>
<p className="text-[12px] text-foreground pl-5 whitespace-pre-wrap">{text}</p>
{children}
</div>
)
@@ -146,6 +146,7 @@ function CopyButton({ text, label }: { text: string; label: string }) {
function extractPrompt(rawRequest: unknown, role: 'system' | 'user'): string | null {
const messages = (rawRequest as { messages?: { role: string; content?: unknown }[] })?.messages
if (!messages) return null
if (!Array.isArray(messages)) return null
const msg =
role === 'system'
? messages.find((m) => m.role === role)
@@ -14,6 +14,7 @@ export function StatusDot({ status }: { status: AgentStatus }) {
running: 'bg-blue-500',
completed: 'bg-green-500',
error: 'bg-destructive',
stopped: 'bg-muted-foreground',
}[status]
const label = {
@@ -21,6 +22,7 @@ export function StatusDot({ status }: { status: AgentStatus }) {
running: 'Running',
completed: 'Done',
error: 'Error',
stopped: 'Stopped',
}[status]
return (
@@ -111,6 +113,7 @@ export function EmptyState() {
]}
cursorStyle="underscore"
loop
startOnView={false}
typeSpeed={20}
deleteSpeed={10}
pauseDelay={3000}
@@ -1,13 +1,9 @@
import { handlePageControlMessage } from '@/agent/RemotePageController.background'
import { handleTabControlMessage, setupTabChangeEvents } from '@/agent/TabsController.background'
import { handleTabControlMessage } from '@/agent/TabsController.background'
export default defineBackground(() => {
console.log('[Background] Service Worker started')
// tab change events
setupTabChangeEvents()
// generate user auth token
chrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => {
@@ -46,6 +46,8 @@ async function exposeAgentToPage() {
let multiPageAgent: InstanceType<typeof MultiPageAgent> | null = null
window.addEventListener('message', async (e) => {
if (e.source !== window) return
const data = e.data
if (typeof data !== 'object' || data === null) return
if (data.channel !== 'PAGE_AGENT_EXT_REQUEST') return
@@ -70,11 +72,15 @@ async function exposeAgentToPage() {
try {
const { task, config } = payload
const { systemInstruction, ...agentConfig } = config
// Dispose old instance before creating new one
multiPageAgent?.dispose()
multiPageAgent = new MultiPageAgent(config)
multiPageAgent = new MultiPageAgent({
...agentConfig,
instructions: systemInstruction ? { system: systemInstruction } : undefined,
})
// events
@@ -133,7 +133,6 @@ export default function App() {
)}
{history.map((event, index) => (
// eslint-disable-next-line react-x/no-array-index-key
<EventCard key={index} event={event} />
))}
@@ -206,9 +206,9 @@ export function useHubWs(
const [wsState, setWsState] = useState<HubWsState>(() => (wsPort ? 'connecting' : 'disconnected'))
const hubWsRef = useRef<HubWs | null>(null)
const latest = useRef({ execute, stop, configure, config })
const latestRef = useRef({ execute, stop, configure, config })
useEffect(() => {
latest.current = { execute, stop, configure, config }
latestRef.current = { execute, stop, configure, config }
})
useEffect(() => {
@@ -218,14 +218,14 @@ export function useHubWs(
Number(wsPort),
{
onExecute: async (task, incomingConfig) => {
const { execute, configure, config } = latest.current
const { execute, configure, config } = latestRef.current
if (incomingConfig) {
await configure({ ...config, ...incomingConfig } as ExtConfig)
}
const result = await execute(task)
return { success: result.success, data: result.data }
},
onStop: () => latest.current.stop(),
onStop: () => latestRef.current.stop(),
},
setWsState
)
@@ -4,8 +4,14 @@ export type Execute = (task: string, config: ExecuteConfig) => Promise<Execution
export interface ExecuteConfig {
baseURL: string
apiKey: string
model: string
apiKey?: string
/**
* Global system-level instructions for the agent.
* Equivalent to `AgentConfig.instructions.system`.
*/
systemInstruction?: string
/**
* Whether to include the initial tab (that holds this main world script) in the task.
@@ -13,6 +19,9 @@ export interface ExecuteConfig {
*/
includeInitialTab?: boolean
/** Control all unpinned tabs in the window instead of only the tab group. */
experimentalIncludeAllTabs?: boolean
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
@@ -30,13 +39,14 @@ export default defineUnlistedScript(() => {
if (task.trim().length === 0) throw new Error('Task cannot be empty')
if (!config) throw new Error('Config is required')
if (!config.baseURL) throw new Error('Config must have a baseURL')
if (!config.apiKey) throw new Error('Config must have an apiKey')
if (!config.model) throw new Error('Config must have a model')
const id = getId()
const promise = new Promise<ExecutionResult>((resolve, reject) => {
function handleMessage(e: MessageEvent) {
if (e.source !== window) return
const data = e.data
if (typeof data !== 'object' || data === null) return
if (data.channel !== 'PAGE_AGENT_EXT_RESPONSE') return
@@ -85,9 +95,11 @@ export default defineUnlistedScript(() => {
task,
config: {
baseURL: config.baseURL,
apiKey: config.apiKey,
model: config.model,
apiKey: config.apiKey,
systemInstruction: config.systemInstruction,
includeInitialTab: config.includeInitialTab,
experimentalIncludeAllTabs: config.experimentalIncludeAllTabs,
},
},
},
@@ -39,7 +39,7 @@ export default function App() {
if (
prev === 'running' &&
(status === 'completed' || status === 'error') &&
(status === 'completed' || status === 'error' || status === 'stopped') &&
history.length > 0 &&
currentTask
) {
@@ -56,19 +56,27 @@ export default function App() {
}
}, [history, activity])
const handleSubmit = useCallback(
(e?: React.SyntheticEvent) => {
e?.preventDefault()
if (!inputValue.trim() || status === 'running') return
const runTask = useCallback(
(task: string) => {
const normalizedTask = task.trim()
if (!normalizedTask || status === 'running') return
const taskToExecute = inputValue.trim()
setInputValue('')
setView({ name: 'chat' })
execute(taskToExecute).catch((error) => {
execute(normalizedTask).catch((error) => {
console.error('[SidePanel] Failed to execute task:', error)
})
},
[inputValue, status, execute]
[execute, status]
)
const handleSubmit = useCallback(
(e?: React.SyntheticEvent) => {
e?.preventDefault()
runTask(inputValue)
},
[inputValue, runTask]
)
const handleStop = useCallback(() => {
@@ -103,12 +111,19 @@ export default function App() {
<HistoryList
onSelect={(id) => setView({ name: 'history-detail', sessionId: id })}
onBack={() => setView({ name: 'chat' })}
onRerun={runTask}
/>
)
}
if (view.name === 'history-detail') {
return <HistoryDetail sessionId={view.sessionId} onBack={() => setView({ name: 'history' })} />
return (
<HistoryDetail
sessionId={view.sessionId}
onBack={() => setView({ name: 'history' })}
onRerun={runTask}
/>
)
}
// --- Chat view ---
@@ -132,6 +147,8 @@ export default function App() {
size="icon-sm"
onClick={() => setView({ name: 'history' })}
className="cursor-pointer"
aria-label="History"
title="History"
>
<History className="size-3.5" />
</Button>
@@ -140,6 +157,8 @@ export default function App() {
size="icon-sm"
onClick={() => setView({ name: 'config' })}
className="cursor-pointer"
aria-label="Settings"
title="Settings"
>
<Settings className="size-3.5" />
</Button>
@@ -163,7 +182,6 @@ export default function App() {
{showEmptyState && <EmptyState />}
{history.map((event, index) => (
// eslint-disable-next-line react-x/no-array-index-key
<EventCard key={index} event={event} />
))}
@@ -191,6 +209,8 @@ export default function App() {
variant="destructive"
onClick={handleStop}
className="size-7"
aria-label="Stop task"
title="Stop task"
>
<Square className="size-3" />
</InputGroupButton>
@@ -201,6 +221,8 @@ export default function App() {
onClick={() => handleSubmit()}
disabled={!inputValue.trim()}
className="size-7 cursor-pointer"
aria-label="Send"
title="Send"
>
<Send className="size-3" />
</InputGroupButton>
+1 -1
View File
@@ -8,7 +8,7 @@ export interface SessionRecord {
id: string
task: string
history: HistoricalEvent[]
status: 'completed' | 'error'
status: 'completed' | 'error' | 'stopped'
createdAt: number
}
@@ -0,0 +1,60 @@
import type { HistoricalEvent } from '@page-agent/core'
const EXPORT_FILE_PREFIX = 'page-agent-history'
const MAX_TASK_SLUG_LENGTH = 40
export function serializeHistoryExport(history: HistoricalEvent[]): string {
return `${JSON.stringify(history, null, 2)}\n`
}
export function buildHistoryExportFilename(task: string, createdAt: number): string {
const taskSlug = sanitizeTaskForFilename(task)
const timestamp = formatTimestampForFilename(createdAt)
return taskSlug
? `${EXPORT_FILE_PREFIX}-${taskSlug}-${timestamp}.json`
: `${EXPORT_FILE_PREFIX}-${timestamp}.json`
}
export function downloadHistoryExport(
task: string,
createdAt: number,
history: HistoricalEvent[]
): void {
const filename = buildHistoryExportFilename(task, createdAt)
const content = serializeHistoryExport(history)
const blob = new Blob([content], { type: 'application/json;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
link.click()
URL.revokeObjectURL(url)
}
function sanitizeTaskForFilename(task: string): string {
return task
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, MAX_TASK_SLUG_LENGTH)
}
function formatTimestampForFilename(createdAt: number): string {
const date = new Date(createdAt)
const year = date.getFullYear()
const month = pad(date.getMonth() + 1)
const day = pad(date.getDate())
const hours = pad(date.getHours())
const minutes = pad(date.getMinutes())
const seconds = pad(date.getSeconds())
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`
}
function pad(value: number): string {
return value.toString().padStart(2, '0')
}
+3 -18
View File
@@ -3,26 +3,11 @@
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"useDefineForClassFields": true,
"noEmit": false,
"allowImportingTsExtensions": false,
"strictNullChecks": true,
"jsx": "react-jsx",
"baseUrl": ".",
"types": ["node", "chrome"],
"paths": {
// Self root
"@/*": ["src/*"],
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
"@page-agent/core": ["../core/src/PageAgentCore.ts"],
"@page-agent/ui": ["../ui/src/index.ts"]
"@/*": ["./src/*"]
}
},
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" },
{ "path": "../core" },
{ "path": "../ui" }
]
}
}
+21 -10
View File
@@ -1,15 +1,25 @@
{
"name": "@page-agent/llms",
"version": "1.5.10",
"version": "1.12.0",
"type": "module",
"main": "./dist/lib/page-agent-llms.js",
"module": "./dist/lib/page-agent-llms.js",
"types": "./dist/lib/index.d.ts",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./dist/lib/index.d.ts",
"import": "./dist/lib/page-agent-llms.js",
"default": "./dist/lib/page-agent-llms.js"
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"publishConfig": {
"main": "./dist/lib/page-agent-llms.js",
"module": "./dist/lib/page-agent-llms.js",
"types": "./dist/lib/index.d.ts",
"exports": {
".": {
"types": "./dist/lib/index.d.ts",
"import": "./dist/lib/page-agent-llms.js",
"default": "./dist/lib/page-agent-llms.js"
}
}
},
"files": [
@@ -33,8 +43,9 @@
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
"test": "vitest run",
"prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node ../../scripts/post-publish.js"
},
"dependencies": {
"chalk": "^5.6.2"
@@ -43,6 +54,6 @@
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"zod": "^4.3.5"
"zod": "^4.4.3"
}
}
+369
View File
@@ -0,0 +1,369 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as z from 'zod/v4'
import { OpenAIClient } from './OpenAIClient'
import { InvokeError, InvokeErrorTypes } from './errors'
import { parseLLMConfig } from './index'
import type { LLMConfig, Tool } from './types'
// ---------- Fixtures ----------
function makeClient(overrides: Partial<LLMConfig> = {}) {
const fetchMock = vi.fn<typeof fetch>()
const config = parseLLMConfig({
baseURL: 'http://test.local/v1',
model: 'test-model',
apiKey: 'sk-test',
customFetch: fetchMock,
...overrides,
})
const client = new OpenAIClient(config)
return { client, fetchMock }
}
function makeTool(): Tool<{ name: string }, string> {
return {
description: 'greet',
inputSchema: z.object({ name: z.string() }),
execute: vi.fn(async (args) => `hello ${args.name}`),
}
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' },
})
}
function toolCallBody(toolName: string, args: unknown, finishReason = 'tool_calls') {
return {
choices: [
{
finish_reason: finishReason,
message: {
tool_calls: [
{
id: 'call_1',
type: 'function',
function: {
name: toolName,
arguments: typeof args === 'string' ? args : JSON.stringify(args),
},
},
],
},
},
],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
}
}
function abortError(message = 'aborted'): Error {
const err = new Error(message)
err.name = 'AbortError'
return err
}
function getSentBody(fetchMock: ReturnType<typeof vi.fn>): Record<string, unknown> {
const init = fetchMock.mock.calls[0][1] as RequestInit
return JSON.parse(init.body as string)
}
const signal = new AbortController().signal
// ---------- Request construction ----------
describe('OpenAIClient.invoke — request construction', () => {
let setup: ReturnType<typeof makeClient>
const tools = { greet: makeTool() }
beforeEach(() => {
setup = makeClient()
setup.fetchMock.mockResolvedValue(jsonResponse(toolCallBody('greet', { name: 'world' })))
})
it('defaults tool_choice to "required" when no toolChoiceName is given', async () => {
await setup.client.invoke([], tools, signal)
expect(getSentBody(setup.fetchMock).tool_choice).toBe('required')
})
it('uses named tool_choice when toolChoiceName is given', async () => {
await setup.client.invoke([], tools, signal, { toolChoiceName: 'greet' })
expect(getSentBody(setup.fetchMock).tool_choice).toEqual({
type: 'function',
function: { name: 'greet' },
})
})
it('falls back to "required" when disableNamedToolChoice is true', async () => {
const { client, fetchMock } = makeClient({ disableNamedToolChoice: true })
fetchMock.mockResolvedValue(jsonResponse(toolCallBody('greet', { name: 'world' })))
await client.invoke([], tools, signal, { toolChoiceName: 'greet' })
expect(getSentBody(fetchMock).tool_choice).toBe('required')
})
it('sends Authorization header when apiKey is set', async () => {
await setup.client.invoke([], tools, signal)
const init = setup.fetchMock.mock.calls[0][1]!
expect((init.headers as Record<string, string>).Authorization).toBe('Bearer sk-test')
})
it('omits Authorization header when apiKey is empty', async () => {
const { client, fetchMock } = makeClient({ apiKey: '' })
fetchMock.mockResolvedValue(jsonResponse(toolCallBody('greet', { name: 'world' })))
await client.invoke([], tools, signal)
const init = fetchMock.mock.calls[0][1]!
expect((init.headers as Record<string, string>).Authorization).toBeUndefined()
})
it('applies transformRequestBody (in-place form, returns undefined)', async () => {
const { client, fetchMock } = makeClient({
transformRequestBody: (body) => {
body.custom_flag = 42
return undefined
},
})
fetchMock.mockResolvedValue(jsonResponse(toolCallBody('greet', { name: 'world' })))
await client.invoke([], tools, signal)
expect(getSentBody(fetchMock).custom_flag).toBe(42)
})
it('applies transformRequestBody (returning a new object)', async () => {
const { client, fetchMock } = makeClient({
transformRequestBody: () => ({ replaced: true }),
})
fetchMock.mockResolvedValue(jsonResponse(toolCallBody('greet', { name: 'world' })))
await client.invoke([], tools, signal)
expect(getSentBody(fetchMock)).toEqual({ replaced: true })
})
it('wraps transformRequestBody throws as CONFIG_ERROR', async () => {
const { client } = makeClient({
transformRequestBody: () => {
throw new Error('bad transform')
},
})
await expect(client.invoke([], tools, signal)).rejects.toMatchObject({
name: 'InvokeError',
type: InvokeErrorTypes.CONFIG_ERROR,
})
})
})
// ---------- Success path ----------
describe('OpenAIClient.invoke — success', () => {
it('returns toolCall, toolResult and usage', async () => {
const { client, fetchMock } = makeClient()
const tool = makeTool()
fetchMock.mockResolvedValue(jsonResponse(toolCallBody('greet', { name: 'Alice' })))
const result = await client.invoke([], { greet: tool }, signal)
expect(result.toolCall).toEqual({ name: 'greet', args: { name: 'Alice' } })
expect(result.toolResult).toBe('hello Alice')
expect(result.usage).toEqual({
promptTokens: 10,
completionTokens: 5,
totalTokens: 15,
cachedTokens: undefined,
reasoningTokens: undefined,
})
expect(tool.execute).toHaveBeenCalledWith({ name: 'Alice' })
})
it('accepts finish_reason="stop" with tool calls', async () => {
const { client, fetchMock } = makeClient()
fetchMock.mockResolvedValue(jsonResponse(toolCallBody('greet', { name: 'Bob' }, 'stop')))
const result = await client.invoke([], { greet: makeTool() }, signal)
expect(result.toolResult).toBe('hello Bob')
})
})
// ---------- HTTP errors ----------
describe('OpenAIClient.invoke — HTTP errors', () => {
const tools = { greet: makeTool() }
it.each([
[401, InvokeErrorTypes.AUTH_ERROR],
[403, InvokeErrorTypes.AUTH_ERROR],
[429, InvokeErrorTypes.RATE_LIMIT],
[500, InvokeErrorTypes.SERVER_ERROR],
[502, InvokeErrorTypes.SERVER_ERROR],
[418, InvokeErrorTypes.UNKNOWN],
])('maps status %i to %s', async (status, type) => {
const { client, fetchMock } = makeClient()
fetchMock.mockResolvedValue(jsonResponse({ error: { message: 'nope' } }, status))
await expect(client.invoke([], tools, signal)).rejects.toMatchObject({
name: 'InvokeError',
type,
})
})
it('falls back to statusText when body has no error.message', async () => {
const { client, fetchMock } = makeClient()
fetchMock.mockResolvedValue(
new Response('not json', { status: 500, statusText: 'Internal Boom' })
)
await expect(client.invoke([], tools, signal)).rejects.toMatchObject({
type: InvokeErrorTypes.SERVER_ERROR,
message: expect.stringContaining('Internal Boom'),
})
})
})
// ---------- Response anomalies ----------
describe('OpenAIClient.invoke — response anomalies', () => {
const tools = { greet: makeTool() }
it('throws CONTEXT_LENGTH on finish_reason="length"', async () => {
const { client, fetchMock } = makeClient()
fetchMock.mockResolvedValue(
jsonResponse({ choices: [{ finish_reason: 'length', message: {} }] })
)
await expect(client.invoke([], tools, signal)).rejects.toMatchObject({
type: InvokeErrorTypes.CONTEXT_LENGTH,
})
})
it('throws CONTENT_FILTER on finish_reason="content_filter"', async () => {
const { client, fetchMock } = makeClient()
fetchMock.mockResolvedValue(
jsonResponse({ choices: [{ finish_reason: 'content_filter', message: {} }] })
)
await expect(client.invoke([], tools, signal)).rejects.toMatchObject({
type: InvokeErrorTypes.CONTENT_FILTER,
})
})
it('throws INVALID_SCHEMA when there are no choices', async () => {
const { client, fetchMock } = makeClient()
fetchMock.mockResolvedValue(jsonResponse({}))
await expect(client.invoke([], tools, signal)).rejects.toMatchObject({
type: InvokeErrorTypes.INVALID_SCHEMA,
})
})
it('throws NO_TOOL_CALL when message has no tool_calls', async () => {
const { client, fetchMock } = makeClient()
fetchMock.mockResolvedValue(
jsonResponse({ choices: [{ finish_reason: 'tool_calls', message: {} }] })
)
await expect(client.invoke([], tools, signal)).rejects.toMatchObject({
type: InvokeErrorTypes.NO_TOOL_CALL,
})
})
it('throws INVALID_TOOL_ARGS when arguments are not valid JSON', async () => {
const { client, fetchMock } = makeClient()
fetchMock.mockResolvedValue(jsonResponse(toolCallBody('greet', 'not-json{')))
await expect(client.invoke([], tools, signal)).rejects.toMatchObject({
type: InvokeErrorTypes.INVALID_TOOL_ARGS,
message: expect.stringContaining('JSON'),
})
})
it('throws INVALID_TOOL_ARGS when args fail Zod validation', async () => {
const { client, fetchMock } = makeClient()
// tool expects { name: string }, send { name: 123 }
fetchMock.mockResolvedValue(jsonResponse(toolCallBody('greet', { name: 123 })))
await expect(client.invoke([], tools, signal)).rejects.toMatchObject({
type: InvokeErrorTypes.INVALID_TOOL_ARGS,
})
})
it('throws UNKNOWN when model calls a tool that does not exist', async () => {
const { client, fetchMock } = makeClient()
fetchMock.mockResolvedValue(jsonResponse(toolCallBody('mystery', { name: 'x' })))
await expect(client.invoke([], tools, signal)).rejects.toMatchObject({
type: InvokeErrorTypes.UNKNOWN,
message: expect.stringContaining('mystery'),
})
})
it('wraps tool.execute failures as TOOL_EXECUTION_ERROR', async () => {
const { client, fetchMock } = makeClient()
const tool: Tool<{ name: string }, string> = {
inputSchema: z.object({ name: z.string() }),
execute: vi.fn(async () => {
throw new Error('downstream blew up')
}),
}
fetchMock.mockResolvedValue(jsonResponse(toolCallBody('greet', { name: 'x' })))
await expect(client.invoke([], { greet: tool }, signal)).rejects.toMatchObject({
type: InvokeErrorTypes.TOOL_EXECUTION_ERROR,
message: expect.stringContaining('downstream blew up'),
})
})
})
// ---------- Abort handling (the important part) ----------
describe('OpenAIClient.invoke — abort', () => {
const tools = { greet: makeTool() }
it('throws AbortError immediately when signal is already aborted', async () => {
const { client, fetchMock } = makeClient()
const controller = new AbortController()
controller.abort()
await expect(client.invoke([], tools, controller.signal)).rejects.toMatchObject({
name: 'AbortError',
})
expect(fetchMock).not.toHaveBeenCalled()
})
it('propagates AbortError thrown by fetch (does NOT wrap as NETWORK_ERROR)', async () => {
const { client, fetchMock } = makeClient()
fetchMock.mockRejectedValue(abortError())
const err = await client.invoke([], tools, signal).catch((e) => e)
expect(err).toBeInstanceOf(Error)
expect(err).not.toBeInstanceOf(InvokeError)
expect(err.name).toBe('AbortError')
})
it('propagates AbortError thrown by response.json() (does NOT wrap as INVALID_RESPONSE)', async () => {
const { client, fetchMock } = makeClient()
// Fake Response whose .json() rejects with AbortError mid-read
const fakeResponse = {
ok: true,
status: 200,
statusText: 'OK',
json: () => Promise.reject(abortError()),
} as unknown as Response
fetchMock.mockResolvedValue(fakeResponse)
const err = await client.invoke([], tools, signal).catch((e) => e)
expect(err).not.toBeInstanceOf(InvokeError)
expect(err.name).toBe('AbortError')
})
it('propagates AbortError thrown by tool.execute (does NOT wrap as TOOL_EXECUTION_ERROR)', async () => {
const { client, fetchMock } = makeClient()
const tool: Tool<{ name: string }, string> = {
inputSchema: z.object({ name: z.string() }),
execute: vi.fn(async () => {
throw abortError()
}),
}
fetchMock.mockResolvedValue(jsonResponse(toolCallBody('greet', { name: 'x' })))
const err = await client.invoke([], { greet: tool }, signal).catch((e) => e)
expect(err).not.toBeInstanceOf(InvokeError)
expect(err.name).toBe('AbortError')
})
// Sanity check: the wrappers we deliberately bypass for AbortError still work for normal errors
it('still wraps non-Abort fetch errors as NETWORK_ERROR', async () => {
const { client, fetchMock } = makeClient()
fetchMock.mockRejectedValue(new TypeError('ECONNREFUSED'))
await expect(client.invoke([], tools, signal)).rejects.toMatchObject({
name: 'InvokeError',
type: InvokeErrorTypes.NETWORK_ERROR,
})
})
})
+79 -38
View File
@@ -3,18 +3,25 @@
*/
import * as z from 'zod/v4'
import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
import { InvokeError, InvokeErrorTypes } from './errors'
import type {
InvokeOptions,
InvokeResult,
LLMClient,
Message,
ResolvedLLMConfig,
Tool,
} from './types'
import { modelPatch, zodToOpenAITool } from './utils'
/**
* Client for OpenAI compatible APIs
*/
export class OpenAIClient implements LLMClient {
config: Required<LLMConfig>
config: ResolvedLLMConfig
private fetch: typeof globalThis.fetch
constructor(config: Required<LLMConfig>) {
constructor(config: ResolvedLLMConfig) {
this.config = config
this.fetch = config.customFetch
}
@@ -25,23 +32,43 @@ export class OpenAIClient implements LLMClient {
abortSignal?: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult> {
abortSignal?.throwIfAborted()
// 1. Convert tools to OpenAI format
const openaiTools = Object.entries(tools).map(([name, t]) => zodToOpenAITool(name, t))
// Build request body
let toolChoice: unknown = 'required'
if (options?.toolChoiceName && !this.config.disableNamedToolChoice) {
toolChoice = { type: 'function', function: { name: options.toolChoiceName } }
}
const requestBody: Record<string, unknown> = {
model: this.config.model,
temperature: this.config.temperature,
messages,
tools: openaiTools,
parallel_tool_calls: false,
// Require tool call: specific tool if provided, otherwise any tool
tool_choice: options?.toolChoiceName
? { type: 'function', function: { name: options.toolChoiceName } }
: 'required',
tool_choice: toolChoice,
}
// Only sent if the caller explicitly set it. Most new models throw if this is set.
if (this.config.temperature !== undefined) {
requestBody.temperature = this.config.temperature
}
modelPatch(requestBody)
modelPatch(requestBody, this.config.baseURL)
let transformedBody: Record<string, unknown> | undefined
try {
transformedBody = this.config.transformRequestBody(requestBody)
} catch (error) {
throw new InvokeError(
InvokeErrorTypes.CONFIG_ERROR,
`transformRequestBody failed: ${(error as Error).message}`,
error
)
}
const finalRequestBody = transformedBody ?? requestBody
// 2. Call API
let response: Response
@@ -50,58 +77,71 @@ export class OpenAIClient implements LLMClient {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`,
...(this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }),
},
body: JSON.stringify(requestBody),
body: JSON.stringify(finalRequestBody),
signal: abortSignal,
})
} catch (error: unknown) {
const isAbortError = (error as any)?.name === 'AbortError'
const errorMessage = isAbortError ? 'Network request aborted' : 'Network request failed'
if (!isAbortError) console.error(error)
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, errorMessage, error)
if ((error as any)?.name === 'AbortError') throw error
console.error(error)
throw new InvokeError(InvokeErrorTypes.NETWORK_ERROR, 'Network request failed', error)
}
// 3. Handle HTTP errors
if (!response.ok) {
const errorData = await response.json().catch()
const errorMessage =
(errorData as { error?: { message?: string } }).error?.message || response.statusText
let errorData: any
try {
errorData = await response.json()
} catch (error) {
if ((error as any)?.name === 'AbortError') throw error
}
const errorMessage = errorData?.error?.message || response.statusText
if (response.status === 401 || response.status === 403) {
throw new InvokeError(
InvokeErrorType.AUTH_ERROR,
InvokeErrorTypes.AUTH_ERROR,
`Authentication failed: ${errorMessage}`,
errorData
)
}
if (response.status === 429) {
throw new InvokeError(
InvokeErrorType.RATE_LIMIT,
InvokeErrorTypes.RATE_LIMIT,
`Rate limit exceeded: ${errorMessage}`,
errorData
)
}
if (response.status >= 500) {
throw new InvokeError(
InvokeErrorType.SERVER_ERROR,
InvokeErrorTypes.SERVER_ERROR,
`Server error: ${errorMessage}`,
errorData
)
}
throw new InvokeError(
InvokeErrorType.UNKNOWN,
InvokeErrorTypes.UNKNOWN,
`HTTP ${response.status}: ${errorMessage}`,
errorData
)
}
// 4. Parse and validate response
const data = await response.json()
let data: any
try {
data = await response.json()
} catch (error) {
if ((error as any)?.name === 'AbortError') throw error
throw new InvokeError(
InvokeErrorTypes.INVALID_RESPONSE,
'Response body is not valid JSON',
error
)
}
const choice = data.choices?.[0]
if (!choice) {
throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', data)
throw new InvokeError(InvokeErrorTypes.INVALID_SCHEMA, 'No choices in response', data)
}
// Check finish_reason
@@ -112,21 +152,21 @@ export class OpenAIClient implements LLMClient {
break
case 'length':
throw new InvokeError(
InvokeErrorType.CONTEXT_LENGTH,
InvokeErrorTypes.CONTEXT_LENGTH,
'Response truncated: max tokens reached',
undefined,
data
)
case 'content_filter':
throw new InvokeError(
InvokeErrorType.CONTENT_FILTER,
InvokeErrorTypes.CONTENT_FILTER,
'Content filtered by safety system',
undefined,
data
)
default:
throw new InvokeError(
InvokeErrorType.UNKNOWN,
InvokeErrorTypes.INVALID_SCHEMA,
`Unexpected finish_reason: ${choice.finish_reason}`,
undefined,
data
@@ -141,7 +181,7 @@ export class OpenAIClient implements LLMClient {
const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name
if (!toolCallName) {
throw new InvokeError(
InvokeErrorType.NO_TOOL_CALL,
InvokeErrorTypes.NO_TOOL_CALL,
'No tool call found in response',
undefined,
data
@@ -151,7 +191,7 @@ export class OpenAIClient implements LLMClient {
const tool = tools[toolCallName]
if (!tool) {
throw new InvokeError(
InvokeErrorType.UNKNOWN,
InvokeErrorTypes.UNKNOWN,
`Tool "${toolCallName}" not found in tools`,
undefined,
data
@@ -162,7 +202,7 @@ export class OpenAIClient implements LLMClient {
const argString = normalizedChoice.message?.tool_calls?.[0]?.function?.arguments
if (!argString) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
InvokeErrorTypes.INVALID_TOOL_ARGS,
'No tool call arguments found',
undefined,
data
@@ -174,7 +214,7 @@ export class OpenAIClient implements LLMClient {
parsedArgs = JSON.parse(argString)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
InvokeErrorTypes.INVALID_TOOL_ARGS,
'Failed to parse tool arguments as JSON',
error,
data
@@ -186,7 +226,7 @@ export class OpenAIClient implements LLMClient {
if (!validation.success) {
console.error(z.prettifyError(validation.error))
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
InvokeErrorTypes.INVALID_TOOL_ARGS,
'Tool arguments validation failed',
validation.error,
data
@@ -198,11 +238,12 @@ export class OpenAIClient implements LLMClient {
let toolResult: unknown
try {
toolResult = await tool.execute(toolInput)
} catch (e) {
} catch (error: unknown) {
if ((error as any)?.name === 'AbortError') throw error
throw new InvokeError(
InvokeErrorType.TOOL_EXECUTION_ERROR,
`Tool execution failed: ${(e as Error).message}`,
e,
InvokeErrorTypes.TOOL_EXECUTION_ERROR,
`Tool execution failed: ${(error as Error)?.message}`,
error,
data
)
}
@@ -222,7 +263,7 @@ export class OpenAIClient implements LLMClient {
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens,
},
rawResponse: data,
rawRequest: requestBody,
rawRequest: finalRequestBody,
}
}
}
-3
View File
@@ -1,3 +0,0 @@
// Internal constants
export const LLM_MAX_RETRIES = 2
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
+19 -20
View File
@@ -1,8 +1,8 @@
/**
* Error types and error handling for LLM invocations
* Error types and error handling for LLM invocations.
*/
export const InvokeErrorType = {
export const InvokeErrorTypes = {
// Retryable
NETWORK_ERROR: 'network_error', // Network error, retry
RATE_LIMIT: 'rate_limit', // Rate limit, retry
@@ -10,16 +10,31 @@ export const InvokeErrorType = {
NO_TOOL_CALL: 'no_tool_call', // Model did not call tool
INVALID_TOOL_ARGS: 'invalid_tool_args', // Tool args don't match schema
TOOL_EXECUTION_ERROR: 'tool_execution_error', // Tool execution error
INVALID_RESPONSE: 'invalid_response', // Response body is not valid JSON
INVALID_SCHEMA: 'invalid_schema', // Response is valid JSON but doesn't match expected shape
UNKNOWN: 'unknown',
// Non-retryable
CONFIG_ERROR: 'config_error', // Invalid local configuration or hook
AUTH_ERROR: 'auth_error', // Authentication failed
CONTEXT_LENGTH: 'context_length', // Prompt too long
CONTENT_FILTER: 'content_filter', // Content filtered
} as const
export type InvokeErrorType = (typeof InvokeErrorType)[keyof typeof InvokeErrorType]
type InvokeErrorType = (typeof InvokeErrorTypes)[keyof typeof InvokeErrorTypes]
const RETRYABLE_TYPES: readonly InvokeErrorType[] = [
InvokeErrorTypes.NETWORK_ERROR,
InvokeErrorTypes.RATE_LIMIT,
InvokeErrorTypes.SERVER_ERROR,
InvokeErrorTypes.NO_TOOL_CALL,
InvokeErrorTypes.INVALID_TOOL_ARGS,
InvokeErrorTypes.TOOL_EXECUTION_ERROR,
InvokeErrorTypes.INVALID_RESPONSE,
InvokeErrorTypes.INVALID_SCHEMA,
InvokeErrorTypes.UNKNOWN,
]
export class InvokeError extends Error {
type: InvokeErrorType
@@ -34,24 +49,8 @@ export class InvokeError extends Error {
super(message)
this.name = 'InvokeError'
this.type = type
this.retryable = this.isRetryable(type, rawError)
this.retryable = RETRYABLE_TYPES.includes(type)
this.rawError = rawError
this.rawResponse = rawResponse
}
private isRetryable(type: InvokeErrorType, rawError?: unknown): boolean {
const isAbortError = (rawError as any)?.name === 'AbortError'
if (isAbortError) return false
const retryableTypes: InvokeErrorType[] = [
InvokeErrorType.NETWORK_ERROR,
InvokeErrorType.RATE_LIMIT,
InvokeErrorType.SERVER_ERROR,
InvokeErrorType.NO_TOOL_CALL,
InvokeErrorType.INVALID_TOOL_ARGS,
InvokeErrorType.TOOL_EXECUTION_ERROR,
InvokeErrorType.UNKNOWN,
]
return retryableTypes.includes(type)
}
}
+114
View File
@@ -0,0 +1,114 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { InvokeError, InvokeErrorTypes, LLM } from './index'
import type { LLMClient } from './types'
function makeLLM(maxRetries = 2): LLM {
return new LLM({
baseURL: 'http://test.local/v1',
model: 'gpt-5',
maxRetries,
})
}
function abortError(): Error {
const err = new Error('aborted')
err.name = 'AbortError'
return err
}
describe('LLM.invoke retry behavior', () => {
let llm: LLM
let client: { invoke: ReturnType<typeof vi.fn> }
const signal = new AbortController().signal
beforeEach(() => {
llm = makeLLM(2)
client = { invoke: vi.fn() }
llm.client = client as unknown as LLMClient
})
it('returns immediately on first success', async () => {
client.invoke.mockResolvedValueOnce('ok')
const retryListener = vi.fn()
llm.addEventListener('retry', retryListener)
await expect(llm.invoke([], {}, signal)).resolves.toBe('ok')
expect(client.invoke).toHaveBeenCalledOnce()
expect(retryListener).not.toHaveBeenCalled()
})
it('retries up to maxRetries on retryable errors, then throws last error', async () => {
const retryable = new InvokeError(InvokeErrorTypes.NETWORK_ERROR, 'boom')
client.invoke
.mockRejectedValueOnce(retryable)
.mockRejectedValueOnce(retryable)
.mockRejectedValueOnce(retryable)
await expect(llm.invoke([], {}, signal)).rejects.toBe(retryable)
// 1 initial + 2 retries = 3 attempts total
expect(client.invoke).toHaveBeenCalledTimes(3)
})
it('succeeds on retry after transient failure', async () => {
const retryable = new InvokeError(InvokeErrorTypes.RATE_LIMIT, 'slow down')
client.invoke.mockRejectedValueOnce(retryable).mockResolvedValueOnce('ok')
await expect(llm.invoke([], {}, signal)).resolves.toBe('ok')
expect(client.invoke).toHaveBeenCalledTimes(2)
})
it('emits "retry" events with attempt count and lastError', async () => {
const err1 = new InvokeError(InvokeErrorTypes.NETWORK_ERROR, 'first')
const err2 = new InvokeError(InvokeErrorTypes.NETWORK_ERROR, 'second')
client.invoke
.mockRejectedValueOnce(err1)
.mockRejectedValueOnce(err2)
.mockResolvedValueOnce('ok')
const events: { attempt: number; maxAttempts: number; lastError: Error }[] = []
llm.addEventListener('retry', (e) => {
events.push((e as CustomEvent).detail)
})
await llm.invoke([], {}, signal)
expect(events).toEqual([
{ attempt: 1, maxAttempts: 2, lastError: err1 },
{ attempt: 2, maxAttempts: 2, lastError: err2 },
])
})
it('does not retry on AbortError, throws immediately', async () => {
const err = abortError()
client.invoke.mockRejectedValueOnce(err)
await expect(llm.invoke([], {}, signal)).rejects.toBe(err)
expect(client.invoke).toHaveBeenCalledOnce()
})
it('does not retry on non-retryable InvokeError (AUTH_ERROR)', async () => {
const err = new InvokeError(InvokeErrorTypes.AUTH_ERROR, 'bad token')
client.invoke.mockRejectedValueOnce(err)
await expect(llm.invoke([], {}, signal)).rejects.toBe(err)
expect(client.invoke).toHaveBeenCalledOnce()
})
it('does not retry on non-retryable InvokeError (CONFIG_ERROR)', async () => {
const err = new InvokeError(InvokeErrorTypes.CONFIG_ERROR, 'bad config')
client.invoke.mockRejectedValueOnce(err)
await expect(llm.invoke([], {}, signal)).rejects.toBe(err)
expect(client.invoke).toHaveBeenCalledOnce()
})
it('retries plain (non-InvokeError) errors as unknown failures', async () => {
// Plain errors are treated as retryable by withRetry (only InvokeError carries retryable flag)
const plain = new TypeError('weird')
client.invoke.mockRejectedValueOnce(plain).mockResolvedValueOnce('ok')
await expect(llm.invoke([], {}, signal)).resolves.toBe('ok')
expect(client.invoke).toHaveBeenCalledTimes(2)
})
})
+62 -65
View File
@@ -1,32 +1,23 @@
import { OpenAIClient } from './OpenAIClient'
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
import { InvokeError, InvokeErrorTypes } from './errors'
import type {
InvokeOptions,
InvokeResult,
LLMClient,
LLMConfig,
Message,
ResolvedLLMConfig,
Tool,
} from './types'
export { InvokeError, InvokeErrorType }
export { InvokeError, InvokeErrorTypes }
export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
// Runtime validation as defensive programming (types already guarantee these)
if (!config.baseURL || !config.apiKey || !config.model) {
throw new Error(
'[PageAgent] LLM configuration required. Please provide: baseURL, apiKey, model. ' +
'See: https://alibaba.github.io/page-agent/docs/features/models'
)
}
return {
baseURL: config.baseURL,
apiKey: config.apiKey,
model: config.model,
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
}
}
/**
* LLM module
*/
export class LLM extends EventTarget {
config: Required<LLMConfig>
config: ResolvedLLMConfig
client: LLMClient
constructor(config: LLMConfig) {
@@ -48,65 +39,71 @@ export class LLM extends EventTarget {
abortSignal: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult> {
return await withRetry(
async () => {
// in case user aborted before invoking
if (abortSignal.aborted) throw new Error('AbortError')
const result = await this.client.invoke(messages, tools, abortSignal, options)
return result
return await withRetry(async () => this.client.invoke(messages, tools, abortSignal, options), {
maxRetries: this.config.maxRetries,
onRetry: (attempt, lastError) => {
this.dispatchEvent(
new CustomEvent('retry', {
detail: { attempt, maxAttempts: this.config.maxRetries, lastError },
})
)
},
// retry settings
{
maxRetries: this.config.maxRetries,
onRetry: (attempt: number) => {
this.dispatchEvent(
new CustomEvent('retry', { detail: { attempt, maxAttempts: this.config.maxRetries } })
)
},
onError: (error: Error) => {
this.dispatchEvent(new CustomEvent('error', { detail: { error } }))
},
}
)
})
}
}
/**
* Retry a function until it succeeds or reaches the maximum number of retries.
*/
async function withRetry<T>(
fn: () => Promise<T>,
settings: {
maxRetries: number
onRetry: (attempt: number) => void
onError: (error: Error) => void
onRetry: (attempt: number, lastError: Error) => void
}
): Promise<T> {
let attempt = 0
let lastError: Error | null = null
while (attempt <= settings.maxRetries) {
if (attempt > 0) {
settings.onRetry(attempt)
await new Promise((resolve) => setTimeout(resolve, 100))
}
while (true) {
try {
return await fn()
} catch (error: unknown) {
// do not retry if aborted by user
if ((error as any)?.rawError?.name === 'AbortError') throw error
console.error(error)
settings.onError(error as Error)
// do not retry if error is not retryable (InvokeError)
if ((error as any)?.name === 'AbortError') throw error
if (error instanceof InvokeError && !error.retryable) throw error
lastError = error as Error
attempt++
if (attempt > settings.maxRetries) throw error
console.debug('[LLM] retryable failure, will retry:', error)
settings.onRetry(attempt, error as Error)
await new Promise((resolve) => setTimeout(resolve, 100))
}
}
throw lastError!
}
export function parseLLMConfig(config: LLMConfig): ResolvedLLMConfig {
// Runtime validation as defensive programming (types already guarantee these)
if (!config.baseURL || !config.model) {
throw new Error(
'[PageAgent] LLM configuration required. Please provide: baseURL, model. ' +
'See: https://alibaba.github.io/page-agent/docs/features/models'
)
}
if (config.temperature !== undefined) {
console.warn(
'[PageAgent] LLMConfig.temperature is deprecated and will be removed in a future version. ' +
'Use transformRequestBody to set it only for models you have verified accept it.'
)
}
return {
baseURL: config.baseURL,
model: config.model,
apiKey: config.apiKey || '',
temperature: config.temperature,
maxRetries: config.maxRetries ?? 2,
transformRequestBody: config.transformRequestBody ?? ((requestBody) => requestBody),
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
}
}
+224
View File
@@ -0,0 +1,224 @@
/**
* Live compatibility test hits real provider APIs.
*
* Purpose: verify request formatting and `modelPatch` (see `utils.ts`) work
* for every model below, across the providers that serve them. Not a
* correctness/quality eval the tool call is trivial and forced via
* `toolChoiceName`, so a failure here means the request/response shape is
* wrong for that model, not that the model is "dumb".
*
* Tests `OpenAIClient` directly (not the `LLM` retry wrapper), so a failure
* always reflects the very first request/response no retry can mask it.
*
* Each provider's tests skip (not fail) when its `TESTING_*_KEY` env var is
* absent, so this stays CI-safe. To actually run these, put keys in the
* repo-root `.env`:
*
* TESTING_OPENROUTER_KEY=...
* TESTING_DEEPSEEK_KEY=...
* TESTING_ALIYUN_KEY=...
*/
import { config as dotenvConfig } from 'dotenv'
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { describe, expect, it } from 'vitest'
import * as z from 'zod/v4'
import { OpenAIClient } from './OpenAIClient'
import { parseLLMConfig } from './index'
import type { Message, Tool } from './types'
const __dirname = dirname(fileURLToPath(import.meta.url))
dotenvConfig({ path: resolve(__dirname, '../../../.env'), quiet: true })
const TEST_TIMEOUT = 30_000
/**
* Mirrors `packages/website/src/pages/docs/features/models/page.tsx`.
* This package cannot depend on the website, so the list is duplicated here.
* Keep both lists in sync manually when models are added or renamed.
*/
const MODEL_GROUPS: Record<string, string[]> = {
Qwen: [
'qwen3.7-max',
'qwen3.7-plus',
'qwen3.6-max',
'qwen3.6-plus',
'qwen3.6-flash',
'qwen3.5-plus',
'qwen3.5-flash',
'qwen3-max',
],
OpenAI: [
'gpt-5.5',
'gpt-5.4',
'gpt-5.4-mini',
'gpt-5.4-nano',
'gpt-5.2',
'gpt-5.1',
'gpt-5',
'gpt-5-mini',
'gpt-4.1',
'gpt-4.1-mini',
],
DeepSeek: ['deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-3.2'],
Google: [
'gemini-3.5-flash',
'gemini-3.1-pro',
'gemini-3.1-flash-lite',
'gemini-2.5-pro',
'gemini-2.5-flash',
],
Anthropic: [
'claude-sonnet-5',
'claude-opus-4-8',
'claude-opus-4-7',
'claude-opus-4-6',
'claude-opus-4-5',
'claude-sonnet-4-5',
'claude-haiku-4-5',
],
MiniMax: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.5'],
xAI: ['grok-4.3', 'grok-build-0.1'],
MoonshotAI: ['kimi-k2.7-code', 'kimi-k2.6', 'kimi-k2.5'],
'Z.AI': ['glm-5.2', 'glm-5.1', 'glm-5', 'glm-4.7'],
}
/**
* OpenRouter lists every model as `<vendor-slug>/<model-id>`, lowercase.
* See the commented-out entries in the repo-root `.env` for real examples,
* e.g. `x-ai/grok-4.1-fast`, `qwen/qwen3-coder-next`, `deepseek/deepseek-v3.2-exp`.
*/
const OPENROUTER_VENDOR_SLUG: Record<string, string> = {
Qwen: 'qwen',
OpenAI: 'openai',
DeepSeek: 'deepseek',
Google: 'google',
Anthropic: 'anthropic',
MiniMax: 'minimax',
xAI: 'x-ai',
MoonshotAI: 'moonshotai',
'Z.AI': 'z-ai',
}
/**
* Overrides for models whose OpenRouter id doesn't match the
* `<vendor-slug>/<lowercased-name>` heuristic dated snapshots, "-preview"
* suffixes, "v"-prefixed versions, or dots instead of hyphens in the
* version number. Verified against `GET https://openrouter.ai/api/v1/models`
* on 2026-07-03; re-check when models are added to `MODEL_GROUPS`.
*/
const OPENROUTER_ID_OVERRIDES: Record<string, string> = {
'qwen3.6-max': 'qwen/qwen3.6-max-preview',
'qwen3.5-plus': 'qwen/qwen3.5-plus-20260420',
'qwen3.5-flash': 'qwen/qwen3.5-flash-02-23',
'deepseek-3.2': 'deepseek/deepseek-v3.2',
'gemini-3.1-pro': 'google/gemini-3.1-pro-preview',
'claude-opus-4-8': 'anthropic/claude-opus-4.8',
'claude-opus-4-7': 'anthropic/claude-opus-4.7',
'claude-opus-4-6': 'anthropic/claude-opus-4.6',
'claude-opus-4-5': 'anthropic/claude-opus-4.5',
'claude-sonnet-4-5': 'anthropic/claude-sonnet-4.5',
'claude-haiku-4-5': 'anthropic/claude-haiku-4.5',
}
/**
* Derive a model's OpenRouter id from its brand (a `MODEL_GROUPS` key) and
* native model name.
*/
function toOpenRouterModelId(brand: string, model: string): string {
if (model in OPENROUTER_ID_OVERRIDES) return OPENROUTER_ID_OVERRIDES[model]
const slug = OPENROUTER_VENDOR_SLUG[brand]
if (!slug) throw new Error(`No OpenRouter vendor slug mapped for brand "${brand}"`)
return `${slug}/${model.toLowerCase()}`
}
const PROVIDERS = {
openrouter: {
baseURL: 'https://openrouter.ai/api/v1',
apiKey: process.env.TESTING_OPENROUTER_KEY,
},
aliyun: {
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: process.env.TESTING_ALIYUN_KEY,
},
deepseek: {
baseURL: 'https://api.deepseek.com',
apiKey: process.env.TESTING_DEEPSEEK_KEY,
},
} as const
const ECHO_TOOL: Tool<{ message: string }, string> = {
description: 'Echo back the given message in uppercase.',
inputSchema: z.object({ message: z.string() }),
execute: async ({ message }) => message.toUpperCase(),
}
const PROMPT: Message[] = [
{
role: 'user',
content: 'Call the "echo" tool with message set to "ping". You must call the tool.',
},
]
async function expectEchoToolCall(baseURL: string, apiKey: string, model: string) {
const client = new OpenAIClient(parseLLMConfig({ baseURL, apiKey, model }))
const result = await client.invoke(PROMPT, { echo: ECHO_TOOL }, new AbortController().signal, {
toolChoiceName: 'echo',
})
expect(result.toolResult).toBe('PING')
}
describe.concurrent('OpenRouter — all listed models', () => {
const { baseURL, apiKey } = PROVIDERS.openrouter
for (const [brand, models] of Object.entries(MODEL_GROUPS)) {
for (const model of models) {
const id = toOpenRouterModelId(brand, model)
it.skipIf(!apiKey)(
id,
async () => {
await expectEchoToolCall(baseURL, apiKey!, id)
},
TEST_TIMEOUT
)
}
}
})
// Aliyun native ids that don't match the display name in MODEL_GROUPS.
const ALIYUN_ID_OVERRIDES: Record<string, string> = {
'qwen3.6-max': 'qwen3.6-max-preview',
}
describe.concurrent('Aliyun DashScope — Qwen native', () => {
const { baseURL, apiKey } = PROVIDERS.aliyun
for (const model of MODEL_GROUPS.Qwen) {
const id = ALIYUN_ID_OVERRIDES[model] ?? model
it.skipIf(!apiKey)(
id,
async () => {
await expectEchoToolCall(baseURL, apiKey!, id)
},
TEST_TIMEOUT
)
}
})
describe.concurrent('DeepSeek — native', () => {
const { baseURL, apiKey } = PROVIDERS.deepseek
// deepseek-3.2 isn't served on DeepSeek's own API (only via OpenRouter
// resellers) — its official API only accepts deepseek-v4-pro/-flash.
const nativeModels = MODEL_GROUPS.DeepSeek.filter((model) => model !== 'deepseek-3.2')
for (const model of nativeModels) {
it.skipIf(!apiKey)(
model,
async () => {
await expectEchoToolCall(baseURL, apiKey!, model)
},
TEST_TIMEOUT
)
}
})
+24 -1
View File
@@ -89,12 +89,33 @@ export interface InvokeResult<TResult = unknown> {
*/
export interface LLMConfig {
baseURL: string
apiKey: string
model: string
apiKey?: string
/**
* @deprecated No longer a standard parameter; many models reject it outright.
* Use `transformRequestBody` to set it only for models you've verified.
*/
temperature?: number
maxRetries?: number
/**
* Transform the final request body before sending it to the provider.
* Use this to implement provider-specific request tweaks such as caching hints or custom flags.
*
* Return a new object, or mutate the input object and return undefined.
*/
transformRequestBody?: (
requestBody: Record<string, unknown>
) => Record<string, unknown> | undefined
/**
* remove the tool_choice field from the request.
* @note fix "Invalid tool_choice type: 'object'" for some LLMs.
*/
disableNamedToolChoice?: boolean
/**
* Custom fetch function for LLM API requests.
* Use this to customize headers, credentials, proxy, etc.
@@ -102,3 +123,5 @@ export interface LLMConfig {
*/
customFetch?: typeof globalThis.fetch
}
export type ResolvedLLMConfig = Required<Omit<LLMConfig, 'temperature'>> & { temperature?: number }
+15
View File
@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest'
import { modelPatch, normalizeModelName } from './utils'
describe('normalizeModelName', () => {
it.each([
['gpt-5.2', 'gpt-52'],
['gpt_5_2', 'gpt52'],
['GPT-52-2026-01-01', 'gpt-52-2026-01-01'],
['openai/gpt-5.2-chat', 'gpt-52-chat'],
['claude_sonnet4_5', 'claudesonnet45'],
])('%s -> %s', (input, expected) => {
expect(normalizeModelName(input)).toBe(expected)
})
})
+133 -51
View File
@@ -24,82 +24,152 @@ export function zodToOpenAITool(name: string, tool: Tool) {
}
/**
* Patch model specific parameters
* @note in-place modification
* Patch model specific parameters. Only patches known models.
*
* @purpose
* - Reconcile the differences in the parameter schema each model accepts.
* - Disable thinking/reasoning, or lower it to the minimum where a full disable is impossible.
* - Minimize returned tokens.
* - Raise temperature for known smaller models to improve auto-recovery odds.
* @note Honor temperature if explicitly set by the user
*
* @todo Need vendor-specific patches.
* Local and 3rd-party hosted models may have different schema.
*/
export function modelPatch(body: Record<string, any>) {
export function modelPatch(body: Record<string, any>, baseURL?: string) {
const model: string = body.model || ''
if (!model) return body
const provider = getProvider(baseURL)
const modelName = normalizeModelName(model)
if (modelName.startsWith('qwen')) {
debug('Applying Qwen patch: use higher temperature for auto fixing')
body.temperature = Math.max(body.temperature || 0, 1.0)
debug('Patch Qwen: disable thinking')
body.enable_thinking = false
}
if (modelName.startsWith('claude')) {
debug('Applying Claude patch: disable thinking')
body.thinking = { type: 'disabled' }
// Convert tool_choice to Claude format
if (body.tool_choice === 'required') {
// 'required' -> { type: 'any' } (must call some tool)
debug('Applying Claude patch: convert tool_choice "required" to { type: "any" }')
body.tool_choice = { type: 'any' }
} else if (body.tool_choice?.function?.name) {
// { type: 'function', function: { name: '...' } } -> { type: 'tool', name: '...' }
debug('Applying Claude patch: convert tool_choice format')
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
if (body.temperature === undefined && !/max|plus/.test(modelName)) {
debug('Patch Qwen: raise temperature to 1.0')
body.temperature = 1.0
}
}
if (modelName.startsWith('grok')) {
debug('Applying Grok patch: removing tool_choice')
if (modelName.startsWith('deepseek')) {
debug('Patch DeepSeek: disable thinking, remove tool_choice')
body.thinking = { type: 'disabled' }
delete body.tool_choice
debug('Applying Grok patch: disable reasoning and thinking')
body.thinking = { type: 'disabled', effort: 'minimal' }
body.reasoning = { enabled: false, effort: 'low' }
}
if (modelName.startsWith('gpt')) {
debug('Applying GPT patch: set verbosity to low')
body.verbosity = 'low'
if (modelName.startsWith('gpt-5')) {
body.verbosity = 'low'
if (modelName.startsWith('gpt-52')) {
debug('Applying GPT-52 patch: disable reasoning')
body.reasoning_effort = 'none'
} else if (modelName.startsWith('gpt-51')) {
debug('Applying GPT-51 patch: disable reasoning')
body.reasoning_effort = 'none'
} else if (modelName.startsWith('gpt-54')) {
debug(
'Applying GPT-5.4 patch: skip reasoning_effort because chat/completions rejects it with function tools'
)
// gpt-5 gpt-5-mini gpt-5-nano only supports "minimal";
// 5.1+ supports "none".
body.reasoning_effort = /^gpt-5(-|$)/.test(modelName) ? 'minimal' : 'none'
debug(`Patch GPT-5: verbosity=low, reasoning_effort=${body.reasoning_effort}`)
}
if (modelName.includes('chat-latest')) {
debug('Omitting reasoning_effort and temperature for chat-latest')
delete body.reasoning_effort
} else if (modelName.startsWith('gpt-5-mini')) {
debug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1')
body.reasoning_effort = 'low'
body.temperature = 1
} else if (modelName.startsWith('gpt-5')) {
debug('Applying GPT-5 patch: set reasoning effort to low')
delete body.temperature
}
}
if (modelName.startsWith('claude')) {
if (/opus|sonnet|haiku/.test(modelName)) {
debug('Patch Claude: disable thinking')
body.thinking = { type: 'disabled' }
if (provider !== 'openrouter') {
// Convert tool_choice to Claude format
if (body.tool_choice === 'required') {
// 'required' -> { type: 'any' } (must call some tool)
debug('Applying Claude patch: convert tool_choice "required" to { type: "any" }')
body.tool_choice = { type: 'any' }
} else if (body.tool_choice?.function?.name) {
// { type: 'function', function: { name: '...' } } -> { type: 'tool', name: '...' }
debug('Applying Claude patch: convert tool_choice format')
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
}
}
} else {
debug('Patch Claude: reasoning_effort=low')
body.reasoning_effort = 'low'
// Fable and mythos can not disable adaptive thinking.
// Claude does not support tool_choice with extended thinking.
// These 2 concepts are blurred. Basically no tool_choice with thinking.
delete body.tool_choice
}
}
if (modelName.startsWith('gemini')) {
debug('Applying Gemini patch: set reasoning effort to minimal')
body.reasoning_effort = 'minimal'
debug('Patch Gemini: reasoning_effort=low')
body.reasoning_effort = 'low'
if (/^gemini-25(?!.*pro)/.test(modelName)) {
debug('Patch Gemini 2.5 non-Pro: reasoning_effort=none')
body.reasoning_effort = 'none'
} else if (
modelName.startsWith('gemini-35-flash') ||
modelName.startsWith('gemini-31-flash-lite') ||
modelName.startsWith('gemini-3-flash')
) {
debug('Patch Gemini 3.x Flash/Lite: reasoning_effort=minimal')
body.reasoning_effort = 'minimal'
}
}
if (modelName.startsWith('glm')) {
debug('Patch GLM: disable thinking')
body.thinking = { type: 'disabled' }
}
if (modelName.startsWith('grok')) {
if (/^grok-4-?3/.test(modelName)) {
debug('Patch Grok 4.3: reasoning_effort=none')
body.reasoning_effort = 'none'
} else if (modelName.startsWith('grok-3-mini') || modelName.startsWith('grok-code-fast')) {
debug('Patch Grok mini/code: reasoning_effort=low')
body.reasoning_effort = 'low'
}
}
if (modelName.startsWith('kimi')) {
if (!modelName.includes('code')) {
// kimi-k2.7-code cannot disable thinking
debug('Patch Kimi: disable thinking')
body.thinking = { type: 'disabled' }
}
}
if (modelName.startsWith('minimax')) {
debug('Applying MiniMax patch: clamp temperature to (0, 1]')
// MiniMax API rejects temperature = 0; clamp to a small positive value
body.temperature = Math.max(body.temperature || 0, 0.01)
if (body.temperature > 1) body.temperature = 1
// MiniMax does not support parallel_tool_calls
debug('Patch MiniMax: remove parallel_tool_calls')
delete body.parallel_tool_calls
if (modelName.includes('m3')) {
// Only M3 can disable thinking
debug('Patch MiniMax: disable thinking')
body.thinking = { type: 'disabled' }
}
}
// provider patches
if (provider === 'openrouter') {
// openrouter use reasoning object instead of reasoning_effort
const reasoningEffort = body.reasoning_effort
const reasoningDisabled =
body.thinking?.type === 'disabled' ||
body.enable_thinking === false ||
reasoningEffort === 'none'
if (reasoningDisabled) {
body.reasoning = { enabled: false }
} else if (reasoningEffort) {
body.reasoning = { enabled: true, effort: reasoningEffort }
}
}
return body
@@ -120,7 +190,7 @@ export function modelPatch(body: Record<string, any>) {
* They should be treated as the same model.
* Normalize them to `gpt-52`
*/
function normalizeModelName(modelName: string): string {
export function normalizeModelName(modelName: string): string {
let normalizedName = modelName.toLowerCase()
// remove prefix before '/'
@@ -136,3 +206,15 @@ function normalizeModelName(modelName: string): string {
return normalizedName
}
export function getProvider(baseURL?: string): 'openrouter' | undefined {
if (!baseURL) return undefined
try {
const url = new URL(baseURL)
const hostname = url.hostname
if (hostname === 'openrouter.ai') return 'openrouter'
return undefined
} catch (e) {
return undefined
}
}
-9
View File
@@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+2 -6
View File
@@ -1,12 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
},
"include": ["**/*.ts"],
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"]
}
+13 -4
View File
@@ -11,11 +11,20 @@ console.log(chalk.cyan(`📦 Building @page-agent/llms`))
export default defineConfig({
clearScreen: false,
plugins: [dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true })],
plugins: [
dts({
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts'],
bundleTypes: true,
compilerOptions: {
composite: true,
noEmit: false,
emitDeclarationOnly: true,
declaration: true,
},
}),
],
publicDir: false,
esbuild: {
keepNames: true,
},
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
name: 'llms',
include: ['src/**/*.test.ts'],
// Keep live provider suites under OpenRouter's ~20 req/min free-route cap.
maxConcurrency: 2,
// Suppress console output from passing tests; failed tests still get their logs.
silent: 'passed-only',
},
})
+4 -5
View File
@@ -36,11 +36,11 @@ Same format — add the config to the MCP settings of your client.
## MCP Tools
| Tool | Input | Description |
| -------------- | ------------------ | ---------------------------------------------------- |
| Tool | Input | Description |
| -------------- | ------------------ | ----------------------------------------------------- |
| `execute_task` | `{ task: string }` | Execute a browser task in natural language. Blocking. |
| `get_status` | — | Returns `{ connected, busy }` |
| `stop_task` | — | Stop the currently running task. |
| `get_status` | — | Returns `{ connected, busy }` |
| `stop_task` | — | Stop the currently running task. |
## Environment Variables
@@ -88,7 +88,6 @@ src/
## Dev
```bash
npm run build:libs
npm run dev:ext
npx @modelcontextprotocol/inspector node packages/mcp/src/index.js
```
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/mcp",
"private": false,
"version": "1.5.10",
"version": "1.12.0",
"type": "module",
"bin": {
"page-agent-mcp": "src/index.js"
@@ -28,8 +28,8 @@
"node": ">=20"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1",
"ws": "^8.19.0",
"zod": "^4.3.5"
"@modelcontextprotocol/sdk": "^1.29.0",
"ws": "^8.21.0",
"zod": "^4.4.3"
}
}
+3 -2
View File
@@ -6,6 +6,7 @@ import { WebSocketServer } from 'ws'
const EXT_ID = 'akldabonmimlicnjlflnapfeklbfemhj'
const STORE_URL = `https://chromewebstore.google.com/detail/page-agent-ext/${EXT_ID}`
const LOOPBACK_HOST = 'localhost'
const launcherTemplate = readFileSync(
fileURLToPath(new URL('./launcher.html', import.meta.url)),
@@ -60,8 +61,8 @@ export class HubBridge {
reject(err)
}
})
this.#httpServer.listen(this.port, () => {
console.error(`[page-agent-mcp] HTTP + WS on http://localhost:${this.port}`)
this.#httpServer.listen(this.port, LOOPBACK_HOST, () => {
console.error(`[page-agent-mcp] HTTP + WS on http://${LOOPBACK_HOST}:${this.port}`)
resolve()
})
})
+14 -9
View File
@@ -2,6 +2,7 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { exec } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { platform } from 'node:os'
import * as z from 'zod/v4'
@@ -9,12 +10,13 @@ import { HubBridge } from './hub-bridge.js'
const env = process.env
const port = parseInt(env.PORT || '38401')
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
/** @type {Record<string, string>} */
const llmConfig = {}
if (env.LLM_API_KEY) llmConfig.apiKey = env.LLM_API_KEY
if (env.LLM_BASE_URL) llmConfig.baseURL = env.LLM_BASE_URL
if (env.LLM_MODEL_NAME) llmConfig.model = env.LLM_MODEL_NAME
if (env.LLM_API_KEY) llmConfig.apiKey = env.LLM_API_KEY
// --- Hub bridge (HTTP + WebSocket) ---
@@ -30,16 +32,19 @@ exec(`${cmd} "${url}"`, (err) => {
// --- MCP server (stdio) ---
const mcpServer = new McpServer({ name: 'page-agent', version: '1.5.8' })
const mcpServer = new McpServer({ name: 'page-agent', version })
mcpServer.registerTool(
'execute_task',
{
description:
'Execute a browser automation task described in natural language. ' +
'The Page Agent extension will control the browser to complete the task. ' +
'Blocks until the task is complete.',
inputSchema: { task: z.string().describe('Task description in natural language') },
description: "Execute a task in user's browser.",
inputSchema: {
task: z
.string()
.describe(
'Task description. Give specific instructions for the task. Steps preferable. And the information you want to get after the task is done.'
),
},
},
async ({ task }) => {
try {
@@ -50,7 +55,7 @@ mcpServer.registerTool(
{
type: 'text',
text: result.success
? `Task completed successfully.\n\n${result.data}`
? `Task completed.\n\n${result.data}`
: `Task failed.\n\n${result.data}`,
},
],
@@ -67,7 +72,7 @@ mcpServer.registerTool(
mcpServer.registerTool(
'get_status',
{
description: 'Check the current status of the Page Agent hub connection and agent.',
description: 'Check the current status of the Page Agent hub.',
},
async () => ({
content: [
+21 -16
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://img.alicdn.com/imgextra/i1/O1CN01mRGret1QrKiu7CFJI_!!6000000002029-2-tps-64-64.png" />
<link rel="icon" href="https://page-agent.github.io/assets/brand/page-agent-color-64.png" />
<title>Page Agent MCP Launcher</title>
<style>
* {
@@ -142,7 +142,7 @@
<div class="card">
<img
class="logo"
src="https://img.alicdn.com/imgextra/i3/O1CN01JPT4Fj1FJTfmHfNxO_!!6000000000466-49-tps-512-512.webp"
src="https://page-agent.github.io/assets/brand/page-agent-color-512.webp"
alt="Page Agent"
/>
<div class="badge">Page Agent MCP Launcher</div>
@@ -161,7 +161,7 @@
<a class="store-btn" href="__STORE_URL__" target="_blank">
<img
src="https://img.alicdn.com/imgextra/i3/O1CN01JpW0Vo1sR3FpiZKFM_!!6000000005762-55-tps-192-192.svg"
src="https://page-agent.github.io/assets/third-party/chrome-web-store-192.svg"
alt=""
/>
<span data-i18n="install_btn">Install from Chrome Web Store</span>
@@ -172,16 +172,26 @@
If the extension is outdated, please update it to the latest version.
</li>
<li data-i18n="tip_other_browser">
If the extension is not installed in this browser, open this page from the
browser that has it installed.
If the extension is not installed in this browser, open this page from the browser that
has it installed.
</li>
<li data-i18n="tip_refresh">Refresh this page after installing or updating.</li>
</ul>
</div>
<div class="links">
<a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank" data-i18n="link_docs">Docs</a>
<a href="https://github.com/alibaba/page-agent/issues" target="_blank" data-i18n="link_issues">Report an Issue</a>
<a
href="https://alibaba.github.io/page-agent/docs/introduction/overview"
target="_blank"
data-i18n="link_docs"
>Docs</a
>
<a
href="https://github.com/alibaba/page-agent/issues"
target="_blank"
data-i18n="link_issues"
>Report an Issue</a
>
</div>
</div>
@@ -197,8 +207,7 @@
install_sub: 'Page Agent 需要安装最新版浏览器插件才能运行。',
install_btn: '从 Chrome 应用商店安装',
tip_outdated: '如果插件版本过旧,请更新到最新版本。',
tip_other_browser:
'如果该浏览器中未安装插件,请从装有插件的浏览器打开此页面。',
tip_other_browser: '如果该浏览器中未安装插件,请从装有插件的浏览器打开此页面。',
tip_refresh: '安装或更新后,请刷新此页面。',
link_docs: '文档',
link_issues: '问题反馈',
@@ -220,13 +229,9 @@
if (!globalThis.chrome?.runtime?.sendMessage) {
showInstall()
} else {
chrome.runtime.sendMessage(
EXT_ID,
{ type: 'OPEN_HUB', wsPort },
(response) => {
if (chrome.runtime.lastError || !response?.ok) showInstall()
}
)
chrome.runtime.sendMessage(EXT_ID, { type: 'OPEN_HUB', wsPort }, (response) => {
if (chrome.runtime.lastError || !response?.ok) showInstall()
})
}
} catch {
showInstall()
+24 -14
View File
@@ -1,16 +1,26 @@
{
"name": "page-agent",
"private": false,
"version": "1.5.10",
"version": "1.12.0",
"type": "module",
"main": "./dist/esm/page-agent.js",
"module": "./dist/esm/page-agent.js",
"types": "./dist/esm/PageAgent.d.ts",
"main": "./src/PageAgent.ts",
"types": "./src/PageAgent.ts",
"exports": {
".": {
"types": "./dist/esm/PageAgent.d.ts",
"import": "./dist/esm/page-agent.js",
"default": "./dist/esm/page-agent.js"
"types": "./src/PageAgent.ts",
"default": "./src/PageAgent.ts"
}
},
"publishConfig": {
"main": "./dist/esm/page-agent.js",
"module": "./dist/esm/page-agent.js",
"types": "./dist/esm/PageAgent.d.ts",
"exports": {
".": {
"types": "./dist/esm/PageAgent.d.ts",
"import": "./dist/esm/page-agent.js",
"default": "./dist/esm/page-agent.js"
}
}
},
"files": [
@@ -40,20 +50,20 @@
"build": "vite build && npm run build:demo",
"build:demo": "vite build --config vite.iife.config.js",
"dev:demo": "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{}})\""
"prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node ../../scripts/post-publish.js"
},
"dependencies": {
"@page-agent/core": "1.5.10",
"@page-agent/llms": "1.5.10",
"@page-agent/page-controller": "1.5.10",
"@page-agent/ui": "1.5.10",
"@page-agent/core": "1.12.0",
"@page-agent/llms": "1.12.0",
"@page-agent/page-controller": "1.12.0",
"@page-agent/ui": "1.12.0",
"chalk": "^5.6.2"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"zod": "^4.3.5"
"zod": "^4.4.3"
}
}
+3 -2
View File
@@ -4,11 +4,11 @@
*/
import { type AgentConfig, PageAgentCore } from '@page-agent/core'
import { PageController, type PageControllerConfig } from '@page-agent/page-controller'
import { Panel } from '@page-agent/ui'
import { Panel, type PanelConfig } from '@page-agent/ui'
export * from '@page-agent/core'
export type PageAgentConfig = AgentConfig & PageControllerConfig
export type PageAgentConfig = AgentConfig & PageControllerConfig & Omit<PanelConfig, 'language'>
export class PageAgent extends PageAgentCore {
panel: Panel
@@ -23,6 +23,7 @@ export class PageAgent extends PageAgentCore {
this.panel = new Panel(this, {
language: config.language,
promptForNextTask: config.promptForNextTask,
})
}
}
+32 -24
View File
@@ -3,8 +3,12 @@
*/
import { PageAgent, type PageAgentConfig } from './PageAgent'
const currentScript = document.currentScript as HTMLScriptElement | null
const currentScriptURL = currentScript?.src ? new URL(currentScript.src) : null
const autoInit = currentScriptURL?.searchParams.get('autoInit') !== 'false'
// Clean up existing instances to prevent multiple injections from bookmarklet
if (window.pageAgent) {
if (autoInit && window.pageAgent) {
window.pageAgent.dispose()
}
@@ -18,30 +22,34 @@ const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
const DEMO_API_KEY = 'NA'
// in case document.x is not ready yet
setTimeout(() => {
const currentScript = document.currentScript as HTMLScriptElement | null
let config: PageAgentConfig
if (autoInit) {
setTimeout(() => {
let config: PageAgentConfig
let showPanel = true
if (currentScript) {
console.log('🚀 page-agent.js detected current script:', currentScript.src)
const url = new URL(currentScript.src)
const model = url.searchParams.get('model') || DEMO_MODEL
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
config = { model, baseURL, apiKey, language }
} else {
console.log('🚀 page-agent.js no current script detected, using default demo config')
config = {
model: import.meta.env.LLM_MODEL_NAME ? import.meta.env.LLM_MODEL_NAME : DEMO_MODEL,
baseURL: import.meta.env.LLM_BASE_URL ? import.meta.env.LLM_BASE_URL : DEMO_BASE_URL,
apiKey: import.meta.env.LLM_API_KEY ? import.meta.env.LLM_API_KEY : DEMO_API_KEY,
if (currentScriptURL) {
const url = currentScriptURL
const model = url.searchParams.get('model') || DEMO_MODEL
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
showPanel = ((url.searchParams.get('showPanel') as 'true' | 'false') || 'true') === 'true'
config = { model, baseURL, apiKey, language }
} else {
console.log('🚀 page-agent.js no current script detected, using default demo config')
config = {
model: import.meta.env.LLM_MODEL_NAME ? import.meta.env.LLM_MODEL_NAME : DEMO_MODEL,
baseURL: import.meta.env.LLM_BASE_URL ? import.meta.env.LLM_BASE_URL : DEMO_BASE_URL,
apiKey: import.meta.env.LLM_API_KEY ? import.meta.env.LLM_API_KEY : DEMO_API_KEY,
}
}
}
// Create agent
window.pageAgent = new PageAgent(config)
window.pageAgent.panel.show()
// Create agent
window.pageAgent = new PageAgent(config)
if (showPanel) {
window.pageAgent.panel.show()
}
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
})
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
})
}
-9
View File
@@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+3 -21
View File
@@ -1,26 +1,8 @@
{
"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"],
"@page-agent/core": ["../core/src/PageAgentCore.ts"],
"@page-agent/ui": ["../ui/src/index.ts"]
}
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"],
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" },
{ "path": "../core" },
{ "path": "../ui" }
]
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"]
}
+11 -4
View File
@@ -11,13 +11,20 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
export default defineConfig({
clearScreen: false,
plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
dts({
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts'],
bundleTypes: true,
compilerOptions: {
composite: true,
noEmit: false,
emitDeclarationOnly: true,
declaration: true,
},
}),
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
],
publicDir: false,
esbuild: {
keepNames: true,
},
build: {
lib: {
entry: resolve(__dirname, 'src/PageAgent.ts'),
-12
View File
@@ -3,7 +3,6 @@ import { config as dotenvConfig } from 'dotenv'
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
// import { analyzer } from 'vite-bundle-analyzer'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -21,17 +20,6 @@ export default defineConfig(() => ({
// analyzer()
],
publicDir: false,
esbuild: {
keepNames: true,
},
resolve: {
alias: {
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
'@page-agent/core': resolve(__dirname, '../core/src/PageAgentCore.ts'),
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
},
},
build: {
lib: {
entry: resolve(__dirname, 'src/demo.ts'),
+20 -9
View File
@@ -1,15 +1,25 @@
{
"name": "@page-agent/page-controller",
"version": "1.5.10",
"version": "1.12.0",
"type": "module",
"main": "./dist/lib/page-controller.js",
"module": "./dist/lib/page-controller.js",
"types": "./dist/lib/PageController.d.ts",
"main": "./src/PageController.ts",
"types": "./src/PageController.ts",
"exports": {
".": {
"types": "./dist/lib/PageController.d.ts",
"import": "./dist/lib/page-controller.js",
"default": "./dist/lib/page-controller.js"
"types": "./src/PageController.ts",
"default": "./src/PageController.ts"
}
},
"publishConfig": {
"main": "./dist/lib/page-controller.js",
"module": "./dist/lib/page-controller.js",
"types": "./dist/lib/PageController.d.ts",
"exports": {
".": {
"types": "./dist/lib/PageController.d.ts",
"import": "./dist/lib/page-controller.js",
"default": "./dist/lib/page-controller.js"
}
}
},
"files": [
@@ -32,8 +42,9 @@
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
"test": "vitest run",
"prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node ../../scripts/post-publish.js"
},
"dependencies": {
"ai-motion": "^0.4.8"
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest'
import { PageController } from './PageController'
describe('PageController', () => {
it('constructs and exposes the current url', async () => {
const controller = new PageController()
expect(controller).toBeInstanceOf(PageController)
expect(await controller.getCurrentUrl()).toBe(window.location.href)
})
describe('executeJavascript', () => {
it('runs a script and returns its result', async () => {
const controller = new PageController()
const result = await controller.executeJavascript('return 1 + 2')
expect(result).toMatchObject({ success: true })
expect(result.message).toContain('3')
})
it('exposes the abort signal to the script scope', async () => {
const controller = new PageController()
const controllerSignal = new AbortController()
controllerSignal.abort()
const result = await controller.executeJavascript(
'return signal.aborted',
controllerSignal.signal
)
expect(result).toMatchObject({ success: true })
expect(result.message).toContain('true')
})
it('reports a syntax error as a failed result', async () => {
const controller = new PageController()
const result = await controller.executeJavascript('return (')
expect(result.success).toBe(false)
expect(result.message).toContain('❌')
})
})
})

Some files were not shown because too many files have changed in this diff Show More