Compare commits

..

265 Commits

Author SHA1 Message Date
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
Simon 24eefdef23 chore(version): bump version to 1.5.10 2026-03-18 20:36:29 +08:00
Simon 252f550c04 style(ext): fix ai-motion overflow; wording; launcher info 2026-03-18 20:25:54 +08:00
Simon 628c249d45 fix(ext): init without initialTab throws error 2026-03-18 19:36:02 +08:00
Simon 61d598142d feat(ext): ask user approval for MCP task 2026-03-18 19:14:44 +08:00
Simon c0510b2235 Merge pull request #294 from octo-patch/feature/upgrade-minimax-m27
feat: upgrade MiniMax default model to M2.7
2026-03-18 15:21:19 +08:00
Octopus ca0005c825 feat: upgrade MiniMax default model to M2.7
- Add MiniMax-M2.7 and MiniMax-M2.7-highspeed to model list
- Set MiniMax-M2.7 as default model in code example
- Keep all previous models as alternatives
2026-03-18 02:12:58 -05:00
Simon 17e92b6200 chore(version): bump version to 1.5.9 2026-03-18 03:45:56 +08:00
Simon 2e0cbef034 Merge pull request #283 from alibaba/feat/mcp
feat: mcp (WIP)
2026-03-18 03:42:44 +08:00
Simon 95b6785f05 feat(mcp): mark MCP package as private 2026-03-18 03:37:02 +08:00
Simon 2917941b13 feat(mcp): add TODO for version checking 2026-03-18 03:31:38 +08:00
Simon 31c496cc65 docs(mcp): add development instructions for mcp 2026-03-18 03:15:02 +08:00
Simon cef39d5090 feat(mcp): MCP connector 2026-03-18 03:12:20 +08:00
Simon 3063b2a06d feat(ext): hub protocol 2026-03-18 02:59:28 +08:00
Simon d66a390516 chore(ext): add pub key to fix extension ID 2026-03-18 01:56:56 +08:00
Simon aae6c93f11 fix(ext): useAgent execute return result 2026-03-18 01:55:09 +08:00
Simon 2f5476b76c refactor(ext): mv ui components for later reuse 2026-03-17 19:11:13 +08:00
Simon 88ea2c6708 Merge pull request #221 from octo-patch/feature/add-minimax-provider
feat: add MiniMax model support with temperature clamping
2026-03-17 14:16:59 +08:00
Simon 37760a97df Merge pull request #274 from alibaba/dependabot/npm_and_yarn/development-dependencies-8ef9c9264a
chore(deps-dev): bump the development-dependencies group with 10 updates
2026-03-17 14:09:18 +08:00
dependabot[bot] de76d97303 chore(deps-dev): bump the development-dependencies group with 10 updates
Bumps the development-dependencies group with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.4.3` | `20.5.0` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.4.3` | `20.5.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.4.0` | `25.5.0` |
| [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react-swc) | `4.2.3` | `4.3.0` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `16.3.2` | `16.4.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.57.0` | `8.57.1` |
| [@wxt-dev/module-react](https://github.com/wxt-dev/wxt/tree/HEAD/packages/module-react) | `1.2.1` | `1.2.2` |
| [motion](https://github.com/motiondivision/motion) | `12.35.2` | `12.37.0` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.11.0` | `16.12.0` |
| [wxt](https://github.com/wxt-dev/wxt) | `0.20.18` | `0.20.19` |


Updates `@commitlint/cli` from 20.4.3 to 20.5.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/v20.5.0/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.4.3 to 20.5.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/v20.5.0/@commitlint/config-conventional)

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

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

Updates `lint-staged` from 16.3.2 to 16.4.0
- [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/v16.3.2...v16.4.0)

Updates `typescript-eslint` from 8.57.0 to 8.57.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.57.1/packages/typescript-eslint)

Updates `@wxt-dev/module-react` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/wxt-dev/wxt/releases)
- [Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/module-react/CHANGELOG.md)
- [Commits](https://github.com/wxt-dev/wxt/commits/storage-v1.2.2/packages/module-react)

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

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

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

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@vitejs/plugin-react-swc"
  dependency-version: 4.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lint-staged
  dependency-version: 16.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.57.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@wxt-dev/module-react"
  dependency-version: 1.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: motion
  dependency-version: 12.37.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: wxt
  dependency-version: 0.20.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 19:36:05 +00:00
Simon 92196ac793 chore(website): tailwind important; footer 2026-03-17 00:29:13 +08:00
Simon 80dda37f8c fix: improve button cursor styles in UI components 2026-03-16 23:23:27 +08:00
Simon 5e7fe18463 docs(website): clean up 2026-03-16 23:14:48 +08:00
Simon 3d014f15e7 chore(version): bump version to 1.5.8 2026-03-16 22:52:32 +08:00
Simon ce8f0fdbd8 Merge pull request #273 from alibaba/feat/extension-control-curr-tab-group
feat(ext): initial controlled group
2026-03-16 22:51:51 +08:00
Simon 48fede38fd feat(ext): initialTab should be in controlled group; rm windowId; rm gray color 2026-03-16 20:51:32 +08:00
Simon f303ed5fc5 Merge branch 'main' of https://github.com/alibaba/page-agent 2026-03-16 19:45:29 +08:00
Simon 6dbc33efd9 docs: limitation; readme; attribution 2026-03-16 19:45:14 +08:00
Simon 53628532df Merge pull request #251 from linked-danis/pr5-scrollintoview 2026-03-16 18:45:19 +08:00
Simon 2aae34d74a Merge pull request #250 from linked-danis/pr2-step-delay 2026-03-16 17:50:50 +08:00
Simon e27d1fb57f fix(style): Tailwind important flag 2026-03-16 17:35:32 +08:00
octo-patch 8ad4d19232 feat: add MiniMax model support with temperature clamping
Add MiniMax (M2.5 / M2.5-highspeed) as a tested model provider:

- Add model-specific patch in utils.ts: clamp temperature to (0, 1]
  since MiniMax API rejects temperature=0, and remove unsupported
  parallel_tool_calls parameter
- Add MiniMax to the tested models list on the Models documentation page
- Add MiniMax configuration example alongside existing providers
2026-03-15 23:42:30 +00:00
Simon 0fa7dc0531 chore(version): bump version to 1.5.7 2026-03-13 22:02:02 +08:00
linked-danis 89546887bd fix: type-safe scrollIntoViewIfNeeded
Add proper interface for WebKit extension method.

Changes:
- Add ScrollableElement interface
- Use typeof check instead of 'as any' cast
2026-03-13 14:22:47 +01:00
linked-danis b0bcba2922 feat: add stepDelay config option
Add configurable delay between agent steps.
Previously hardcoded to 0.4s.

Changes:
- Add stepDelay?: number to AgentConfig
- Use config value with 0.4s default
2026-03-13 14:22:37 +01:00
Simon 618ae8bf14 Merge pull request #236 from Wizard-Guido/fix/some-typos-and-grammar
fix: typos and grammar in system prompts and source code
2026-03-13 21:13:40 +08:00
Simon 5ed5d574fd Merge pull request #248 from alibaba/split/pr-207-simulator-mask-css 2026-03-13 21:07:26 +08:00
Simon bde9d9023a Merge pull request #247 from alibaba/split/pr-207-validate-url
fix: validate URL in fetchLlmsTxt
2026-03-13 21:05:10 +08:00
Simon c8a8858d24 fix(llms): edge cases 2026-03-13 21:03:29 +08:00
linked-danis 6491118cde refactor: SimulatorMask use CSS classes
Replace inline style.display with CSS class toggling.

Changes:
- Add .visible class to CSS module
- Use classList.add/remove instead of style.display

(cherry picked from commit 33465bbf52)
2026-03-13 20:44:34 +08:00
linked-danis b4acd02007 fix: validate URL in fetchLlmsTxt
Prevent crash on invalid URLs.

Changes:
- Wrap URL constructor in try/catch
- Return null for invalid URLs instead of throwing

(cherry picked from commit 8b0acf314d)
2026-03-13 20:44:14 +08:00
Simon 5695b3d6aa chore(website): add social media link; add star 2026-03-13 20:23:13 +08:00
Simon 0a160697d1 docs(terms): add Permitted Use Only 2026-03-13 19:16:20 +08:00
Wen 11d3ca3d6e fix: typos and grammar in system prompts and source code 2026-03-13 01:32:34 +00:00
Simon ee88b23891 Merge pull request #229 from alibaba/feat/seo
feat(website): basic SEO
2026-03-12 20:06:56 +08:00
Simon ca68be7a56 feat(website): basic SEO 2026-03-12 20:06:12 +08:00
Simon 4ad2abe997 docs: no-bots policy 2026-03-12 19:12:59 +08:00
Simon 8f08322721 chore: add security policy document 2026-03-12 17:06:16 +08:00
Simon fb043e5a81 Merge pull request #202 from Lubrsy706/fix/heuristic-elements-include-attributes
fix: extract attributes for heuristically-detected interactive elements
2026-03-12 02:20:58 +08:00
Simon 1628d48c97 chore: align doc styles 2026-03-12 02:20:24 +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
Simon 025cb3391e chore: update README 2026-03-12 01:05:24 +08:00
Simon 4aab7f991f chore(version): bump version to 1.5.6 2026-03-12 00:53:10 +08:00
Simon 67846db28f chore(ext): ui layout 2026-03-12 00:52:19 +08:00
Simon cac55fa2bf Merge pull request #209 from alibaba/feat/extension-use-same-version-as-packages
feat: extension use the same version as packages
2026-03-12 00:41:58 +08:00
Simon a74b7542ba feat: extension use the same version as packages 2026-03-12 00:37:43 +08:00
Simon a9f462ed37 Merge pull request #191 from RinZ27/fix/ui-security-xss
fix(ui): escape array content in cards and fix double-escaping
2026-03-11 23:43:02 +08:00
Simon c33b3d8278 fix(ui): remove unnecessary tags 2026-03-11 23:38:57 +08:00
caibing de3a6e4660 fix: extract attributes for heuristically-detected interactive elements
Elements detected as interactive via heuristic methods (cursor:pointer
style, interactive class names, event listeners) had empty attributes
because `isInteractiveCandidate()` was used as the gate for attribute
extraction. This function only recognizes standard HTML tags and ARIA
attributes, missing heuristic detections.

After interactivity is confirmed by `isInteractiveElement()`, backfill
attributes for elements that were missed. This ensures
`includeAttributes` (e.g. `['class']`) works correctly for all
interactive elements, not just semantically standard ones.

Closes #124

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:38:36 +08:00
Simon e2c00b1bfc chore(ext): bump version to 0.1.17 2026-03-10 22:50:02 +08:00
Simon 692ac4acca chore(version): bump version to 1.5.5 2026-03-10 22:49:03 +08:00
RinZ27 c4bdd8fc4b fix(ui): escape array content in cards and fix double-escaping 2026-03-10 21:08:20 +07:00
Simon aa88221845 Merge branch 'main' of https://github.com/alibaba/page-agent 2026-03-10 22:06:51 +08:00
Simon b425e48afe docs: guideline against AI-generated PRs 2026-03-10 22:06:41 +08:00
Simon ff54b363aa Merge pull request #179 from JasonOA888/fix/contenteditable-input-events
fix(page-controller): improve contenteditable input with proper events
2026-03-10 21:04:05 +08:00
Simon 4d931b4430 chore: comments; spell check 2026-03-10 21:02:48 +08:00
JasonOA888 6054ca1217 docs(page-controller): document contenteditable limitations and execCommand fallback
## Changes

Based on @gaomeng1900's comprehensive testing feedback:

1. **Document known limitations**
   - Slate.js and Draft.js do not work with synthetic events
   - Draft.js: Cannot be supported via DOM manipulation (by design, unmaintained)
   - Monaco/CodeMirror: Require direct JS instance access

2. **Clarify tested editors**
   - Works: Quill, LinkedIn, simple contenteditable editors
   - Does not work: Slate.js, Draft.js

3. **Preserve execCommand as fallback**
   - execCommand works better for LinkedIn, Quill, and Draft.js
   - Kept as commented fallback (deprecated API)
   - Users can uncomment if synthetic events don't work

Co-authored-by: gaomeng1900 <gaomeng1900@users.noreply.github.com>
2026-03-10 20:39:20 +08:00
Simon 7f9f0d1589 feat: simplify ContentEditable handling 2026-03-10 19:40:49 +08:00
Simon ddcfa5b499 Merge branch 'main' into fix/contenteditable-input-events 2026-03-10 17:37:20 +08:00
JasonOA888 441b41c713 fix(page-controller): address all Copilot review feedback
## Changes

1. **Fix early return bypassing cleanup**
   - Remove early return when beforeinput is canceled
   - Use shouldInsert flag to skip mutation but continue cleanup
   - Ensures waitFor and blurLastClickedElement always run

2. **Fix duplicate input events**
   - Contenteditable path now dispatches its own input events with inputType
   - Skip shared input dispatch for contenteditable
   - Prevents double-triggering framework listeners

3. **Fix event order (mutation before events)**
   - Clear content now dispatches beforeinput(inputType:deleteContent) first
   - Then performs the clear mutation
   - Then dispatches input event for the deletion
   - Proper event-mutation ordering

4. **Single-character keyboard events remain**
   - PR description clarified this is intentional
   - Multi-character input uses bulk insertion (not per-character typing)
   - Maintains semantic consistency

5. **Fix duplicate focusout event**
   - blur() already triggers native focusout
   - Removed manual focusout dispatch
   - Prevents double validation handlers

All changes follow Copilot's suggested fixes.
2026-03-10 16:03:25 +08:00
Simon ed71dd08b9 Merge pull request #181 from fancyboi999/fancy/fix-180-viewport-expansion
fix(page-controller): honor viewportExpansion in DOM extraction
2026-03-10 15:50:34 +08:00
fancy 1c354ab5d3 refactor(page-controller): remove viewportExpansion constants module 2026-03-10 15:34:24 +08:00
Simon 151760713a Merge pull request #177 from alibaba/dependabot/npm_and_yarn/development-dependencies-05afa3b718
chore(deps-dev): bump the development-dependencies group with 10 updates
2026-03-10 15:34:14 +08:00
Simon 4450d6302b docs: update guidelines, CLA should be optional 2026-03-10 15:23:19 +08:00
JasonOA888 2d055d3909 style: fix Prettier formatting 2026-03-10 15:10:17 +08:00
fancy 16da7d936d fix(page-controller): honor viewportExpansion in DOM extraction 2026-03-10 12:28:54 +08:00
JasonOA888 efe08f445f fix(page-controller): address Copilot review feedback
## Changes

1. **Check beforeinput cancellation**
   - dispatchEvent returns false if canceled
   - Check defaultPrevented as well
   - Abort mutation if event was canceled by any listener

2. **Fix event order to match real user typing**
   - Before: beforeinput -> mutation -> input -> keydown -> keyup
   - After: keydown -> beforeinput -> mutation -> input -> keyup
   - This matches typical browser event sequence

3. **Fix blur event semantics**
   - blur doesn't bubble; focusout does
   - Call editableElement.blur() to actually change focus
   - Dispatch focusout with bubbles:true for listeners
   - Then refocus

4. **Keep single-character keyboard events**
   - Already fixed in previous commit
   - Maintained here with correct order

All changes follow Copilot's suggested fixes.
2026-03-10 12:02:01 +08:00
JasonOA888 4e7f755ae9 fix(page-controller): address PR review feedback
## Changes

1. **Fix keyboard event semantics** (per review feedback)
   - Only dispatch keydown/keyup for single-character input
   - Avoids inconsistent event payloads for multi-character strings
   - Prevents confusion in editors that correlate key events with text changes

2. **Remove extra blank line**
   - Formatting consistency

Reviewer noted that dispatching key events with only the last character
of multi-character text creates semantic inconsistency with the actual
DOM mutation (which inserts the full string at once).

This fix follows the suggested change from the review.
2026-03-10 12:00:47 +08:00
JasonOA888 28bb2204e7 fix(page-controller): improve contenteditable input with proper events
## Problem
Input into contenteditable elements (like LinkedIn post editor) fails
because simply setting innerText does not trigger framework event listeners.

## Solution
Dispatch a full sequence of events that rich text editors expect:
- beforeinput (for React apps)
- input (standard)
- keydown/keyup (for keyboard listeners)
- change (for validation)
- blur + refocus (to trigger change detection)

## Testing
Tested on:
- LinkedIn post editor
- Draft.js editors
- Contenteditable divs with React listeners

Fixes #168
2026-03-10 10:32:07 +08:00
dependabot[bot] 0c3b4592b2 chore(deps-dev): bump the development-dependencies group with 10 updates
Bumps the development-dependencies group with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.4.2` | `20.4.3` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.4.2` | `20.4.3` |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.57.6` | `7.57.7` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.3.3` | `25.4.0` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `16.3.1` | `16.3.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.56.1` | `8.57.0` |
| [@wxt-dev/module-react](https://github.com/wxt-dev/wxt/tree/HEAD/packages/module-react) | `1.1.5` | `1.2.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.576.0` | `0.577.0` |
| [motion](https://github.com/motiondivision/motion) | `12.34.5` | `12.35.2` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.10.0` | `16.11.0` |


Updates `@commitlint/cli` from 20.4.2 to 20.4.3
- [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.4.3/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.4.2 to 20.4.3
- [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/v20.4.3/@commitlint/config-conventional)

Updates `@microsoft/api-extractor` from 7.57.6 to 7.57.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.57.7/apps/api-extractor)

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

Updates `lint-staged` from 16.3.1 to 16.3.2
- [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/v16.3.1...v16.3.2)

Updates `typescript-eslint` from 8.56.1 to 8.57.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.57.0/packages/typescript-eslint)

Updates `@wxt-dev/module-react` from 1.1.5 to 1.2.1
- [Release notes](https://github.com/wxt-dev/wxt/releases)
- [Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/module-react/CHANGELOG.md)
- [Commits](https://github.com/wxt-dev/wxt/commits/storage-v1.2.1/packages/module-react)

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

Updates `motion` from 12.34.5 to 12.35.2
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.34.5...v12.35.2)

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

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.4.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.4.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.57.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lint-staged
  dependency-version: 16.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.57.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@wxt-dev/module-react"
  dependency-version: 1.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 0.577.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: motion
  dependency-version: 12.35.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.11.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-09 20:33:42 +00:00
Simon 222bbef670 chore: update readme 2026-03-09 23:41:12 +08:00
Simon fa8ab451eb chore: update readme 2026-03-09 23:39:41 +08:00
Simon b4ebb2b98f chore(version): bump version to 1.5.4 2026-03-09 23:35:04 +08:00
Simon c757270101 Merge pull request #173 from alibaba/feat/tolerant-html-cleaning
feat: support wildcard in `includeAttributes`
2026-03-09 22:48:34 +08:00
Simon 5873e68d63 Merge pull request #175 from alibaba/copilot/sub-pr-173-again
perf(page-controller): cache compiled regexes in `globToRegex`
2026-03-09 22:34:58 +08:00
Simon 0c124c3b44 Merge pull request #176 from alibaba/copilot/sub-pr-173-another-one
refactor: use Object.keys() instead of for...in in matchAttributes
2026-03-09 22:33:56 +08:00
copilot-swe-agent[bot] d46a57f8ef refactor: use Object.keys() instead of for...in in matchAttributes
Co-authored-by: gaomeng1900 <10131203+gaomeng1900@users.noreply.github.com>
2026-03-09 14:32:20 +00:00
Simon 153fa23a71 docs: update model list; give example for cookie auth 2026-03-09 22:31:35 +08:00
copilot-swe-agent[bot] 5852054e3a perf: cache compiled regexes in globToRegex to avoid repeated compilation
Co-authored-by: gaomeng1900 <10131203+gaomeng1900@users.noreply.github.com>
2026-03-09 14:29:56 +00:00
copilot-swe-agent[bot] 5988d4ba24 Initial plan 2026-03-09 14:29:10 +00:00
copilot-swe-agent[bot] 0b6a698f6b Initial plan 2026-03-09 14:26:53 +00:00
Simon 01db520881 feat: support wildcard in includeAttributes 2026-03-09 22:02:35 +08:00
Simon 30b9956c4f Merge pull request #172 from alibaba/fix/contenteditable
fix: contenteditable
2026-03-09 19:19:23 +08:00
Simon 4b37a3e538 feat(controller): add contenteditable to DEFAULT_INCLUDE_ATTRIBUTES 2026-03-09 19:15:51 +08:00
Simon 928d8d2fb3 fix: allow ContentEditable in inputTextElement; clean up code 2026-03-09 19:05:32 +08:00
Simon 8ece20f54b chore(ext): build script 2026-03-09 17:29:45 +08:00
Simon 26b4afca6a chore(ext): bump version to 0.1.16 2026-03-09 17:17:04 +08:00
Simon 5f43b76b57 chore: improve scripts for extension 2026-03-09 17:15:16 +08:00
Simon 15d609aebc chore(version): bump version to 1.5.3 2026-03-09 17:02:34 +08:00
Simon d66bf8f3cf docs: improve Contributing.md 2026-03-09 17:00:25 +08:00
Simon d3b42036af Merge pull request #169 from tsubasakong/lucas/opener-b-page-agent-167
fix(llms): avoid reasoning_effort for GPT-5.4 chat tools
2026-03-09 16:33:23 +08:00
Simon e448a43769 docs(terms): wording 2026-03-09 15:53:02 +08:00
Frank 349de7bf48 Merge remote-tracking branch 'upstream/main' into HEAD 2026-03-08 16:51:01 -07:00
tsubasakong eec601e6b2 fix(llms): avoid reasoning_effort for GPT-5.4 chat tools 2026-03-08 16:37:32 -07:00
Simon 5b280a4a67 docs: update README graph 2026-03-09 03:44:36 +08:00
Simon 46deaf373f docs(readme): add HN discussion link and star history 2026-03-08 00:18:48 +08:00
Simon e098d5701f docs(terms): wording 2026-03-07 23:57:09 +08:00
Simon 21d4be0020 docs(terms): wording 2026-03-07 23:51:14 +08:00
Simon 3f6a1856ac chore(ext): bump version; fix zip naming 2026-03-07 23:29:12 +08:00
Simon 816e24a102 Merge pull request #160 from alibaba/fix/extension-error-handling
fix: add button to clear saved configuration from the error boundary
2026-03-07 23:22:41 +08:00
Simon 80e2a93a8c feat: add button to clear saved configuration from the error boundary 2026-03-07 23:16:03 +08:00
Simon 2f8096883f docs(terms): wording for testing API terms 2026-03-07 04:50:34 +08:00
Simon 088f002583 chore(ext): bump version 2026-03-07 03:16:47 +08:00
Simon 27e9e78636 chore(version): bump version to 1.5.2 2026-03-07 03:08:33 +08:00
Simon a0c979602e feat: do not throw for webgl2 fail 2026-03-07 03:07:21 +08:00
Simon 3dc9edb8b5 fix(ext): ts types 2026-03-06 17:39:09 +08:00
Simon 32b68ab7c8 fix(docs): broken demo video 2026-03-06 16:07:22 +08:00
Simon 75a61c05c1 docs(terms): wording
Clarify the note regarding data processing location for the demo.
2026-03-06 04:33:03 +08:00
Simon bdfa98358b fix: requestIdleCallback on safari 2026-03-06 02:35:01 +08:00
144 changed files with 7325 additions and 3214 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
+100
View File
@@ -0,0 +1,100 @@
---
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. **Never check "Testing" checkboxes.** The template Testing checkboxes (browser tested, no console errors, types/doc added) require manual browser 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.
- **Lint and build**: Run lint/build for the affected area. 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. Leave all "Testing" and "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.
- Prefer area-specific build, lint, or test commands when they exist.
- 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" and "Testing" checkboxes are unchecked (`- [ ]`) in the PR body. Double-check: if any `- [x]` appears in these sections, it is a violation — fix it before submitting.
- 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 -7
View File
@@ -2,15 +2,15 @@
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
@@ -18,8 +18,7 @@ Brief description of changes.
- [ ] No console errors
- [ ] Types/doc added
Closes #(issue)
## Requirements / 要求
- [ ] I have read and follow the [Code of Conduct](../docs/CODE_OF_CONDUCT.md) and [Contributing Guide](../CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
- [ ] This PR is NOT generated by a bot or AI agent acting autonomously. I have authored or meaningfully reviewed every change. / 此 PR 不是由 bot 或 AI 自主生成的,我已亲自编写或充分审查了每一处变更。
+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:
+3 -3
View File
@@ -28,13 +28,13 @@ jobs:
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
+3
View File
@@ -25,6 +25,9 @@ dist-ssr
*.sw?
.qoder
# publish backup
package.json.bak
# env files
.env
.env.*
+5 -1
View File
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"contenteditable",
"deepseek",
"historychange",
"HITL",
@@ -34,5 +35,8 @@
"blanks-around-lists": false,
"ol-prefix": false,
"no-duplicate-heading": false
}
},
"js/ts.tsdk.path": "node_modules/typescript/lib",
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
+7 -6
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,10 +18,11 @@ 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 start # Start website dev server
npm run build # Build all packages
npm run build:libs # Build all libraries
npm run typecheck # Typecheck all packages (libs + website + extension)
npm run lint # ESLint
npm run zip -w @page-agent/ext # Zip the extension package
```
@@ -29,7 +30,7 @@ npm run zip -w @page-agent/ext # Zip the extension package
### 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/
+22 -188
View File
@@ -1,41 +1,14 @@
# Contributing to Page-Agent
# Contributing to PageAgent
Thank you for your interest in contributing to Page-Agent! We welcome contributions from everyone.
♥️ We welcome contributions from everyone.
## 🚀 Quick Start
### Development Setup
1. **Prerequisites**
- `node.js >= 20` with `npm >= 10`
- An editor that supports `ts/eslint/prettier`
- Make sure `eslint`, `prettier` and `commitlint` work well
2. **Setup**
```bash
npm ci
npm start # Start demo and documentation site
```
### 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 to leave deps out.
- When bundling `IIFE` and `Website`. Bundle everything including local packages.
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
@@ -44,170 +17,31 @@ We use a simplified monorepo solution with `native npm-workspace + ts reference
### Code Contributions
1. **Fork and Clone**
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
```bash
git clone https://github.com/your-username/page-agent.git
cd page-agent
```
### Vibe Coding with AI
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**
- Test in our demo website
- Test it on other websites if applicable
- `@TODO: test suite`
5. **Commit and Push**
```bash
git add .
git commit -m "feat: add awesome feature"
git push origin feat/your-feature-name
```
6. **Create Pull Request**
- Provide clear description of changes
- Link related issues
- Include screenshots for UI changes
## 📝 Code Style
### General Guidelines
- Use TypeScript for type safety
- Follow existing naming conventions
- Write meaningful commit messages
- Keep functions small and focused
- Add JSDoc for public APIs
### Vibe coding with AI
- It's **recommended** to heavily rely on AI (aka "vibe coding") when maintaining **demo pages and tests**.
- Be very careful if AI ever touched the core lib!!!
- 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**.
- 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.
- Update the [AI instructions](AGENTS.md) when structure changed.
If your lame 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"
```
> ⚠️ Add `*` to `OLLAMA_ORIGINS` (403). Models < 10B unlikely strong enough. Requires tool_call support. Set context length > 15k (default 4k will NOT work): `$env:OLLAMA_CONTEXT_LENGTH=64000; ollama serve`
- Restart the dev server to load new env vars
- If not provided, the demo will the free testing proxy by default
### Website Development
```bash
npm start
```
### Extension Development
```bash
npm run dev -w @page-agent/ext
npm run zip -w @page-agent/ext
```
- Load extension in Chrome via `chrome://extensions` -> **Load unpacked**
- Use `packages/extension/docs/extension_api.md` (EN) or `packages/extension/docs/extension_api_zh.md` (ZH) 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.
### 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.
## 🎯 Contribution Areas
We especially welcome contributions in:
- **Browser compatibility** improvements
- **Performance optimizations** for DOM processing
- **Documentation** and examples
- **Testing** and quality assurance
- **Accessibility** features
- **Internationalization** support
## 🚫 What We Don't Accept
- Changes that break existing API compatibility (Discuss first)
- Heavy dependencies to core library
- Contributions without proper testing
- Code that doesn't follow project conventions
- Breaking changes and large PRs without prior discussion
- Heavy dependencies to core libs
- Dependencies or code with licenses incompatible with MIT
- Bot or AI-generated pull requests without meaningful human involvement
## 📄 Legal
By contributing to this project, you agree that your contributions will be licensed under the MIT License.
> You may need to sign a github CLA before you create a PR.
---
## 💬 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 Page-Agent better! 🎉
Thank you for helping make PageAgent better! 🎉
+2 -2
View File
@@ -1,7 +1,7 @@
MIT License
Copyright (c) 2026 Alibaba
Copyright (c) 2026 Simon
Copyright (c) 2026 SimonLuvRamen
Copyright (c) 2026 Alibaba Group Holding Limited
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+31 -25
View File
@@ -5,15 +5,15 @@
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
</picture>
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
[![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)
The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
🌐 **English** | [中文](./docs/README-zh.md)
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
<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/11aed429-b69c-49d5-8982-fa99b4a0e9a8" controls crossorigin muted></video>
<video id="demo-video" src="https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2" controls crossorigin muted></video>
---
@@ -22,20 +22,19 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
- **🎯 Easy integration**
- No need for `browser extension` / `python` / `headless browser`.
- Just in-page javascript. Everything happens in your web page.
- The best tool for your agent to control web pages.
- **📖 Text-based DOM manipulation**
- No screenshots. No OCR or multi-modal LLMs needed.
- No special permissions required.
- No screenshots. No multi-modal LLMs or special permissions needed.
- **🧠 Bring your own LLMs**
- **🎨 Pretty UI with human-in-the-loop**
- **🐙 Optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension) for multi-page tasks.**
- 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 needed.
- **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 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 [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension).
- **MCP** - Allow your agent clients to control your browser.
## 🚀 Quick Start
@@ -47,12 +46,12 @@ Fastest way to try PageAgent with our free Demo LLM:
<script src="{URL}" crossorigin="true"></script>
```
> **⚠️ For technical evaluation only.** This demo CDN uses our free [testing LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api). By using it, you agree to its [terms](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md).
| Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.5.1/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.5.1/files/dist/iife/page-agent.demo.js |
> **⚠️ 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).
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.8.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.8.0/files/dist/iife/page-agent.demo.js |
### NPM Installation
@@ -75,11 +74,27 @@ await agent.execute('Click the login button')
For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/docs/introduction/overview).
## 🌟 Awesome Page Agent
Built something cool with PageAgent? Add it here! Open a PR to share your project.
> These are community projects — not maintained or endorsed by us. Use at your own discretion.
| Project | Description |
| -------- | ----------------------------------------------------------- |
| _Yours?_ | [Open a PR](https://github.com/alibaba/page-agent/pulls) 🙌 |
## 🤝 Contributing
We welcome contributions from the community! Follow our instructions in [CONTRIBUTING.md](CONTRIBUTING.md) for environment setup and local development.
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.
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
@@ -90,24 +105,15 @@ This project builds upon the excellent work of **[`browser-use`](https://github.
```
DOM processing components and prompt are derived from browser-use:
Browser Use
Browser Use <https://github.com/browser-use/browser-use>
Copyright (c) 2024 Gregor Zunic
Licensed under the MIT License
Original browser-use project: <https://github.com/browser-use/browser-use>
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!**
+75 -36
View File
@@ -5,6 +5,81 @@ 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.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 +228,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 -27
View File
@@ -5,37 +5,35 @@
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
</picture>
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
[![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)
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
🌐 [English](../README.md) | **中文**
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
<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/11aed429-b69c-49d5-8982-fa99b4a0e9a8" controls crossorigin muted></video>
<video id="demo-video" src="https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2" controls crossorigin muted></video>
---
## ✨ Features
- **🎯 轻松集成**
- 无需 `浏览器插件` / `Python` / `无头浏览器`
- 纯页面内 JavaScript,一切都在你的网页中完成。
- The best tool for your agent to control web pages.
- 无需 `浏览器插件` / `Python` / `无头浏览器`,纯页面内 JavaScript
- **📖 基于文本的 DOM 操作**
- 无需截图,无需 OCR 或多模态模型。
- 无需特殊权限。
- **🧠 用你自己的 LLM**
- **🎨 精美 UI,支持人机协同**
- **🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),支持跨页面任务。**
- 无需截图,无需多模态模型或特殊权限
- **🧠 自备 LLM**
- 🐙 可选的 [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 加入浏览器控制能力。
## 🚀 快速开始
@@ -47,12 +45,12 @@
<script src="{URL}" crossorigin="true"></script>
```
> **⚠️ 仅用于技术评估。** 该 Demo CDN 使用了免费的[测试 LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api),使用即表示您同意其[条款](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md)。
| Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.5.1/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.5.1/files/dist/iife/page-agent.demo.js |
> **⚠️ 仅用于技术评估。** 该 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)。
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.8.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.8.0/files/dist/iife/page-agent.demo.js |
### NPM 安装
@@ -77,11 +75,13 @@ await agent.execute('点击登录按钮')
## 🤝 贡献
欢迎社区贡献!请参阅 [CONTRIBUTING.md](../CONTRIBUTING.md) 了解环境配置和本地开发说明
欢迎社区贡献!请参阅 [CONTRIBUTING.md](../CONTRIBUTING.md) 了解安装与贡献指南
请在贡献前阅读[行为准则](CODE_OF_CONDUCT.md)。
提交 issue 或 PR 之前,请先阅读[作者声明](https://github.com/alibaba/page-agent/issues/349)和[行为准则](CODE_OF_CONDUCT.md)。
## 👏 致谢
我们不接受未经实质性人类参与、完全由 Bot 或 Agent 自动生成的代码。
## 👏 声明与致谢
本项目基于 **[`browser-use`](https://github.com/browser-use/browser-use)** 的优秀工作构建。
@@ -90,24 +90,20 @@ await agent.execute('点击登录按钮')
```
DOM processing components and prompt are derived from browser-use:
Browser Use
Browser Use <https://github.com/browser-use/browser-use>
Copyright (c) 2024 Gregor Zunic
Licensed under the MIT License
Original browser-use project: <https://github.com/browser-use/browser-use>
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 有用或有趣,请给项目点个星!**
+50
View File
@@ -0,0 +1,50 @@
# Security Policy
## Supported Versions
We provide security fixes on a best-effort basis for:
| Version | Supported |
| --------------------------------------------------------- | --------- |
| `main` | Yes |
| Latest npm release of `page-agent` and workspace packages | Yes |
| Older releases | No |
Please upgrade to the latest release before reporting an issue against an older build.
## Reporting a Vulnerability
Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.
Use GitHub's private vulnerability reporting flow:
- Open https://github.com/alibaba/page-agent/security/policy
- Click `Report a vulnerability`
If private reporting is unavailable, open a minimal public issue only to request a private contact channel. Do not include exploit details.
## What to Include
- Affected package or feature
- Exact version, commit, or build
- Browser, OS, and runtime environment
- Reproduction steps or a proof of concept
- Expected impact
## Scope
We prioritize reports that show a real security boundary failure, such as:
- Unauthorized access to data, tokens, or extension capabilities
- Bypassing explicit safety constraints
- Sensitive data exposure caused by default behavior
The following usually do not qualify by themselves:
- Unsafe custom integrations that ignore documented safeguards
- Intentionally embedding secrets into client-side builds
- Reports against unsupported older versions
## Disclosure
Please avoid public disclosure until maintainers have had a reasonable chance to investigate and ship a fix.
+117
View File
@@ -0,0 +1,117 @@
# 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 >= 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
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.
## 🤖 AGENTS.md Alias
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](./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.
+22 -20
View File
@@ -1,42 +1,44 @@
# Terms of Use & Privacy
**Last updated:** February 2026
**Last updated:** March 2026
"We" in this document refers to the maintainers of the open-source Page Agent project (https://github.com/alibaba/page-agent). This document covers the Page Agent software itself and the testing services we provide — **not** any third-party product or service built with it.
"We" in this document refers to the maintainers of the open-source Page Agent project (https://github.com/alibaba/page-agent). "The software" refers to Page Agent (the JavaScript library) and Page Agent Ext (the browser extension). This document covers the software itself and the testing API we provide — **not** any third-party product or service built with it.
---
## 1. Open Source Software Privacy
Page Agent (PageAgent.js and Page Agent Extension) is a **client-side only** tool with a "Bring Your Own Key" (BYOK) architecture. The software itself does **not** include any backend service. The software does **not** collect or transmit any user data on its own, and the maintainers do **not** have access to your browsing activity, page content, or task instructions through the software.
The software is a **client-side only** tool with a "Bring Your Own Key" (BYOK) architecture. The software itself does **not** include any backend service. The software does **not** collect or transmit any user data on its own, and we do **not** have access to your browsing activity, page content, or task instructions through the software.
All data transmission occurs **only** between your browser and the LLM provider you configure. You are in full control of which provider receives your data.
- You choose which LLM provider to use
- You may configure your own API endpoint at any time
- The project is open source and can be audited: https://github.com/alibaba/page-agent
The project is open source under the [MIT License](https://github.com/alibaba/page-agent/blob/main/LICENSE) and can be audited at: https://github.com/alibaba/page-agent
---
## 2. Testing API and Demo Disclaimer & Terms of Use
To facilitate easy testing and technical evaluation, we provide a free testing LLM API. This API is used in the project homepage's live demo, the pre-built demo CDN bundles, and the browser extension's default configuration. Users may also use it independently for their own technical evaluation of page-agent.
To facilitate easy testing and technical evaluation, we provide a free testing LLM API. This API is used in the project homepage's live demo, the pre-built demo CDN bundles, and the browser extension's default configuration. Users may also use it independently for their own technical evaluation of the software.
This free testing API is provided **strictly for technical evaluation and R&D purposes only**. It must not be used in any production environment. By using this service, you agree to the following terms:
This free testing API is provided **strictly for technical evaluation and R&D purposes only**. It must not be used in any production environment. By using this API, you agree to the following terms:
**No Sensitive Data**: You are strictly prohibited from inputting any Personal Identifiable Information (PII), confidential business data, financial/medical records, or using this agent on web pages containing such sensitive information.
- **Permitted Use Only**: This API must be used solely for technical evaluation of the software. Any other use — including integration into other products or services, unlawful activities, violation of the underlying LLM provider's usage policies, or automated scraping at scale — is strictly prohibited.
**Zero Data Retention**: We do not store, log, or use your prompts or webpage data (DOM) for model training. All data is processed in-transit and immediately discarded. However, we do temporarily process necessary, identity-agnostic network data (such as IP addresses) solely for rate-limiting and anti-abuse purposes.
- **No Sensitive Data**: You are strictly prohibited from inputting any Personal Identifiable Information (PII), confidential business data, financial/medical records, or using this agent on web pages containing such sensitive information.
**Independent Infrastructure**: The `page-agent` open-source software is completely frontend-based with a "Bring Your Own Key" (BYOK) architecture and no built-in backend. To facilitate easy testing, the maintainers have independently purchased public cloud services from Alibaba Cloud China ([aliyun.com](https://www.aliyun.com) FC and BaiLian Qwen models). This project is not affiliated with, nor endorsed by, Alibaba Cloud.
- **Data Processing**: We do not store or log your prompts, webpage data (HTML), or any submitted content, nor do we use such data for model training. All data is processed in-transit and immediately discarded. We perform in-memory request validation to prevent abuse of the testing API, and temporarily process IP addresses for rate-limiting purposes. No data from these processes is retained. Data is processed through Alibaba Cloud infrastructure, which is subject to its own privacy policy.
**No Guaranteed Availability**: This testing API may be rate-limited, degraded, or discontinued at any time without prior notice.
- **Independent Infrastructure**: The software is completely frontend-based with a "Bring Your Own Key" (BYOK) architecture and **no built-in backend**. To facilitate easy testing, the maintainers have purchased public cloud services from Alibaba Cloud China ([aliyun.com](https://www.aliyun.com) Function Compute and BaiLian Qwen models). This project is not a product of, nor endorsed by, Alibaba Cloud.
**"AS IS" & Limitation of Liability**: This service is provided strictly on an "AS IS" and "AS AVAILABLE" basis, without any warranties. The maintainers bear no liability for any data loss, service interruption, or legal consequences arising from your use of this demo.
- **No Guaranteed Availability**: This testing API may be rate-limited, degraded, or discontinued at any time without prior notice.
**Recommendation for Real Usage**: For secure and continuous usage, we strongly advise using the default BYOK mode with your own legally compliant commercial LLM API keys, or connecting to local, offline models (e.g., Ollama).
- **"AS IS" & Limitation of Liability**: This service is provided strictly on an "AS IS" and "AS AVAILABLE" basis, without any warranties. The maintainers bear no liability for any data loss, service interruption, or legal consequences arising from your use of this service.
**Note**: This service processes data via servers located in Mainland China. If you are located in a region with strict data localization laws (such as the EU/EEA), please do not use this demo.
- **Recommendation for Real Usage**: For secure and continuous usage, we strongly advise using the BYOK mode with your own legally compliant commercial LLM API keys, or connecting to local, offline models (e.g., Ollama).
**Note**: This free testing LLM API processes data via servers located in Mainland China. If you are located in a region with strict data localization laws (such as the EU/EEA), please do not use this API.
**Age Requirement**: The software and testing API are not intended for use by individuals under the age of 13 (or the minimum age of digital consent in your jurisdiction).
---
@@ -49,19 +51,19 @@ The extension performs DOM analysis and automation actions **locally in your bro
Data is transmitted to external servers **only when you initiate an automation task**. When this occurs:
- Your task instructions (natural language commands)
- Simplified page structure (cleaned DOM) of all pages under the extension's control
- Simplified page structure (cleaned HTML) of all pages under the extension's control
are sent to the LLM API endpoint configured in **your settings**.
> **Note:** The DOM cleaning process simplifies page structure for AI readability but **does not guarantee removal of sensitive information** (e.g., visible text, form values, or personal data on the page). Please be mindful of the page content when initiating tasks.
> **Note:** The HTML cleaning process simplifies page structure for AI readability but **does not guarantee removal of sensitive information** (e.g., visible text, form values, or personal data on the page). Please be mindful of the page content when initiating tasks.
**If you configure a third-party LLM provider** (e.g., OpenAI, Anthropic, or others), data is sent directly to that provider. Their privacy policies apply.
**If you use the built-in testing API**, the terms in [Section 2](#2-testing-api-and-demo-disclaimer--terms-of-use) apply. By using the extension with the default testing API, you agree to those terms.
**If you use the testing API**, the terms in [Section 2](#2-testing-api-and-demo-disclaimer--terms-of-use) apply. By using the extension with the default testing API, you agree to those terms.
### Data Storage
- **Local storage only**: Your configuration (API endpoint, API key, model selection) is stored in your browser via `chrome.storage.local`
- **Local storage only**: Your configuration (API endpoint, API key, model selection) is stored in your browser via `chrome.storage.local` (or equivalent browser storage APIs)
- **No cloud sync**: Configuration is not synced to any external server
- **No analytics**: The extension does not include any analytics or tracking code
@@ -76,7 +78,7 @@ are sent to the LLM API endpoint configured in **your settings**.
## Changes
We may update these terms as the project evolves.
We may update these terms at our discretion.
## Contact
+6 -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,14 @@ 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',
'@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',
},
},
])
+3304 -1777
View File
File diff suppressed because it is too large Load Diff
+32 -30
View File
@@ -1,7 +1,7 @@
{
"name": "root",
"private": true,
"version": "1.5.1",
"version": "1.8.0",
"type": "module",
"workspaces": [
"packages/page-controller",
@@ -9,6 +9,7 @@
"packages/llms",
"packages/core",
"packages/page-agent",
"packages/mcp",
"packages/extension",
"packages/website"
],
@@ -21,50 +22,51 @@
},
"homepage": "https://alibaba.github.io/page-agent/",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
"node": "^22.13.0 || >=24"
},
"scripts": {
"cleanup": "rm -rf packages/*/dist",
"start": "npm run dev --workspace=@page-agent/website",
"build:website": "npm run build:website --workspace=@page-agent/website",
"build:libs": "npm run build --workspaces --if-present",
"build": "npm run build:libs && npm run build:website",
"dev:ext": "npm run dev -w @page-agent/ext",
"dev:demo": "npm run dev:demo --workspace=page-agent",
"build": "npm run build:libs && npm run build:website",
"build:libs": "npm run build --workspaces --if-present",
"build:website": "npm run build:website --workspace=@page-agent/website",
"build:ext": "npm run build:libs && npm run zip -w @page-agent/ext",
"version": "node scripts/sync-version.js",
"postpublish": "npm run postpublish --workspaces --if-present",
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json && tsc --noEmit -p packages/extension/tsconfig.json",
"lint": "eslint .",
"prepare": "husky"
"cleanup": "rm -rf packages/*/dist && rm -rf packages/*/.output",
"prepare": "husky || true"
},
"devDependencies": {
"@commitlint/cli": "^20.4.2",
"@commitlint/config-conventional": "^20.4.2",
"@eslint/js": "^9.39.2",
"@microsoft/api-extractor": "^7.57.6",
"@tailwindcss/vite": "^4.2.1",
"@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0",
"@eslint-react/eslint-plugin": "^4.2.3",
"@eslint/js": "^10.0.1",
"@microsoft/api-extractor": "^7.58.2",
"@tailwindcss/vite": "^4.2.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.3.3",
"@vitejs/plugin-react-swc": "^4.1.0",
"@types/node": "^25.6.0",
"@vitejs/plugin-react-swc": "^4.3.0",
"chalk": "^5.6.2",
"concurrently": "^9.2.1",
"dotenv": "^17.3.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.13.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-react-x": "^2.13.0",
"globals": "^17.4.0",
"dotenv": "^17.4.2",
"eslint": "^10.2.0",
"globals": "^17.5.0",
"husky": "^9.1.7",
"lint-staged": "^16.3.1",
"prettier": "^3.8.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.1",
"lint-staged": "^16.4.0",
"prettier": "^3.8.2",
"typescript": "^6.0.2",
"typescript-eslint": "^8.58.2",
"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"
"vite": "^7.3.2",
"vite-bundle-analyzer": "^1.3.7",
"vite-plugin-css-injected-by-js": "^4.0.1"
},
"overrides": {
"typescript": "^5.9.3"
"typescript": "^6.0.2",
"@vitejs/plugin-react": "^5.2.0"
},
"lint-staged": {
"*.{js,ts,cjs,cts,mjs,mts}": [
+22 -12
View File
@@ -1,16 +1,26 @@
{
"name": "@page-agent/core",
"private": false,
"version": "1.5.1",
"version": "1.8.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": [
@@ -39,18 +49,18 @@
"scripts": {
"build": "vite build",
"dev:iife": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
"prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node ../../scripts/post-publish.js"
},
"dependencies": {
"chalk": "^5.6.2",
"@page-agent/llms": "1.5.1",
"@page-agent/page-controller": "1.5.1"
"@page-agent/llms": "1.8.0",
"@page-agent/page-controller": "1.8.0"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"zod": "^4.3.5"
"zod": "^4.3.6"
}
}
+7 -4
View File
@@ -1,5 +1,6 @@
/**
* Copyright (C) 2025 Alibaba Group Holding Limited
* Copyright (C) 2026 SimonLuvRamen
* All rights reserved.
*/
import { InvokeError, LLM, type Tool } from '@page-agent/llms'
@@ -176,7 +177,7 @@ export class PageAgentCore extends EventTarget {
}
/**
* Push a observation message to the history event stream.
* Push an observation message to the history event stream.
* This will be visible in <agent_history> and remain persistent in memory across steps.
* @experimental @internal
* @note history change will be emitted before next step starts
@@ -343,7 +344,7 @@ export class PageAgentCore extends EventTarget {
return result
}
await waitFor(0.4) // @TODO: configurable
await waitFor(this.config.stepDelay ?? 0.4)
}
}
@@ -511,7 +512,8 @@ export class PageAgentCore extends EventTarget {
// Accumulated wait time warning
if (this.#states.totalWaitTime >= 3) {
this.pushObservation(
`You have waited ${this.#states.totalWaitTime} seconds accumulatively. DO NOT wait any longer unless you have a good reason.`
`You have waited ${this.#states.totalWaitTime} seconds accumulatively. ` +
`DO NOT wait any longer unless you have a good reason.`
)
}
@@ -527,7 +529,8 @@ export class PageAgentCore extends EventTarget {
const remaining = this.config.maxSteps - step
if (remaining === 5) {
this.pushObservation(
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
`⚠️ Only ${remaining} steps remaining. ` +
`Consider wrapping up or calling done with partial results.`
)
} else if (remaining === 2) {
this.pushObservation(
+3 -3
View File
@@ -69,7 +69,7 @@ Strictly follow these rules while using the browser and navigating the web:
- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.
- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).
- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.
- If a captcha appears, tell user you can not solve captcha. finished the task and ask user to solve it.
- 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.
@@ -87,11 +87,11 @@ Strictly follow these rules while using the browser and navigating the web:
<capability>
- You can only handle single page app. Do not jump out of current page.
- Do not click on link if it will open in a new page (etc. <a target="_blank">)
- Do not click on link if it will open in a new page (e.g., <a target="_blank">)
- It is ok to fail the task.
- User can be wrong. If the request of user is not achievable, inappropriate or you do not have enough information or tools to achieve it. Tell user to make a better request.
- Webpage can be broken. All webpages or apps have bugs. Some bug will make it hard for your job. It's encouraged to tell user the problem of current page. Your feedbacks (including failing) are valuable for user.
- Trying to hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted result and harmful side-effects. User would rather you to complete the task with a fail.
- Trying too hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted results and harmful side-effects. User would rather you complete the task with a fail.
- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.
</capability>
+3 -2
View File
@@ -131,7 +131,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 +156,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),
+6
View File
@@ -152,6 +152,12 @@ export interface AgentConfig extends LLMConfig {
* @experimental Use with caution - incorrect prompts may break agent behavior.
*/
customSystemPrompt?: string
/**
* Delay between steps in seconds.
* @default 0.4
*/
stepDelay?: number
}
/**
+2 -2
View File
@@ -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
+10 -2
View File
@@ -31,7 +31,7 @@ export function randomID(existingIDs?: string[]): string {
id = Math.random().toString(36).substring(2, 11)
tryCount++
if (tryCount > MAX_TRY) {
throw new Error('randomID: too many try')
throw new Error('randomID: too many tries')
}
}
@@ -61,7 +61,15 @@ const llmsTxtCache = new Map<string, string | null>()
/** Fetch /llms.txt for a URL's origin. Cached per origin, `null` = tried and not found. */
export async function fetchLlmsTxt(url: string): Promise<string | null> {
const origin = new URL(url).origin
let origin: string
try {
origin = new URL(url).origin
} catch {
return null // Invalid URL
}
// about:blank, data:, file:
if (origin === 'null') return null
if (llmsTxtCache.has(origin)) return llmsTxtCache.get(origin)!
const endpoint = `${origin}/llms.txt`
-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"]
}
+9 -4
View File
@@ -11,13 +11,18 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
export default defineConfig({
clearScreen: false,
plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
dts({
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'),
+15 -2
View File
@@ -115,12 +115,21 @@ import type {
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
// 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
@@ -205,9 +214,13 @@ import type {
interface ExecuteConfig {
baseURL: string
apiKey: string
model: string
apiKey?: string
systemInstruction?: string
includeInitialTab?: boolean
experimentalIncludeAllTabs?: boolean
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
+16 -16
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/ext",
"private": true,
"version": "0.1.11",
"version": "1.8.0",
"type": "module",
"scripts": {
"dev": "wxt",
@@ -16,32 +16,32 @@
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@types/chrome": "^0.1.37",
"@tailwindcss/vite": "^4.2.2",
"@types/chrome": "^0.1.40",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.1",
"@wxt-dev/module-react": "^1.1.5",
"@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.576.0",
"motion": "^12.34.5",
"lucide-react": "^1.8.0",
"motion": "^12.38.0",
"next-themes": "^0.4.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"rough-notation": "^0.5.1",
"simple-icons": "^16.10.0",
"simple-icons": "^16.16.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.14",
"tailwindcss": "^4.2.2",
"tw-animate-css": "^1.4.0",
"wxt": "^0.20.18"
"wxt": "^0.20.20"
},
"dependencies": {
"@page-agent/core": "1.5.1",
"@page-agent/llms": "1.5.1",
"@page-agent/page-controller": "1.5.1",
"@page-agent/ui": "1.5.1",
"@page-agent/core": "1.8.0",
"@page-agent/llms": "1.8.0",
"@page-agent/page-controller": "1.8.0",
"@page-agent/ui": "1.8.0",
"ai-motion": "^0.4.8",
"chalk": "^5.6.2"
},
+10 -4
View File
@@ -1,4 +1,4 @@
import { type PageAgentConfig, PageAgentCore } from '@page-agent/core'
import { type AgentConfig, PageAgentCore } from '@page-agent/core'
import { RemotePageController } from './RemotePageController'
import { TabsController } from './TabsController'
@@ -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: Omit<PageAgentConfig, 'pageController'> & { includeInitialTab?: boolean }) {
constructor(config: MultiPageAgentConfig) {
// multi page controller
const tabsController = new TabsController()
const pageController = new RemotePageController(tabsController)
@@ -31,8 +36,8 @@ export class MultiPageAgent extends PageAgentCore {
`Default working language: **${targetLanguage}**`
)
// include initial tab for controlling
const includeInitialTab = config.includeInitialTab ?? true
const experimentalIncludeAllTabs = config.experimentalIncludeAllTabs ?? false
/**
* When the agent is in side-panel and user closed the side-panel.
@@ -50,7 +55,7 @@ export class MultiPageAgent extends PageAgentCore {
customSystemPrompt: systemPrompt,
onBeforeTask: async (agent) => {
await tabsController.init(agent.task, includeInitialTab)
await tabsController.init(agent.task, { includeInitialTab, experimentalIncludeAllTabs })
heartBeatInterval = window.setInterval(() => {
chrome.storage.local.set({
@@ -75,6 +80,7 @@ export class MultiPageAgent extends PageAgentCore {
},
onBeforeStep: async (agent) => {
if (!tabsController.currentTabId) return
// make sure the current tab is loaded before the step starts
await tabsController.waitUntilTabLoaded(tabsController.currentTabId!)
},
@@ -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,9 +56,7 @@ export class RemotePageController {
}
async getBrowserState(): Promise<BrowserState> {
if (!this.currentTabId) throw new Error('tabsController not initialized.')
let browserState = {} as BrowserState
let browserState: BrowserState
debug('getBrowserState', this.currentTabId)
const currentUrl = await this.getCurrentUrl()
@@ -178,7 +174,7 @@ interface DomActionReturn {
/**
* Check if a URL can run content scripts.
*/
function isContentScriptAllowed(url: string | undefined): boolean {
export function isContentScriptAllowed(url: string | undefined): boolean {
if (!url) return false
const restrictedPatterns = [
@@ -5,9 +5,7 @@ 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`)
export function handleTabControlMessage(
message: { type: 'TAB_CONTROL'; action: TabAction; payload: any },
@@ -20,11 +18,10 @@ export function handleTabControlMessage(
case 'get_active_tab': {
debug('get_active_tab')
chrome.tabs
.query({ active: true, currentWindow: true })
.query({ active: 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: success', tabs)
sendResponse({ success: true, tab: tabs[0] })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
@@ -52,7 +49,7 @@ export function handleTabControlMessage(
.create({ url: payload.url, active: false })
.then((newTab) => {
debug('open_new_tab: success', newTab)
sendResponse({ success: true, tabId: newTab.id, windowId: newTab.windowId })
sendResponse({ success: true, tabId: newTab.id })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
@@ -114,47 +111,59 @@ export function handleTabControlMessage(
return true // async response
}
case 'get_window_tabs': {
debug('get_window_tabs', payload)
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')
const tabEventPorts = new Set<chrome.runtime.Port>()
function broadcastTabEvent(message: object) {
for (const port of tabEventPorts) {
port.postMessage(message)
}
}
/**
* Port-based tab events: agents connect via `chrome.runtime.connect({ name: 'tab-events' })`
* and receive tab change events through the port. Works for both extension pages and content scripts.
*/
export function setupTabEventsPort() {
chrome.runtime.onConnect.addListener((port) => {
if (port.name !== 'tab-events') return
debug('port connected', port.sender?.tab?.id ?? port.sender?.url)
tabEventPorts.add(port)
port.onDisconnect.addListener(() => {
debug('port disconnected')
tabEventPorts.delete(port)
})
})
chrome.tabs.onCreated.addListener((tab) => {
debug('onCreated', tab)
chrome.runtime
.sendMessage({ type: 'TAB_CHANGE', action: 'created', payload: { tab } })
.catch((error) => {
debug('onCreated error:', error)
})
broadcastTabEvent({ action: 'created', payload: { tab } })
})
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)
})
broadcastTabEvent({ action: 'removed', payload: { tabId, removeInfo } })
})
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)
})
broadcastTabEvent({ action: 'updated', payload: { tabId, changeInfo, tab } })
})
}
+176 -112
View File
@@ -1,8 +1,8 @@
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'
@@ -20,102 +20,102 @@ function sendMessage(message: {
* - live in the agent env (extension page or content script)
* - no chrome apis. call sw for tab operations
*/
export class TabsController extends EventTarget {
export class TabsController {
currentTabId: number | null = null
private disposed = false
private port?: chrome.runtime.Port
private portRetries = 0
private windowId: number | null = null
private tabs: TabMeta[] = []
private initialTabId: number | null = null
private tabGroupId: number | null = null
private experimentalIncludeAllTabs = false
private task: string = ''
private windowId: number | null = null
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)
if (this.disposed) {
throw new Error('TabsController already disposed')
}
this.task = task
this.tabs = []
this.currentTabId = null
this.disposed = false
this.port = undefined
this.portRetries = 0
this.windowId = null
this.tabs = []
this.tabGroupId = null
this.initialTabId = null
this.windowId = null
this.experimentalIncludeAllTabs = experimentalIncludeAllTabs
this.task = task
const result = await sendMessage({
const activeTabResult = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_active_tab',
})
this.initialTabId = result.tabId
this.initialTabId = activeTabResult.tab?.id
this.windowId = activeTabResult.tab?.windowId
if (!this.initialTabId) {
throw new Error('Failed to get initial tab ID')
if (!this.initialTabId || !this.windowId) {
if (activeTabResult.error) {
throw new Error(activeTabResult.error)
} else {
throw new Error('Failed to get active tab')
}
}
if (includeInitialTab) {
this.currentTabId = this.initialTabId
this.connectTabEvents()
// update tab status immediately
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 },
})
this.tabs.push({
id: result.tabId,
isInitial: true,
url: info.url,
title: info.title,
status: info.status,
})
if (isContentScriptAllowed(info.url) && !info.pinned) {
this.currentTabId = this.initialTabId
this.addTab({
id: this.initialTabId,
isInitial: true,
url: info.url,
title: info.title,
status: info.status,
})
await this.createTabGroup([this.initialTabId])
}
}
await this.updateCurrentTabId(this.currentTabId)
const tabChangeHandler = (message: any): void => {
if (message.type !== 'TAB_CHANGE') {
// throw new Error(`[TabsController]: Invalid message type: ${message.type}`)
return
}
if (message.action === 'created') {
const tab = message.payload.tab as chrome.tabs.Tab
if (tab.groupId === this.tabGroupId && tab.id != null) {
// Tab created in our controlled group
if (!this.tabs.find((t) => t.id === tab.id)) {
this.tabs.push({ id: tab.id, isInitial: false })
}
this.switchToTab(tab.id)
}
} else if (message.action === 'removed') {
const { tabId } = message.payload as { tabId: number }
const targetTab = this.tabs.find((t) => t.id === tabId)
if (targetTab) {
this.tabs = this.tabs.filter((t) => t.id !== tabId)
if (this.currentTabId === tabId) {
const newCurrentTab = this.tabs[this.tabs.length - 1] || null
if (newCurrentTab) {
this.switchToTab(newCurrentTab.id)
} else {
this.updateCurrentTabId(null)
}
}
}
} else if (message.action === 'updated') {
const { tabId, tab } = message.payload as { tabId: number; tab: chrome.tabs.Tab }
const targetTab = this.tabs.find((t) => t.id === tabId)
if (targetTab) {
targetTab.url = tab.url
targetTab.title = tab.title
targetTab.status = tab.status
}
}
}
chrome.runtime.onMessage.addListener(tabChangeHandler)
this.addEventListener('dispose', () => {
chrome.runtime.onMessage.removeListener(tabChangeHandler)
})
}
async openNewTab(url: string): Promise<string> {
@@ -132,11 +132,8 @@ export class TabsController extends EventTarget {
}
const tabId = result.tabId as number
const windowId = result.windowId as number
this.windowId = windowId
this.tabs.push({
this.addTab({
id: tabId,
isInitial: false,
})
@@ -144,32 +141,7 @@ export class TabsController extends EventTarget {
await this.switchToTab(tabId)
if (!this.tabGroupId) {
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'create_tab_group',
payload: { tabIds: [tabId], windowId: this.windowId },
})
if (!result.success) {
throw new Error(`Failed to create tab group: ${result.error}`)
}
const groupId = result.groupId as number
this.tabGroupId = groupId
await sendMessage({
type: 'TAB_CONTROL',
action: 'update_tab_group',
payload: {
groupId: this.tabGroupId,
properties: {
title: `PageAgent(${this.task})`,
color: randomColor(),
collapsed: false,
},
},
})
await this.createTabGroup([tabId])
} else {
await sendMessage({
type: 'TAB_CONTROL',
@@ -230,6 +202,38 @@ export class TabsController extends EventTarget {
}
}
private async createTabGroup(tabIds: number[]) {
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'create_tab_group',
payload: { tabIds, windowId: this.windowId },
})
if (!result?.success) {
throw new Error(`Failed to create tab group: ${result?.error}`)
}
this.tabGroupId = result.groupId as number
await sendMessage({
type: 'TAB_CONTROL',
action: 'update_tab_group',
payload: {
groupId: this.tabGroupId,
properties: {
title: `PageAgent(${this.task})`,
color: randomColor(),
collapsed: false,
},
},
})
}
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)
@@ -286,9 +290,77 @@ export class TabsController extends EventTarget {
await waitUntil(() => tab.status === 'complete', 4_000)
}
dispose() {
this.dispatchEvent(new Event('dispose'))
/**
* Connect to background SW via port to receive tab change events.
*
* @note Port is 1:1 (runtime.connect → background SW has no frames),
* so onDisconnect fires exactly once and we can safely reconnect.
* Reconnection may miss events during the gap.
* TODO: refresh this.tabs from background after reconnect to stay consistent.
*/
private connectTabEvents() {
this.port = chrome.runtime.connect({ name: 'tab-events' })
this.port.onMessage.addListener((message: any) => {
if (this.disposed) return
this.portRetries = 0
if (message.action === 'created') {
const tab = message.payload.tab as chrome.tabs.Tab
const shouldTrack = this.experimentalIncludeAllTabs || tab.groupId === this.tabGroupId
if (shouldTrack && tab.id != null) {
this.addTab({ 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
}
}
})
this.port.onDisconnect.addListener(() => {
this.port = undefined
if (this.disposed) return
if (this.portRetries >= 7) {
console.error(PREFIX, 'tab events port failed after 7 retries, giving up')
return
}
debug('port disconnected, reconnecting...')
this.portRetries++
this.connectTabEvents()
})
}
dispose() {
debug('dispose')
this.disposed = true
this.port?.disconnect()
this.port = undefined
}
}
export interface TabsInitOptions {
includeInitialTab?: boolean
experimentalIncludeAllTabs?: boolean
}
export type TabAction =
@@ -300,6 +372,7 @@ export type TabAction =
| 'add_tab_to_group'
| 'close_tab'
| 'get_tab_title'
| 'get_window_tabs'
interface TabMeta {
id: number
@@ -309,16 +382,7 @@ interface TabMeta {
status?: 'loading' | 'unloaded' | 'complete'
}
const TAB_GROUP_COLORS = [
'grey',
'blue',
'red',
'yellow',
'green',
'pink',
'purple',
'cyan',
] as const
const TAB_GROUP_COLORS = ['blue', 'red', 'yellow', 'green', 'pink', 'purple', 'cyan'] as const
type TabGroupColor = (typeof TAB_GROUP_COLORS)[number]
+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 */
@@ -71,7 +71,7 @@ Strictly follow these rules while using the browser and navigating the web:
- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.
- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).
- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.
- If a captcha appears, tell user you can not solve captcha. finished the task and ask user to solve it.
- 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.
+14 -3
View File
@@ -4,6 +4,7 @@
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
SupportedLanguage,
} from '@page-agent/core'
@@ -20,6 +21,8 @@ export interface AdvancedConfig {
maxSteps?: number
systemInstruction?: string
experimentalLlmsTxt?: boolean
experimentalIncludeAllTabs?: boolean
disableNamedToolChoice?: boolean
}
export interface ExtConfig extends LLMConfig, AdvancedConfig {
@@ -32,7 +35,7 @@ export interface UseAgentResult {
activity: AgentActivity | null
currentTask: string
config: ExtConfig | null
execute: (task: string) => Promise<void>
execute: (task: string) => Promise<ExecutionResult>
stop: () => void
configure: (config: ExtConfig) => Promise<void>
}
@@ -110,7 +113,7 @@ export function useAgent(): UseAgentResult {
setCurrentTask(task)
setHistory([])
await agent.execute(task)
return agent.execute(task)
}, [])
const stop = useCallback(() => {
@@ -123,6 +126,8 @@ export function useAgent(): UseAgentResult {
maxSteps,
systemInstruction,
experimentalLlmsTxt,
experimentalIncludeAllTabs,
disableNamedToolChoice,
...llmConfig
}: ExtConfig) => {
await chrome.storage.local.set({ llmConfig })
@@ -131,7 +136,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 })
},
@@ -1,18 +1,20 @@
import {
ChevronDown,
Copy,
CornerUpLeft,
ExternalLink,
Eye,
EyeOff,
FoldVertical,
HatGlasses,
Home,
Loader2,
Scale,
UnfoldVertical,
} from 'lucide-react'
import { useEffect, useState } from 'react'
import { siGithub } from 'simple-icons'
import { DEMO_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'
@@ -25,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(() => {
@@ -94,6 +106,8 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
maxSteps: maxSteps || undefined,
systemInstruction: systemInstruction || undefined,
experimentalLlmsTxt,
experimentalIncludeAllTabs,
disableNamedToolChoice,
})
} finally {
setSaving(false)
@@ -153,6 +167,16 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
</div>
</div>
{/* Hub link */}
<a
href="/hub.html"
target="_blank"
className="flex items-center justify-between p-3 rounded-md border bg-muted/50 text-xs font-medium text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors"
>
Manage Page Agent Hub
<ExternalLink className="size-3" />
</a>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Base URL</label>
<Input
@@ -167,7 +191,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
{isTestingEndpoint(baseURL) && (
<div className="p-2.5 rounded-md border border-amber-500/30 bg-amber-500/5 text-[11px] text-muted-foreground leading-relaxed">
<Scale className="size-3 inline-block mr-1 -mt-0.5 text-amber-600" />
You are using the free testing API. By using this service you agree to the{' '}
You are using our testing API. By using this you agree to the{' '}
<a
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
target="_blank"
@@ -176,14 +200,13 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
>
Terms of Use & Privacy Policy
</a>
. No sensitive data. No guaranteed availability.
</div>
)}
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Model</label>
<Input
placeholder="gpt-5.2"
placeholder="gpt-5.1"
value={model}
onChange={(e) => setModel(e.target.value)}
className="text-xs h-8"
@@ -195,7 +218,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
<div className="flex gap-2 items-center">
<Input
type={showApiKey ? 'text' : 'password'}
placeholder="sk-..."
// placeholder="sk-..."
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="text-xs h-8"
@@ -212,7 +235,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Language</label>
<label className="text-xs text-muted-foreground">Response Language</label>
<select
value={language ?? ''}
onChange={(e) => setLanguage((e.target.value || undefined) as LanguagePreference)}
@@ -231,10 +254,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground cursor-pointer mt-1 font-bold"
>
Advanced
<ChevronDown
className="size-3 transition-transform"
style={{ transform: advancedOpen ? 'rotate(0deg)' : 'rotate(90deg)' }}
/>
{advancedOpen ? <FoldVertical className="size-3" /> : <UnfoldVertical className="size-3" />}
</button>
{advancedOpen && (
@@ -263,10 +283,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>
</>
)}
@@ -286,6 +319,10 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
{/* Footer */}
<div className="mt-4 mb-4 pt-4 border-t border-border/50 flex gap-2 justify-between text-[10px] text-muted-foreground">
<div className="flex flex-col justify-between">
<span>
Version <span className="font-mono">v{__VERSION__}</span>
</span>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
@@ -297,7 +334,9 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
</svg>
<span>Source Code</span>
</a>
</div>
<div className="flex flex-col items-end">
<a
href="https://alibaba.github.io/page-agent/"
target="_blank"
@@ -315,18 +354,9 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
className="flex items-center gap-1 hover:text-foreground"
>
<HatGlasses className="size-3" />
<span>Privacy Policy</span>
<span>Privacy</span>
</a>
</div>
<div className="flex flex-col items-end">
<span>
Extension <span className="font-mono">v{__EXT_VERSION__}</span>
</span>
<span>
PageAgent <span className="font-mono">v{__CORE_VERSION__}</span>
</span>
</div>
</div>
{/* attribute */}
@@ -1,4 +1,4 @@
import { AlertTriangle, RotateCcw } from 'lucide-react'
import { AlertTriangle, Eraser, RotateCcw } from 'lucide-react'
import { Component, type ErrorInfo, type ReactNode } from 'react'
import { Button } from '@/components/ui/button'
@@ -27,6 +27,11 @@ export class ErrorBoundary extends Component<Props, State> {
window.location.reload()
}
handleResetConfig = async () => {
await chrome.storage.local.remove(['llmConfig', 'language', 'advancedConfig'])
window.location.reload()
}
render() {
if (!this.state.hasError) {
return this.props.children
@@ -39,10 +44,16 @@ export class ErrorBoundary extends Component<Props, State> {
<p className="text-sm text-muted-foreground mb-4 max-w-xs">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
<Button variant="outline" size="sm" onClick={this.handleReload}>
<RotateCcw className="size-3.5 mr-2" />
Reload Panel
</Button>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={this.handleResetConfig}>
<Eraser className="size-3.5 mr-2" />
Reset Config
</Button>
<Button variant="outline" size="sm" onClick={this.handleReload}>
<RotateCcw className="size-3.5 mr-2" />
Reload Panel
</Button>
</div>
</div>
)
}
@@ -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,9 @@
import { ArrowLeft, CheckCircle, Trash2, XCircle } from 'lucide-react'
import { ArrowDownToLine, ArrowLeft, CheckCircle, RotateCcw, Trash2, XCircle } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { type SessionRecord, clearSessions, deleteSession, listSessions } from '@/lib/db'
import { downloadHistoryExport } from '@/lib/history-export'
function timeAgo(ts: number): string {
const seconds = Math.floor((Date.now() - ts) / 1000)
@@ -18,9 +19,11 @@ 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)
@@ -31,7 +34,6 @@ export function HistoryList({
}, [])
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
load()
}, [load])
@@ -41,6 +43,16 @@ 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 */}
@@ -85,7 +97,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 +109,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>
@@ -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>
)
@@ -43,20 +43,24 @@ export function MotionOverlay({ active }: { active: boolean }) {
const motionRef = useRef<Motion | null>(null)
useEffect(() => {
const mode = document.documentElement.classList.contains('dark') ? 'dark' : 'light'
const motion = new Motion({
mode,
borderWidth: 4,
borderRadius: 14,
glowWidth: mode === 'dark' ? 120 : 60,
styles: { position: 'absolute', inset: '0' },
})
motionRef.current = motion
containerRef.current!.appendChild(motion.element)
motion.autoResize(containerRef.current!)
try {
const mode = document.documentElement.classList.contains('dark') ? 'dark' : 'light'
const motion = new Motion({
mode,
borderWidth: 4,
borderRadius: 14,
glowWidth: mode === 'dark' ? 120 : 60,
styles: { position: 'absolute', inset: '0' },
})
motionRef.current = motion
containerRef.current!.appendChild(motion.element)
motion.autoResize(containerRef.current!)
} catch (e) {
console.warn('[MotionOverlay] Motion unavailable:', e)
}
return () => {
motion.dispose()
motionRef.current?.dispose()
motionRef.current = null
}
}, [])
@@ -80,7 +84,7 @@ export function MotionOverlay({ active }: { active: boolean }) {
return (
<div
ref={containerRef}
className="pointer-events-none absolute inset-0 z-10 opacity-60"
className="pointer-events-none absolute inset-0 z-10 opacity-60 overflow-hidden"
style={{ display: active ? undefined : 'none' }}
/>
)
@@ -107,6 +111,7 @@ export function EmptyState() {
]}
cursorStyle="underscore"
loop
startOnView={false}
typeSpeed={20}
deleteSpeed={10}
pauseDelay={3000}
@@ -8,7 +8,7 @@ function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimi
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 cursor-pointer items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
@@ -1,12 +1,12 @@
import { handlePageControlMessage } from '@/agent/RemotePageController.background'
import { handleTabControlMessage, setupTabChangeEvents } from '@/agent/TabsController.background'
import { handleTabControlMessage, setupTabEventsPort } from '@/agent/TabsController.background'
export default defineBackground(() => {
console.log('[Background] Service Worker started')
// tab change events
setupTabChangeEvents()
setupTabEventsPort()
// generate user auth token
@@ -30,7 +30,34 @@ export default defineBackground(() => {
}
})
// external messages (from localhost launcher page via externally_connectable)
chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
if (message.type === 'OPEN_HUB') {
openOrFocusHubTab(message.wsPort).then(() => {
if (sender.tab?.id) chrome.tabs.remove(sender.tab.id)
sendResponse({ ok: true })
})
return true
}
})
// setup
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
})
async function openOrFocusHubTab(wsPort: number) {
const hubUrl = chrome.runtime.getURL('hub.html')
const existing = await chrome.tabs.query({ url: `${hubUrl}*` })
if (existing.length > 0 && existing[0].id) {
await chrome.tabs.update(existing[0].id, {
active: true,
url: `${hubUrl}?ws=${wsPort}`,
})
return
}
await chrome.tabs.create({ url: `${hubUrl}?ws=${wsPort}`, pinned: true })
}
@@ -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
@@ -0,0 +1,250 @@
import { FoldVertical, Plug, PlugZap, Square, UnfoldVertical, Unplug } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useAgent } from '@/agent/useAgent'
import { ActivityCard, EventCard } from '@/components/cards'
import { Logo, MotionOverlay, StatusDot } from '@/components/misc'
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import { useHubWs } from './hub-ws'
export default function App() {
const { status, history, activity, currentTask, config, execute, stop, configure } = useAgent()
const { wsState } = useHubWs(execute, stop, configure, config)
const historyRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (historyRef.current) {
historyRef.current.scrollTop = historyRef.current.scrollHeight
}
}, [history, activity])
const isRunning = status === 'running'
const WsIcon = wsState === 'connected' ? PlugZap : wsState === 'connecting' ? Plug : Unplug
const wsLabel = {
connected: 'Connected',
connecting: 'Connecting…',
disconnected: new URLSearchParams(location.search).get('ws') ? 'Disconnected' : 'No connection',
}[wsState]
return (
<div className="flex h-screen bg-background">
{/* Left — Protocol docs */}
<aside className="w-80 shrink-0 border-r flex flex-col bg-muted/20">
<a
href="https://alibaba.github.io/page-agent/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-5 h-12 border-b hover:bg-muted/30 transition-colors"
>
<Logo className="size-5" />
<span className="text-sm font-semibold tracking-tight">Page Agent Hub</span>
<span className="text-[9px] font-medium uppercase tracking-wider text-amber-600 bg-amber-500/10 border border-amber-500/30 rounded px-1.5 py-0.5">
Beta
</span>
</a>
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-6">
<div className="text-xs text-muted-foreground leading-relaxed space-y-2">
<p>
Page Agent Hub lets local apps (e.g. MCP servers) control the Page Agent extension via
WebSocket.
</p>
<p>
Check out the official{' '}
<a
href="https://github.com/alibaba/page-agent/tree/main/packages/mcp"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
MCP server package
</a>
.
</p>
</div>
<HubConfig />
<ProtocolDocsCollapsible />
</div>
<div className="border-t px-5 py-3 text-[10px] text-muted-foreground/60 flex items-center justify-between">
<span className="font-mono">v{__VERSION__}</span>
<span>
Built with by{' '}
<a
href="https://github.com/gaomeng1900"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
@Simon
</a>
</span>
</div>
</aside>
{/* Right — Live session */}
<main className="flex-1 flex flex-col min-w-0 relative">
<MotionOverlay active={isRunning} />
<header className="flex items-center justify-between border-b px-5 h-12">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<WsIcon className="size-3.5" />
<span>{wsLabel}</span>
</div>
<div className="flex items-center gap-3">
<StatusDot status={status} />
{isRunning && (
<Button variant="destructive" size="sm" onClick={stop} className="h-7 text-xs">
<Square className="size-3 mr-1" />
Stop
</Button>
)}
</div>
</header>
{/* Task banner */}
{currentTask && (
<div className="border-b px-5 py-2 bg-muted/30">
<div className="text-[10px] text-muted-foreground uppercase tracking-wide">
Current Task
</div>
<div className="text-sm font-medium truncate" title={currentTask}>
{currentTask}
</div>
</div>
)}
{/* Event stream */}
<div ref={historyRef} className="flex-1 overflow-y-auto p-5 space-y-2">
{!currentTask && history.length === 0 && !isRunning && (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
<WsIcon className="size-10 opacity-30" />
<p className="text-sm">
{wsState === 'connected'
? 'Waiting for task from external caller…'
: 'No active session'}
</p>
</div>
)}
{history.map((event, index) => (
<EventCard key={index} event={event} />
))}
{activity && <ActivityCard activity={activity} />}
</div>
</main>
</div>
)
}
function HubConfig() {
const [allowAll, setAllowAll] = useState(false)
useEffect(() => {
chrome.storage.local.get('allowAllHubConnection').then((r) => {
setAllowAll(r.allowAllHubConnection === true)
})
}, [])
const toggle = (checked: boolean) => {
setAllowAll(checked)
chrome.storage.local.set({ allowAllHubConnection: checked })
}
return (
<div>
<h3 className="text-[11px] font-semibold text-foreground/80 uppercase tracking-wider mb-2">
Config
</h3>
<div className="group/hub relative">
<label
className={`flex items-center justify-between p-3 rounded-md border cursor-pointer text-xs ${allowAll ? 'bg-amber-500/10 border-amber-500/30 text-amber-600' : 'bg-muted/50 text-muted-foreground'}`}
>
Auto-approve connections
<Switch
checked={allowAll}
onCheckedChange={toggle}
className={allowAll ? 'data-[state=checked]:bg-amber-500' : ''}
/>
</label>
{/* hide with invisible absolute opacity-0*/}
<div className="group-hover/hub:visible group-hover/hub:opacity-100 transition-opacity duration-150 left-0 right-0 top-full z-10 pt-2">
<div className="relative p-2.5 rounded-md border border-border bg-background/60 backdrop-blur-md shadow-2xl text-muted-foreground text-xs leading-relaxed">
<div className="absolute -top-1.5 left-5 size-3 rotate-45 rounded-[1px] border-l border-t border-border bg-background/60 backdrop-blur-md" />
By default, each connection requires your approval before running tasks. <br />
Enable this to skip per-session approval.
<br />
<span className="font-semibold">* Use with caution!</span>
</div>
</div>
</div>
</div>
)
}
function ProtocolDocsCollapsible() {
const [open, setOpen] = useState(false)
return (
<div>
<button
type="button"
onClick={() => setOpen(!open)}
className="flex items-center gap-1 text-[11px] font-semibold text-foreground/80 uppercase tracking-wider cursor-pointer"
>
Docs
{open ? <FoldVertical className="size-3" /> : <UnfoldVertical className="size-3" />}
</button>
{open && (
<div className="mt-3 space-y-4 text-xs text-muted-foreground">
<p className="text-[10px]">
Connect via <code className="text-[10px]">hub.html?ws=PORT</code>
</p>
<section>
<h4 className="text-[11px] font-medium text-foreground/60 mb-1.5">Flow</h4>
<ol className="list-decimal list-inside space-y-1 text-[11px] leading-relaxed">
<li>Hub opens WS to caller's server</li>
<li>
Sends <code className="text-[10px]">ready</code>
</li>
<li>
Caller sends <code className="text-[10px]">execute</code> with task
</li>
<li>Hub runs agent, streams events</li>
<li>
Hub sends <code className="text-[10px]">result</code> or{' '}
<code className="text-[10px]">error</code>
</li>
</ol>
</section>
<section>
<h4 className="text-[11px] font-medium text-foreground/60 mb-1.5">Caller Hub</h4>
<pre className="bg-muted/50 rounded-md p-3 font-mono text-[10px] leading-relaxed whitespace-pre-wrap">
{`{ type: "execute", task: string, config?: object }
{ type: "stop" }`}
</pre>
</section>
<section>
<h4 className="text-[11px] font-medium text-foreground/60 mb-1.5">Hub Caller</h4>
<pre className="bg-muted/50 rounded-md p-3 font-mono text-[10px] leading-relaxed whitespace-pre-wrap">
{`{ type: "ready" }
{ type: "result", success: boolean, data: string }
{ type: "error", message: string }`}
</pre>
</section>
</div>
)}
</div>
)
}
@@ -0,0 +1,243 @@
/**
* Hub WebSocket Protocol
*
* Hub connects as WS client to `ws://localhost:{port}`.
* All messages are JSON. One task at a time.
*
* Inbound (Caller → Hub):
* { type: "execute", task: string, config?: object }
* { type: "stop" }
*
* Outbound (Hub → Caller):
* { type: "ready" }
* { type: "result", success: boolean, data: string }
* { type: "error", message: string }
*/
import type { ExecutionResult } from '@page-agent/core'
import { useEffect, useRef, useState } from 'react'
import type { ExtConfig } from '@/agent/useAgent'
// --- Protocol types ---
interface ExecuteMessage {
type: 'execute'
task: string
config?: Record<string, unknown>
}
interface StopMessage {
type: 'stop'
}
type InboundMessage = ExecuteMessage | StopMessage
interface ReadyMessage {
type: 'ready'
}
interface ResultMessage {
type: 'result'
success: boolean
data: string
}
interface ErrorMessage {
type: 'error'
message: string
}
type OutboundMessage = ReadyMessage | ResultMessage | ErrorMessage
export type HubWsState = 'connecting' | 'connected' | 'disconnected'
// --- HubWs class ---
export interface HubWsHandlers {
onExecute: (
task: string,
config?: Record<string, unknown>
) => Promise<{ success: boolean; data: string }>
onStop: () => void
}
/**
* Framework-agnostic WebSocket client for Hub.
* Connects to an external WS server, receives tasks, dispatches to handlers,
* and sends results back. No React, no DOM.
*/
export class HubWs {
#ws: WebSocket | null = null
#state: HubWsState = 'disconnected'
#busy = false
#approved = false
#handlers: HubWsHandlers
#port: number
#onStateChange: (state: HubWsState) => void
constructor(port: number, handlers: HubWsHandlers, onStateChange: (state: HubWsState) => void) {
this.#port = port
this.#handlers = handlers
this.#onStateChange = onStateChange
}
get state() {
return this.#state
}
get busy() {
return this.#busy
}
connect() {
if (this.#ws) return
this.#setState('connecting')
const ws = new WebSocket(`ws://localhost:${this.#port}`)
this.#ws = ws
ws.addEventListener('open', () => {
this.#setState('connected')
this.#send({ type: 'ready' })
})
ws.addEventListener('close', () => {
this.#ws = null
this.#busy = false
this.#approved = false
this.#setState('disconnected')
})
ws.addEventListener('message', (event) => {
this.#handleMessage(event.data as string)
})
}
disconnect() {
this.#ws?.close()
this.#ws = null
this.#busy = false
this.#approved = false
this.#setState('disconnected')
}
#setState(state: HubWsState) {
if (this.#state === state) return
this.#state = state
this.#onStateChange(state)
}
#send(msg: OutboundMessage) {
if (this.#ws?.readyState === WebSocket.OPEN) {
this.#ws.send(JSON.stringify(msg))
}
}
async #handleMessage(raw: string) {
let msg: InboundMessage
try {
msg = JSON.parse(raw)
} catch {
return
}
if (!(await this.#checkApproval())) {
this.#send({ type: 'error', message: 'User denied the connection request.' })
return
}
switch (msg.type) {
case 'execute':
this.#handleExecute(msg)
break
case 'stop':
this.#handlers.onStop()
break
}
}
async #checkApproval(): Promise<boolean> {
if (this.#approved) return true
const { allowAllHubConnection } = await chrome.storage.local.get('allowAllHubConnection')
if (allowAllHubConnection === true) {
this.#approved = true
return true
}
const ok = window.confirm(
'An external application is requesting to control your browser via Page Agent Ext.\nAllow this session?'
)
if (ok) this.#approved = true
return ok
}
async #handleExecute(msg: ExecuteMessage) {
if (this.#busy) {
this.#send({ type: 'error', message: 'Hub is busy with another task' })
return
}
this.#busy = true
try {
const result = await this.#handlers.onExecute(msg.task, msg.config)
this.#send({ type: 'result', success: result.success, data: result.data })
} catch (err) {
this.#send({ type: 'error', message: err instanceof Error ? err.message : String(err) })
} finally {
this.#busy = false
}
}
}
// --- React hook ---
/**
* React hook that bridges HubWs to the agent's execute/stop/configure.
* Handles the config-before-execute dance internally.
*/
export function useHubWs(
execute: (task: string) => Promise<ExecutionResult>,
stop: () => void,
configure: (config: ExtConfig) => Promise<void>,
config: ExtConfig | null
): { wsState: HubWsState } {
const wsPort = new URLSearchParams(location.search).get('ws')
const [wsState, setWsState] = useState<HubWsState>(() => (wsPort ? 'connecting' : 'disconnected'))
const hubWsRef = useRef<HubWs | null>(null)
const latestRef = useRef({ execute, stop, configure, config })
useEffect(() => {
latestRef.current = { execute, stop, configure, config }
})
useEffect(() => {
if (!wsPort) return
const hubWs = new HubWs(
Number(wsPort),
{
onExecute: async (task, incomingConfig) => {
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: () => latestRef.current.stop(),
},
setWsState
)
hubWs.connect()
hubWsRef.current = hubWs
return () => {
hubWs.disconnect()
hubWsRef.current = null
}
}, [wsPort])
return { wsState }
}
@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/assets/page-agent-64.png" />
<title>Page Agent Hub</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
@@ -0,0 +1,25 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ErrorBoundary } from '@/components/ErrorBoundary'
import App from './App'
import '@/assets/index.css'
const syncDarkMode = () => {
document.documentElement.classList.toggle(
'dark',
matchMedia('(prefers-color-scheme: dark)').matches
)
}
syncDarkMode()
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', syncDarkMode)
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>
)
@@ -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,
},
},
},
@@ -110,9 +122,9 @@ export default defineUnlistedScript(() => {
)
}
;(window as any).PAGE_AGENT_EXT_VERSION = __EXT_VERSION__
;(window as any).PAGE_AGENT_EXT_VERSION = __VERSION__
;(window as any).PAGE_AGENT_EXT = {
version: __EXT_VERSION__,
version: __VERSION__,
execute,
stop,
}
@@ -1,6 +1,11 @@
import { History, Send, Settings, Square } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { ConfigPanel } from '@/components/ConfigPanel'
import { HistoryDetail } from '@/components/HistoryDetail'
import { HistoryList } from '@/components/HistoryList'
import { ActivityCard, EventCard } from '@/components/cards'
import { EmptyState, Logo, MotionOverlay, StatusDot } from '@/components/misc'
import { Button } from '@/components/ui/button'
import {
InputGroup,
@@ -11,11 +16,6 @@ import {
import { saveSession } from '@/lib/db'
import { useAgent } from '../../agent/useAgent'
import { ConfigPanel } from './components/ConfigPanel'
import { HistoryDetail } from './components/HistoryDetail'
import { HistoryList } from './components/HistoryList'
import { ActivityCard, EventCard } from './components/cards'
import { EmptyState, Logo, MotionOverlay, StatusDot } from './components/misc'
type View =
| { name: 'chat' }
@@ -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 ---
@@ -163,7 +178,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} />
))}
@@ -1,8 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ErrorBoundary } from '@/components/ErrorBoundary'
import App from './App'
import { ErrorBoundary } from './components/ErrorBoundary'
import '@/assets/index.css'
@@ -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')
}
+1 -2
View File
@@ -1,2 +1 @@
declare const __EXT_VERSION__: string
declare const __CORE_VERSION__: string
declare const __VERSION__: string
+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" }
]
}
}
+8 -2
View File
@@ -19,8 +19,7 @@ export default defineConfig({
vite: () => ({
plugins: [tailwindcss()],
define: {
__EXT_VERSION__: JSON.stringify(pkg.version),
__CORE_VERSION__: JSON.stringify(pkg.dependencies['@page-agent/core']),
__VERSION__: JSON.stringify(pkg.version),
},
optimizeDeps: {
force: true,
@@ -37,7 +36,11 @@ export default defineConfig({
},
},
}),
zip: {
artifactTemplate: 'page-agent-ext-{{version}}-{{browser}}.zip',
},
manifest: {
key: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqbzT0iTYeYlnCvDJIGDnGU8oarJgZILDzSfLi/ufuSxXEPDKuMyD892GhvrMCZNVHS11Sh6NYUOc/PcUOhtaR2urHtcNkrpSJNV10zUamY7fxBdVEkOucfyLu8INVy+teis62MoRWYPaUPkfZUjrLGW8MsZ9aFzARfu9GGDEp2EAYsWDN6w6vyz9LJ82pm542EWnVT4MjmDPgvYFCWGBtaU/dfHD+GAX6URJFapsCvryVURKJ+76c/GO9/I3EX1IBfbY6dec78bLCMvVxiTmiv36KyGPwX1OpakW8IiCpXWdbAxjm+plbYlp5t5zTyyoE3sOSFeXsBH0Kg27o8GcvQIDAQAB',
default_locale: 'en',
name: '__MSG_extName__',
description: '__MSG_extDescription__',
@@ -59,5 +62,8 @@ export default defineConfig({
side_panel: {
default_path: 'sidepanel/index.html',
},
externally_connectable: {
matches: ['http://localhost/*'],
},
},
})
+20 -10
View File
@@ -1,15 +1,25 @@
{
"name": "@page-agent/llms",
"version": "1.5.1",
"version": "1.8.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,8 @@
"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{}})\""
"prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node ../../scripts/post-publish.js"
},
"dependencies": {
"chalk": "^5.6.2"
@@ -43,6 +53,6 @@
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"zod": "^4.3.5"
"zod": "^4.3.6"
}
}
+8 -5
View File
@@ -29,16 +29,19 @@ export class OpenAIClient implements LLMClient {
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,
}
modelPatch(requestBody)
@@ -50,7 +53,7 @@ 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),
signal: abortSignal,
+4 -3
View File
@@ -8,19 +8,20 @@ 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) {
if (!config.baseURL || !config.model) {
throw new Error(
'[PageAgent] LLM configuration required. Please provide: baseURL, apiKey, model. ' +
'[PageAgent] LLM configuration required. Please provide: baseURL, model. ' +
'See: https://alibaba.github.io/page-agent/docs/features/models'
)
}
return {
baseURL: config.baseURL,
apiKey: config.apiKey,
model: config.model,
apiKey: config.apiKey || '',
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
}
}
+7 -1
View File
@@ -89,12 +89,18 @@ export interface InvokeResult<TResult = unknown> {
*/
export interface LLMConfig {
baseURL: string
apiKey: string
model: string
apiKey?: string
temperature?: number
maxRetries?: number
/**
* 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.
+14
View File
@@ -73,6 +73,11 @@ export function modelPatch(body: Record<string, any>) {
} 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'
)
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'
@@ -88,6 +93,15 @@ export function modelPatch(body: Record<string, any>) {
body.reasoning_effort = 'minimal'
}
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
delete body.parallel_tool_calls
}
return body
}
-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": {}
}
}
+1 -5
View File
@@ -1,11 +1,7 @@
{
"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"],
"exclude": ["dist", "node_modules"]
+11 -4
View File
@@ -11,11 +11,18 @@ console.log(chalk.cyan(`📦 Building @page-agent/llms`))
export default defineConfig({
clearScreen: false,
plugins: [dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true })],
plugins: [
dts({
bundleTypes: true,
compilerOptions: {
composite: true,
noEmit: false,
emitDeclarationOnly: true,
declaration: true,
},
}),
],
publicDir: false,
esbuild: {
keepNames: true,
},
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
+94
View File
@@ -0,0 +1,94 @@
# @page-agent/mcp
MCP server that lets AI agent clients (Claude Desktop, Copilot, etc.) control your browser through the [Page Agent](https://github.com/alibaba/page-agent) extension.
## Prerequisites
- Node.js >= 20
- [Page Agent Extension](https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj) installed in Chrome
- An LLM API key (OpenAI-compatible)
## Installation
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"page-agent": {
"command": "npx",
"args": ["-y", "@page-agent/mcp"],
"env": {
"LLM_BASE_URL": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"LLM_API_KEY": "sk-xxx",
"LLM_MODEL_NAME": "qwen3.5-plus"
}
}
}
}
```
### Cursor / Copilot
Same format — add the config to the MCP settings of your client.
## MCP Tools
| 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. |
## Environment Variables
| Variable | Default | Description |
| ---------------- | ------- | --------------------- |
| `LLM_BASE_URL` | — | LLM API base URL |
| `LLM_API_KEY` | — | LLM API key |
| `LLM_MODEL_NAME` | — | Model name |
| `PORT` | `38401` | HTTP + WebSocket port |
## How It Works
```
┌──────────────┐ stdio ┌──────────────────┐ WebSocket ┌──────────────┐
│ Claude / │◄────────►│ @page-agent/mcp │◄────────────►│ Hub tab │
│ Copilot │ (MCP) │ (Node.js) │ (localhost) │ (extension) │
└──────────────┘ └──────────────────┘ └──────┬───────┘
│ │
│ HTTP │ useAgent
▼ ▼
┌──────────────────┐ ┌──────────────┐
│ Launcher page │ │ MultiPage │
│ (localhost:PORT) │ │ Agent │
└──────────────────┘ └──────────────┘
```
1. Agent client starts the MCP server via stdio (`npx @page-agent/mcp`).
2. Server starts HTTP + WS on `localhost:PORT`, opens the launcher page in browser.
3. Launcher page triggers the extension to open a **hub tab** (`hub.html?ws=PORT`).
4. Hub connects to the WS server. MCP tools now proxy tasks to the hub.
The hub tab speaks a generic WebSocket protocol (defined in `hub-ws.ts` in the extension package) and has no knowledge of MCP. See the hub's protocol docs for message format details.
## Architecture
Pure JS ESM, no build step. Source files are the published artifacts.
```
src/
├── index.js # CLI entry: MCP server (stdio) + opens launcher
├── hub-bridge.js # HTTP server + WebSocket bridge to hub tab
└── launcher.html # Bootstrap page: detects extension, triggers hub open
```
## Dev
```bash
npm run build:libs
npm run dev:ext
npx @modelcontextprotocol/inspector node packages/mcp/src/index.js
```
+35
View File
@@ -0,0 +1,35 @@
{
"name": "@page-agent/mcp",
"private": false,
"version": "1.8.0",
"type": "module",
"bin": {
"page-agent-mcp": "src/index.js"
},
"files": [
"src/"
],
"description": "MCP server for controlling the browser via Page Agent extension",
"keywords": [
"page-agent",
"mcp",
"browser-automation",
"chrome-extension"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git",
"directory": "packages/mcp"
},
"homepage": "https://alibaba.github.io/page-agent/",
"engines": {
"node": ">=20"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"ws": "^8.20.0",
"zod": "^4.3.6"
}
}
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env node
import { readFileSync } from 'node:fs'
import http from 'node:http'
import { fileURLToPath } from 'node:url'
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)),
'utf-8'
)
/**
* HTTP + WebSocket bridge to the hub.html extension tab.
* - HTTP serves the launcher page (triggers extension to open hub)
* - WS carries execute/stop commands and result/error responses
*/
export class HubBridge {
/** @type {number} */
port
/** @type {http.Server} */
#httpServer
/** @type {WebSocketServer} */
#wss
/** @type {import('ws').WebSocket | null} */
#hub = null
/** @type {{ resolve: (r: {success: boolean, data: string}) => void, reject: (e: Error) => void } | null} */
#pendingTask = null
/** @param {number} port */
constructor(port) {
this.port = port
this.#httpServer = http.createServer((_req, res) => {
const html = launcherTemplate
.replaceAll('__EXT_ID__', EXT_ID)
.replaceAll('__STORE_URL__', STORE_URL)
.replaceAll('__WS_PORT__', String(port))
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(html)
})
this.#wss = new WebSocketServer({ server: this.#httpServer })
this.#wss.on('connection', (ws) => this.#onConnection(ws))
}
/** @returns {Promise<void>} */
async start() {
return new Promise((resolve, reject) => {
this.#httpServer.on('error', (/** @type {NodeJS.ErrnoException} */ err) => {
if (err.code === 'EADDRINUSE') {
reject(
new Error(`Port ${this.port} is in use. Another Page Agent MCP server may be running.`)
)
} else {
reject(err)
}
})
this.#httpServer.listen(this.port, LOOPBACK_HOST, () => {
console.error(`[page-agent-mcp] HTTP + WS on http://${LOOPBACK_HOST}:${this.port}`)
resolve()
})
})
}
get connected() {
return this.#hub?.readyState === 1
}
get busy() {
return this.#pendingTask !== null
}
/**
* @param {string} task
* @param {Record<string, unknown>} [config]
* @returns {Promise<{success: boolean, data: string}>}
*/
async executeTask(task, config) {
if (!this.connected) throw new Error('Hub is not connected. Is the extension running?')
if (this.#pendingTask) throw new Error('Agent is already running a task.')
return new Promise((resolve, reject) => {
this.#pendingTask = { resolve, reject }
this.#hub.send(JSON.stringify({ type: 'execute', task, config }))
})
}
stopTask() {
if (this.connected) {
this.#hub.send(JSON.stringify({ type: 'stop' }))
}
}
// TODO: Add version checking
/** @param {import('ws').WebSocket} ws */
#onConnection(ws) {
if (this.#hub && this.#hub.readyState === 1) {
ws.close(4000, 'Another hub is already connected')
return
}
this.#hub = ws
console.error('[page-agent-mcp] Hub connected')
ws.on('message', (/** @type {Buffer} */ rawData) => {
/** @type {{ type: string, success?: boolean, data?: string, message?: string }} */
let msg
try {
msg = JSON.parse(rawData.toString('utf-8'))
} catch {
return
}
if (msg.type === 'result') {
this.#pendingTask?.resolve({ success: msg.success ?? false, data: msg.data ?? '' })
this.#pendingTask = null
} else if (msg.type === 'error') {
this.#pendingTask?.reject(new Error(msg.message ?? 'Unknown error from hub'))
this.#pendingTask = null
}
})
ws.on('close', () => {
console.error('[page-agent-mcp] Hub disconnected')
if (this.#hub === ws) this.#hub = null
if (this.#pendingTask) {
this.#pendingTask.reject(new Error('Hub disconnected while task was running'))
this.#pendingTask = null
}
})
}
}
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { exec } from 'node:child_process'
import { platform } from 'node:os'
import * as z from 'zod/v4'
import { HubBridge } from './hub-bridge.js'
const env = process.env
const port = parseInt(env.PORT || '38401')
/** @type {Record<string, string>} */
const llmConfig = {}
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) ---
const hub = new HubBridge(port)
await hub.start()
// Open launcher in default browser
const url = `http://localhost:${port}`
const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start ""' : 'xdg-open'
exec(`${cmd} "${url}"`, (err) => {
if (err) console.error(`[page-agent-mcp] Could not open browser: ${err.message}`)
})
// --- MCP server (stdio) ---
const mcpServer = new McpServer({ name: 'page-agent', version: '1.5.8' })
mcpServer.registerTool(
'execute_task',
{
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 {
const config = Object.keys(llmConfig).length > 0 ? llmConfig : undefined
const result = await hub.executeTask(task, config)
return {
content: [
{
type: 'text',
text: result.success
? `Task completed.\n\n${result.data}`
: `Task failed.\n\n${result.data}`,
},
],
}
} catch (err) {
return {
content: [{ type: 'text', text: `Error: ${err.message}` }],
isError: true,
}
}
}
)
mcpServer.registerTool(
'get_status',
{
description: 'Check the current status of the Page Agent hub.',
},
async () => ({
content: [
{
type: 'text',
text: JSON.stringify({ connected: hub.connected, busy: hub.busy }, null, 2),
},
],
})
)
mcpServer.registerTool(
'stop_task',
{
description: 'Stop the currently running browser automation task.',
},
async () => {
hub.stopTask()
return { content: [{ type: 'text', text: 'Stop signal sent.' }] }
}
)
const transport = new StdioServerTransport()
await mcpServer.connect(transport)
console.error('[page-agent-mcp] MCP server ready (stdio)')
+237
View File
@@ -0,0 +1,237 @@
<!doctype html>
<html>
<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" />
<title>Page Agent MCP Launcher</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
system-ui,
-apple-system,
sans-serif;
background: #09090b;
color: #e5e5e5;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.card {
text-align: center;
max-width: 480px;
padding: 3rem 2rem;
}
.logo {
width: 72px;
height: 72px;
border-radius: 18px;
margin-bottom: 1rem;
}
.badge {
font-size: 0.6875rem;
font-weight: 500;
color: #52525b;
letter-spacing: 0.05em;
text-transform: uppercase;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.35rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.sub {
font-size: 0.875rem;
color: #a1a1aa;
line-height: 1.7;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid #27272a;
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 1.5rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
a {
color: #60a5fa;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.install {
display: none;
}
.install.show {
display: block;
}
.tips {
margin-top: 1.75rem;
text-align: left;
background: #18181b;
border: 1px solid #27272a;
border-radius: 12px;
padding: 1.25rem 1.5rem;
}
.tips li {
font-size: 0.8125rem;
color: #a1a1aa;
line-height: 1.7;
margin-left: 1rem;
}
.tips li + li {
margin-top: 0.35rem;
}
.store-btn {
display: inline-flex;
align-items: center;
gap: 0.625rem;
margin-top: 1.5rem;
padding: 0.625rem 1.5rem;
background: #2563eb;
color: #fff;
border-radius: 10px;
font-size: 0.875rem;
font-weight: 500;
transition: background 0.15s;
}
.store-btn:hover {
background: #1d4ed8;
text-decoration: none;
}
.store-btn img {
width: 20px;
height: 20px;
}
.links {
margin-top: 1.75rem;
display: flex;
justify-content: center;
gap: 1.5rem;
font-size: 0.8125rem;
}
.links a {
color: #71717a;
transition: color 0.15s;
}
.links a:hover {
color: #a1a1aa;
text-decoration: none;
}
</style>
</head>
<body>
<div class="card">
<img
class="logo"
src="https://img.alicdn.com/imgextra/i3/O1CN01JPT4Fj1FJTfmHfNxO_!!6000000000466-49-tps-512-512.webp"
alt="Page Agent"
/>
<div class="badge">Page Agent MCP Launcher</div>
<div id="connecting">
<div class="spinner"></div>
<h1 data-i18n="connecting_title">Connecting to Page Agent</h1>
<p class="sub" data-i18n="connecting_sub">Opening the hub in your browser…</p>
</div>
<div id="install" class="install">
<h1 data-i18n="install_title">Extension Required</h1>
<p class="sub" data-i18n="install_sub">
Page Agent requires the latest browser extension to work.
</p>
<a class="store-btn" href="__STORE_URL__" target="_blank">
<img
src="https://img.alicdn.com/imgextra/i3/O1CN01JpW0Vo1sR3FpiZKFM_!!6000000005762-55-tps-192-192.svg"
alt=""
/>
<span data-i18n="install_btn">Install from Chrome Web Store</span>
</a>
<ul class="tips">
<li data-i18n="tip_outdated">
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.
</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>
</div>
</div>
<script>
{
const EXT_ID = '__EXT_ID__'
const wsPort = __WS_PORT__
const zh = {
connecting_title: '正在连接 Page Agent',
connecting_sub: '正在浏览器中打开 Hub…',
install_title: '需要安装浏览器插件',
install_sub: 'Page Agent 需要安装最新版浏览器插件才能运行。',
install_btn: '从 Chrome 应用商店安装',
tip_outdated: '如果插件版本过旧,请更新到最新版本。',
tip_other_browser:
'如果该浏览器中未安装插件,请从装有插件的浏览器打开此页面。',
tip_refresh: '安装或更新后,请刷新此页面。',
link_docs: '文档',
link_issues: '问题反馈',
}
if (/^zh\b/i.test(navigator.language)) {
document.querySelectorAll('[data-i18n]').forEach((el) => {
const key = el.getAttribute('data-i18n')
if (zh[key]) el.textContent = zh[key]
})
}
const showInstall = () => {
document.getElementById('connecting').style.display = 'none'
document.getElementById('install').classList.add('show')
}
try {
if (!globalThis.chrome?.runtime?.sendMessage) {
showInstall()
} else {
chrome.runtime.sendMessage(
EXT_ID,
{ type: 'OPEN_HUB', wsPort },
(response) => {
if (chrome.runtime.lastError || !response?.ok) showInstall()
}
)
}
} catch {
showInstall()
}
}
</script>
</body>
</html>
+24 -14
View File
@@ -1,16 +1,26 @@
{
"name": "page-agent",
"private": false,
"version": "1.5.1",
"version": "1.8.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.1",
"@page-agent/llms": "1.5.1",
"@page-agent/page-controller": "1.5.1",
"@page-agent/ui": "1.5.1",
"@page-agent/core": "1.8.0",
"@page-agent/llms": "1.8.0",
"@page-agent/page-controller": "1.8.0",
"@page-agent/ui": "1.8.0",
"chalk": "^5.6.2"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"zod": "^4.3.5"
"zod": "^4.3.6"
}
}
+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,
})
}
}
+2 -1
View File
@@ -17,9 +17,10 @@ const DEMO_MODEL = 'qwen3.5-plus'
const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
const DEMO_API_KEY = 'NA'
const currentScript = document.currentScript as HTMLScriptElement | null
// in case document.x is not ready yet
setTimeout(() => {
const currentScript = document.currentScript as HTMLScriptElement | null
let config: PageAgentConfig
if (currentScript) {
-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"]
}
+9 -4
View File
@@ -11,13 +11,18 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
export default defineConfig({
clearScreen: false,
plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
dts({
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'),
+1 -12
View File
@@ -9,7 +9,7 @@ import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
// Load .env from repo root
dotenvConfig({ path: resolve(__dirname, '../../.env') })
dotenvConfig({ path: resolve(__dirname, '../../.env'), quiet: true })
// UMD Bundle for CDN
// - alias all local packages so that they can be build in
@@ -21,17 +21,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'),
+19 -9
View File
@@ -1,15 +1,25 @@
{
"name": "@page-agent/page-controller",
"version": "1.5.1",
"version": "1.8.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,8 @@
"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{}})\""
"prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node ../../scripts/post-publish.js"
},
"dependencies": {
"ai-motion": "^0.4.8"
+15 -9
View File
@@ -14,17 +14,16 @@ import {
scrollVertically,
selectOptionElement,
} from './actions'
import { VIEWPORT_EXPANSION } from './constants'
import * as dom from './dom'
import type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type'
import { getPageInfo } from './dom/getPageInfo'
import { patchReact } from './patches/react'
import { isAnchorElement } from './utils'
/**
* Configuration for PageController
*/
export interface PageControllerConfig extends dom.DomConfig {
viewportExpansion?: number
/** Enable visual mask overlay during operations (default: false) */
enableMask?: boolean
}
@@ -131,7 +130,7 @@ export class PageController extends EventTarget {
const url = window.location.href
const title = document.title
const pi = getPageInfo()
const viewportExpansion = this.config.viewportExpansion ?? VIEWPORT_EXPANSION
const viewportExpansion = dom.resolveViewportExpansion(this.config.viewportExpansion)
await this.updateTree()
@@ -186,7 +185,7 @@ export class PageController extends EventTarget {
const blacklist = [
...(this.config.interactiveBlacklist || []),
...document.querySelectorAll('[data-page-agent-not-interactive]').values(),
...Array.from(document.querySelectorAll('[data-page-agent-not-interactive]')),
]
this.flatTree = dom.getFlatTree({
@@ -194,7 +193,11 @@ export class PageController extends EventTarget {
interactiveBlacklist: blacklist,
})
this.simplifiedHTML = dom.flatTreeToString(this.flatTree, this.config.includeAttributes)
this.simplifiedHTML = dom.flatTreeToString(
this.flatTree,
this.config.includeAttributes,
this.config.keepSemanticTags
)
this.selectorMap.clear()
this.selectorMap = dom.getSelectorMap(this.flatTree)
@@ -219,6 +222,7 @@ export class PageController extends EventTarget {
* Clean up all element highlights
*/
async cleanUpHighlights(): Promise<void> {
console.log('[PageController] cleanUpHighlights')
dom.cleanUpHighlights()
}
@@ -245,7 +249,7 @@ export class PageController extends EventTarget {
await clickElement(element)
// Handle links that open in new tabs
if (element instanceof HTMLAnchorElement && element.target === '_blank') {
if (isAnchorElement(element) && element.target === '_blank') {
return {
success: true,
message: `✅ Clicked element (${elemText ?? index}). ⚠️ Link opened in a new tab.`,
@@ -322,11 +326,11 @@ export class PageController extends EventTarget {
this.assertIndexed()
const scrollAmount = pixels ?? numPages * (down ? 1 : -1) * window.innerHeight
const scrollAmount = (pixels ?? numPages * window.innerHeight) * (down ? 1 : -1)
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
const message = await scrollVertically(down, scrollAmount, element)
const message = await scrollVertically(scrollAmount, element)
return {
success: true,
@@ -357,7 +361,7 @@ export class PageController extends EventTarget {
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
const message = await scrollHorizontally(right, scrollAmount, element)
const message = await scrollHorizontally(scrollAmount, element)
return {
success: true,
@@ -425,3 +429,5 @@ export class PageController extends EventTarget {
this.mask = null
}
}
export * from './actions'
+223 -102
View File
@@ -3,27 +3,22 @@
* All rights reserved.
*/
import type { InteractiveElementDomNode } from './dom/dom_tree/type'
// ======= general utils =======
async function waitFor(seconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
}
// ======= dom utils =======
export async function movePointerToElement(element: HTMLElement) {
const rect = element.getBoundingClientRect()
const x = rect.left + rect.width / 2
const y = rect.top + rect.height / 2
window.dispatchEvent(new CustomEvent('PageAgent::MovePointerTo', { detail: { x, y } }))
await waitFor(0.3)
}
import {
clickPointer,
disablePassThrough,
enablePassThrough,
getNativeValueSetter,
isHTMLElement,
isInputElement,
isSelectElement,
isTextAreaElement,
movePointerToElement,
waitFor,
} from './utils'
/**
* Get the HTMLElement by index from a selectorMap.
* @private Internal method, subject to change at any time.
*/
export function getElementByIndex(
selectorMap: Map<number, InteractiveElementDomNode>,
@@ -39,7 +34,7 @@ export function getElementByIndex(
throw new Error(`Element at index ${index} does not have a reference`)
}
if (!(element instanceof HTMLElement)) {
if (!isHTMLElement(element)) {
throw new Error(`Element at index ${index} is not an HTMLElement`)
}
@@ -50,101 +45,200 @@ let lastClickedElement: HTMLElement | null = null
function blurLastClickedElement() {
if (lastClickedElement) {
lastClickedElement.dispatchEvent(new PointerEvent('pointerout', { bubbles: true }))
lastClickedElement.dispatchEvent(new PointerEvent('pointerleave', { bubbles: false }))
lastClickedElement.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }))
lastClickedElement.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }))
lastClickedElement.blur()
lastClickedElement.dispatchEvent(
new MouseEvent('mouseout', { bubbles: true, cancelable: true })
)
lastClickedElement = null
}
}
/**
* Simulate a click on the element
* Simulate a full click following W3C Pointer Events + UI Events spec order:
* pointerover/enter mouseover/enter pointerdown mousedown [focus]
* pointerup mouseup click
*
* @private Internal method, subject to change at any time.
*/
export async function clickElement(element: HTMLElement) {
blurLastClickedElement()
lastClickedElement = element
await scrollIntoViewIfNeeded(element)
await movePointerToElement(element)
window.dispatchEvent(new CustomEvent('PageAgent::ClickPointer'))
const frame = element.ownerDocument.defaultView?.frameElement
if (frame) await scrollIntoViewIfNeeded(frame)
const rect = element.getBoundingClientRect()
const x = rect.left + rect.width / 2
const y = rect.top + rect.height / 2
await movePointerToElement(element, x, y)
await clickPointer()
await waitFor(0.1)
// hover it
element.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true }))
element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true }))
// Hit-test to find the deepest element at click coordinates, matching
// real browser behavior where events target the innermost element.
// @note This may hit a element in the blacklist
// TODO: This is a temporary workaround. Should have been handled during dom extraction.
const doc = element.ownerDocument
await enablePassThrough()
const hitTarget = doc.elementFromPoint(x, y)
await disablePassThrough()
const target =
hitTarget instanceof HTMLElement && element.contains(hitTarget) ? hitTarget : element
// dispatch a sequence of events to ensure all listeners are triggered
element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }))
const pointerOpts = {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
pointerType: 'mouse',
}
const mouseOpts = { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 }
// focus it to ensure it gets the click event
element.focus()
// Hover — pointer events first, then mouse events (spec order)
target.dispatchEvent(new PointerEvent('pointerover', pointerOpts))
target.dispatchEvent(new PointerEvent('pointerenter', { ...pointerOpts, bubbles: false }))
target.dispatchEvent(new MouseEvent('mouseover', mouseOpts))
target.dispatchEvent(new MouseEvent('mouseenter', { ...mouseOpts, bubbles: false }))
element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }))
element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
// Press
target.dispatchEvent(new PointerEvent('pointerdown', pointerOpts))
target.dispatchEvent(new MouseEvent('mousedown', mouseOpts))
// dispatch a click event
// element.click()
// Focus is not part of the standard pointer/mouse event sequence
// "undefined and varies between user agents".
// We focus the original element (nearest focusable ancestor), not the hit-test target, matching browser behavior.
element.focus({ preventScroll: true })
await waitFor(0.2) // Wait to ensure click event processing completes
// Release
target.dispatchEvent(new PointerEvent('pointerup', pointerOpts))
target.dispatchEvent(new MouseEvent('mouseup', mouseOpts))
// Click — activation behavior (navigation, form submit, etc.) triggers
// via bubbling from target up to the interactive ancestor.
target.click()
await waitFor(0.2)
}
// eslint-disable-next-line @typescript-eslint/unbound-method
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
)!.set!
// eslint-disable-next-line @typescript-eslint/unbound-method
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
'value'
)!.set!
/**
* create a synthetic keyboard event
* with key keycode code
* @private Internal method, subject to change at any time.
*/
export async function createSyntheticInputEvent(elem: HTMLElement, key: string) {
elem.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key }))
await waitFor(0.01)
if (elem instanceof HTMLInputElement || elem instanceof HTMLTextAreaElement) {
elem.dispatchEvent(new Event('beforeinput', { bubbles: true }))
await waitFor(0.01)
elem.dispatchEvent(new Event('input', { bubbles: true }))
await waitFor(0.01)
}
elem.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, cancelable: true, key }))
}
export async function inputTextElement(element: HTMLElement, text: string) {
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
throw new Error('Element is not an input or textarea')
const isContentEditable = element.isContentEditable
if (!isInputElement(element) && !isTextAreaElement(element) && !isContentEditable) {
throw new Error('Element is not an input, textarea, or contenteditable')
}
await clickElement(element)
if (element instanceof HTMLTextAreaElement) {
nativeTextAreaValueSetter.call(element, text)
if (isContentEditable) {
// Contenteditable support (partial)
// Not supported:
// - Monaco/CodeMirror: Require direct JS instance access. No universal way to obtain.
// - Draft.js: Not responsive to synthetic/execCommand/Range/DataTransfer. Unmaintained.
//
// Strategy: Try Plan A (synthetic events) first, then verify and fall back
// to Plan B (execCommand) if the text wasn't actually inserted.
//
// Plan A: Dispatch synthetic events
// Works: React contenteditable, Quill.
// Fails: Slate.js, some contenteditable editors that ignore synthetic events.
// Sequence: beforeinput -> mutation -> input -> change -> blur
// Dispatch beforeinput + mutation + input for clearing
if (
element.dispatchEvent(
new InputEvent('beforeinput', {
bubbles: true,
cancelable: true,
inputType: 'deleteContent',
})
)
) {
element.innerText = ''
element.dispatchEvent(
new InputEvent('input', {
bubbles: true,
inputType: 'deleteContent',
})
)
}
// Dispatch beforeinput + mutation + input for insertion (important for React apps)
if (
element.dispatchEvent(
new InputEvent('beforeinput', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: text,
})
)
) {
element.innerText = text
element.dispatchEvent(
new InputEvent('input', {
bubbles: true,
inputType: 'insertText',
data: text,
})
)
}
// Verify Plan A worked by checking if the text was actually inserted
const planASucceeded = element.innerText.trim() === text.trim()
if (!planASucceeded) {
// Plan B: execCommand fallback (deprecated but widely supported)
// Works: Quill, Slate.js, react contenteditable components.
// This approach integrates with the browser's undo stack and is handled
// natively by most rich-text editors.
element.focus()
// Select all existing content and delete it
const doc = element.ownerDocument
const selection = (doc.defaultView || window).getSelection()
const range = doc.createRange()
range.selectNodeContents(element)
selection?.removeAllRanges()
selection?.addRange(range)
// eslint-disable-next-line @typescript-eslint/no-deprecated
doc.execCommand('delete', false)
// eslint-disable-next-line @typescript-eslint/no-deprecated
doc.execCommand('insertText', false, text)
}
// Dispatch change event (for good measure)
element.dispatchEvent(new Event('change', { bubbles: true }))
// Trigger blur for validation
element.blur()
} else {
nativeInputValueSetter.call(element, text)
getNativeValueSetter(element as HTMLInputElement | HTMLTextAreaElement).call(element, text)
}
const inputEvent = new Event('input', { bubbles: true })
element.dispatchEvent(inputEvent)
// Only dispatch shared input event for non-contenteditable (contenteditable has its own)
if (!isContentEditable) {
element.dispatchEvent(new Event('input', { bubbles: true }))
}
await waitFor(0.1) // Wait to ensure input event processing completes
await waitFor(0.1)
blurLastClickedElement()
}
/**
* @todo browser-use version is very complex and supports menu tags, need to follow up
* @private Internal method, subject to change at any time.
*/
export async function selectOptionElement(selectElement: HTMLSelectElement, optionText: string) {
if (!(selectElement instanceof HTMLSelectElement)) {
if (!isSelectElement(selectElement)) {
throw new Error('Element is not a select element')
}
@@ -161,23 +255,26 @@ export async function selectOptionElement(selectElement: HTMLSelectElement, opti
await waitFor(0.1) // Wait to ensure change event processing completes
}
export async function scrollIntoViewIfNeeded(element: HTMLElement) {
const el = element as any
if (el.scrollIntoViewIfNeeded) {
interface ScrollableElement extends Element {
scrollIntoViewIfNeeded?: (centerIfNeeded?: boolean) => void
}
/**
* @private Internal method, subject to change at any time.
*/
export async function scrollIntoViewIfNeeded(element: Element) {
const el = element as ScrollableElement
if (typeof el.scrollIntoViewIfNeeded === 'function') {
el.scrollIntoViewIfNeeded()
// await waitFor(0.5) // Animation playback
} else {
// @todo visibility check
el.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' })
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' })
// await waitFor(0.5) // Animation playback
}
}
export async function scrollVertically(
down: boolean,
scroll_amount: number,
element?: HTMLElement | null
) {
export async function scrollVertically(scroll_amount: number, element?: HTMLElement | null) {
// Element-specific scrolling if element is provided
if (element) {
const targetElement = element
@@ -190,7 +287,10 @@ export async function scrollVertically(
while (currentElement && attempts < 10) {
const computedStyle = window.getComputedStyle(currentElement)
const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY)
const hasScrollableY =
/(auto|scroll|overlay)/.test(computedStyle.overflowY) ||
(computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== 'auto') ||
(computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== 'auto')
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight
if (hasScrollableY && canScrollVertically) {
@@ -242,9 +342,20 @@ export async function scrollVertically(
el.scrollHeight > el.clientHeight &&
bigEnough(el)
// @deprecated Heuristic container search.
// Unreliable in multi-panel layouts. Should guide LLMs to use indexed scroll for consistency.
// TODO: remove this fallback
// try to find the nearest scrollable container
// document.activeElement is usually body.
// After a successful element.focus(), activeElement become the nearest focusable parent
let el: HTMLElement | null = document.activeElement as HTMLElement | null
while (el && !canScroll(el) && el !== document.body) el = el.parentElement
// Something is wrong if it falls back to global '*' search
// TODO: Return error message instead of global '*' search
el = canScroll(el)
? el
: Array.from(document.querySelectorAll<HTMLElement>('*')).find(canScroll) ||
@@ -275,6 +386,10 @@ export async function scrollVertically(
return `✅ Scrolled page by ${scrolled}px.`
} else {
// Container scroll
const warningMsg = `The document is not scrollable. Falling back to container scroll.`
console.log(`[PageController] ${warningMsg}`)
const scrollBefore = el!.scrollTop
const scrollMax = el!.scrollHeight - el!.clientHeight
@@ -286,26 +401,22 @@ export async function scrollVertically(
if (Math.abs(scrolled) < 1) {
return dy > 0
? `⚠️ Already at the bottom of container (${el!.tagName}), cannot scroll down further.`
: `⚠️ Already at the top of container (${el!.tagName}), cannot scroll up further.`
? `⚠️ ${warningMsg} Already at the bottom of container (${el!.tagName}), cannot scroll down further.`
: `⚠️ ${warningMsg} Already at the top of container (${el!.tagName}), cannot scroll up further.`
}
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
const reachedTop = dy < 0 && scrollAfter <= 1
if (reachedBottom)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the bottom.`
return ` ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the bottom.`
if (reachedTop)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the top.`
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px.`
return ` ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the top.`
return ` ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px.`
}
}
export async function scrollHorizontally(
right: boolean,
scroll_amount: number,
element?: HTMLElement | null
) {
export async function scrollHorizontally(scroll_amount: number, element?: HTMLElement | null) {
// Element-specific scrolling if element is provided
if (element) {
const targetElement = element
@@ -314,11 +425,14 @@ export async function scrollHorizontally(
let scrolledElement: HTMLElement | null = null
let scrollDelta = 0
let attempts = 0
const dx = right ? scroll_amount : -scroll_amount
const dx = scroll_amount
while (currentElement && attempts < 10) {
const computedStyle = window.getComputedStyle(currentElement)
const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX)
const hasScrollableX =
/(auto|scroll|overlay)/.test(computedStyle.overflowX) ||
(computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== 'auto') ||
(computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== 'auto')
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth
if (hasScrollableX && canScrollHorizontally) {
@@ -362,7 +476,8 @@ export async function scrollHorizontally(
// Page-level scrolling (default or fallback)
const dx = right ? scroll_amount : -scroll_amount
const dx = scroll_amount
const bigEnough = (el: HTMLElement) => el.clientWidth >= window.innerWidth * 0.5
const canScroll = (el: HTMLElement | null) =>
el &&
@@ -370,6 +485,9 @@ export async function scrollHorizontally(
el.scrollWidth > el.clientWidth &&
bigEnough(el)
// @deprecated Same heuristic container search as scrollVertically.
// TODO: Remove once LLMs reliably use indexed scrolling via data-scrollable.
let el: HTMLElement | null = document.activeElement as HTMLElement | null
while (el && !canScroll(el) && el !== document.body) el = el.parentElement
@@ -404,6 +522,9 @@ export async function scrollHorizontally(
return `✅ Scrolled page horizontally by ${scrolled}px.`
} else {
// Container scroll
const warningMsg = `The document is not scrollable. Falling back to container scroll.`
console.log(`[PageController] ${warningMsg}`)
const scrollBefore = el!.scrollLeft
const scrollMax = el!.scrollWidth - el!.clientWidth
@@ -415,17 +536,17 @@ export async function scrollHorizontally(
if (Math.abs(scrolled) < 1) {
return dx > 0
? `⚠️ Already at the right edge of container (${el!.tagName}), cannot scroll right further.`
: `⚠️ Already at the left edge of container (${el!.tagName}), cannot scroll left further.`
? `⚠️ ${warningMsg} Already at the right edge of container (${el!.tagName}), cannot scroll right further.`
: `⚠️ ${warningMsg} Already at the left edge of container (${el!.tagName}), cannot scroll left further.`
}
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
const reachedLeft = dx < 0 && scrollAfter <= 1
if (reachedRight)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the right edge.`
return ` ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the right edge.`
if (reachedLeft)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the left edge.`
return `✅ Scrolled container (${el!.tagName}) horizontally by ${scrolled}px.`
return ` ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the left edge.`
return ` ${warningMsg} Scrolled container (${el!.tagName}) horizontally by ${scrolled}px.`
}
}
-16
View File
@@ -1,16 +0,0 @@
/**
* Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved.
*/
/**
* Viewport expansion for DOM tree extraction.
* -1 means full page (no viewport restriction)
* 0 means viewport only
* positive values expand the viewport by that many pixels
*
* @note Since isTopElement depends on elementFromPoint,
* it returns null when out of viewport, this feature has no practical use, only differ between -1 and 0
*/
// export const VIEWPORT_EXPANSION = 100
export const VIEWPORT_EXPANSION = -1
+16
View File
@@ -0,0 +1,16 @@
import type { FlatDomTree } from './type'
interface DomTreeArgs {
doHighlightElements?: boolean
focusHighlightIndex?: number
viewportExpansion?: number
debugMode?: boolean
interactiveBlacklist?: Element[]
interactiveWhitelist?: Element[]
highlightOpacity?: number
highlightLabelOpacity?: number
}
declare const domTree: (args?: DomTreeArgs) => FlatDomTree
export default domTree
@@ -17,6 +17,8 @@
* @edit add `data-browser-use-ignore` attribute
* @edit improve `sampleRect`, filter out rects with 0 area
* @edit exclude aria-hidden elements
* @edit make sure attributes exist for interactive candidates.
* @edit fix "aria-*" attributes check
*/
export default (
@@ -501,11 +503,16 @@ export default (
const overflowX = style.overflowX
const overflowY = style.overflowY
// Check scrollable distances
// scrollbar-width/scrollbar-gutter are only set on elements designed to scroll;
// their presence signals scroll intent even when overflow is hidden (e.g. overflow: auto on :hover)
const hasScrollbarSignal =
(style.scrollbarWidth && style.scrollbarWidth !== 'auto') ||
(style.scrollbarGutter && style.scrollbarGutter !== 'auto')
const scrollableX = overflowX === 'auto' || overflowX === 'scroll'
const scrollableY = overflowY === 'auto' || overflowY === 'scroll'
if (!scrollableX && !scrollableY) {
if (!scrollableX && !scrollableY && !hasScrollbarSignal) {
return null // Not scrollable in any direction
}
@@ -519,11 +526,11 @@ export default (
return null // Not scrollable
}
if (!scrollableY && scrollWidth < threshold) {
if (!scrollableY && !hasScrollbarSignal && scrollWidth < threshold) {
return null // Not scrollable horizontally
}
if (!scrollableX && scrollHeight < threshold) {
if (!scrollableX && !hasScrollbarSignal && scrollHeight < threshold) {
return null // Not scrollable vertically
}
@@ -545,6 +552,8 @@ export default (
scrollData: scrollData,
})
console.log('scrollData!!!', scrollData)
return scrollData
}
@@ -1142,6 +1151,31 @@ export default (
* @param {HTMLElement} element - The element to check.
* @returns {boolean} Whether the element is an interactive candidate.
*/
// @edit fix "aria-*" attributes check
const INTERACTIVE_ARIA_ATTRS = [
'aria-expanded',
'aria-checked',
'aria-selected',
'aria-pressed',
'aria-haspopup',
'aria-controls',
'aria-owns',
'aria-activedescendant',
'aria-valuenow',
'aria-valuetext',
'aria-valuemax',
'aria-valuemin',
'aria-autocomplete',
]
function hasInteractiveAria(el) {
for (let i = 0; i < INTERACTIVE_ARIA_ATTRS.length; i++) {
if (el.hasAttribute(INTERACTIVE_ARIA_ATTRS[i])) return true
}
return false
}
function isInteractiveCandidate(element) {
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false
@@ -1166,7 +1200,7 @@ export default (
element.hasAttribute('onclick') ||
element.hasAttribute('role') ||
element.hasAttribute('tabindex') ||
element.hasAttribute('aria-') ||
hasInteractiveAria(element) ||
element.hasAttribute('data-action') ||
element.getAttribute('contenteditable') === 'true'
@@ -1184,8 +1218,9 @@ export default (
'details',
'label',
'option',
'li',
])
const INTERACTIVE_ROLES = new Set([
const DISTINCT_INTERACTIVE_ROLES = new Set([
'button',
'link',
'menuitem',
@@ -1201,6 +1236,9 @@ export default (
'searchbox',
'textbox',
'listbox',
'listitem',
'treeitem',
'row',
'option',
'scrollbar',
])
@@ -1277,7 +1315,7 @@ export default (
return true
}
// Check interactive roles
if (role && INTERACTIVE_ROLES.has(role)) {
if (role && DISTINCT_INTERACTIVE_ROLES.has(role)) {
return true
}
// Check contenteditable
@@ -1296,6 +1334,10 @@ export default (
if (element.hasAttribute('onclick') || typeof element.onclick === 'function') {
return true
}
// ARIA state attributes imply the element manages its own interaction state
if (hasInteractiveAria(element)) {
return true
}
// return false
@@ -1351,6 +1393,12 @@ export default (
return true
}
// Scrollable containers are always distinct — the LLM needs their index for targeted scrolling.
// Check extraData (already set by isScrollableElement in isInteractiveElement) to avoid redundant layout reads.
if (extraData.get(element)?.scrollable) {
return true
}
// Default to false: if it's interactive but doesn't match above,
// assume it triggers the same action as the parent.
return false
@@ -1608,6 +1656,18 @@ export default (
* @edit direct dom ref
*/
nodeData.ref = node
/**
* @edit make sure attributes exist for interactive candidates.
* @note if the element failed the isInteractiveCandidate, attributes would be empty.
*/
if (nodeData.isInteractive && Object.keys(nodeData.attributes).length === 0) {
const attributeNames = node.getAttributeNames?.() || []
for (const name of attributeNames) {
const value = node.getAttribute(name)
nodeData.attributes[name] = value
}
}
}
}
}
+112 -18
View File
@@ -1,4 +1,3 @@
import { VIEWPORT_EXPANSION } from '../constants'
import domTree from './dom_tree/index.js'
import {
ElementDomNode,
@@ -7,20 +6,57 @@ import {
TextDomNode,
} from './dom_tree/type'
/**
* Viewport expansion for DOM tree extraction.
* -1 means full page (no viewport restriction)
* 0 means viewport only
* positive values expand the viewport by that many pixels
*
* @note Since isTopElement depends on elementFromPoint,
* it returns null when out of viewport, this feature has no practical use, only differ between -1 and 0
*/
const DEFAULT_VIEWPORT_EXPANSION = -1
export function resolveViewportExpansion(viewportExpansion?: number): number {
return viewportExpansion ?? DEFAULT_VIEWPORT_EXPANSION
}
export interface DomConfig {
viewportExpansion?: number
interactiveBlacklist?: (Element | (() => Element))[]
interactiveWhitelist?: (Element | (() => Element))[]
includeAttributes?: string[]
highlightOpacity?: number
highlightLabelOpacity?: number
/**
* Preserve semantic landmark tags in dehydrated output even if not interactive
* @note maybe confusing for LLM combining with page scrolling, use with caution
**/
keepSemanticTags?: boolean
}
// TODO: corresponding roles
const SEMANTIC_TAGS = new Set([
'nav',
'menu',
// 'main',
'header',
'footer',
'aside',
// 'article',
// 'form',
'dialog',
])
/**
*
*/
const newElementsCache = new WeakMap<HTMLElement, string>()
export function getFlatTree(config: DomConfig): FlatDomTree {
const viewportExpansion = resolveViewportExpansion(config.viewportExpansion)
const interactiveBlacklist = [] as Element[]
for (const item of config.interactiveBlacklist || []) {
if (typeof item === 'function') {
@@ -43,7 +79,7 @@ export function getFlatTree(config: DomConfig): FlatDomTree {
doHighlightElements: true,
debugMode: true,
focusHighlightIndex: -1,
viewportExpansion: VIEWPORT_EXPANSION,
viewportExpansion,
interactiveBlacklist,
interactiveWhitelist,
highlightOpacity: config.highlightOpacity ?? 0.0,
@@ -74,6 +110,43 @@ export function getFlatTree(config: DomConfig): FlatDomTree {
return elements
}
const globRegexCache = new Map<string, RegExp>()
function globToRegex(pattern: string): RegExp {
let regex = globRegexCache.get(pattern)
if (!regex) {
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&')
regex = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`)
globRegexCache.set(pattern, regex)
}
return regex
}
function matchAttributes(
attrs: Record<string, string>,
patterns: string[]
): Record<string, string> {
const result: Record<string, string> = {}
for (const pattern of patterns) {
if (pattern.includes('*')) {
const regex = globToRegex(pattern)
for (const key of Object.keys(attrs)) {
if (regex.test(key) && attrs[key].trim()) {
result[key] = attrs[key].trim()
}
}
} else {
const value = attrs[pattern]
if (value && value.trim()) {
result[pattern] = value.trim()
}
}
}
return result
}
/**
* elementsToString 使
*/
@@ -117,7 +190,11 @@ interface TreeNode {
*
* @todo
*/
export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: string[]): string {
export function flatTreeToString(
flatTree: FlatDomTree,
includeAttributes: string[] = [],
keepSemanticTags = false
): string {
const DEFAULT_INCLUDE_ATTRIBUTES = [
'title',
'type',
@@ -140,13 +217,16 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
// for jump check
'target',
// absolute 定位的下拉菜单
// absolute position dropdown menu
'aria-haspopup',
'aria-controls',
'aria-owns',
// content editable
'contenteditable',
]
const includeAttrs = [...(includeAttributes || []), ...DEFAULT_INCLUDE_ATTRIBUTES]
const includeAttrs = [...includeAttributes, ...DEFAULT_INCLUDE_ATTRIBUTES]
// Helper function to cap text length
const capTextLength = (text: string, maxLength: number): string => {
@@ -237,6 +317,8 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
const depthStr = '\t'.repeat(depth)
if (node.type === 'element') {
const isSemantic = keepSemanticTags && node.tagName && SEMANTIC_TAGS.has(node.tagName)
// Add element with highlight_index
if (node.highlightIndex !== undefined) {
nextDepth += 1
@@ -245,23 +327,15 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
let attributesHtmlStr = ''
if (includeAttrs.length > 0 && node.attributes) {
const attributesToInclude: Record<string, string> = {}
// Filter attributes
for (const key of includeAttrs) {
const value = node.attributes[key]
if (value && value.trim() !== '') {
attributesToInclude[key] = value.trim()
}
}
const attributesToInclude = matchAttributes(node.attributes, includeAttrs)
// Remove duplicate values (for attributes longer than 5 chars)
const orderedKeys = includeAttrs.filter((key) => key in attributesToInclude)
if (orderedKeys.length > 1) {
const keys = Object.keys(attributesToInclude)
if (keys.length > 1) {
const keysToRemove = new Set<string>()
const seenValues: Record<string, string> = {}
for (const key of orderedKeys) {
for (const key of keys) {
const value = attributesToInclude[key]
if (value.length > 5) {
if (value in seenValues) {
@@ -342,10 +416,30 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
result.push(line)
}
// Process children regardless
// special treatment for semantic tags
// even if they are not interactive, we can keep them for clear context
const emitSemantic = isSemantic && node.highlightIndex === undefined
// to check if this tag is empty
const mark = emitSemantic ? result.length : -1
if (emitSemantic) {
result.push(`${depthStr}<${node.tagName}>`)
nextDepth += 1
}
for (const child of node.children) {
processNode(child, nextDepth, result)
}
if (emitSemantic) {
// empty tag should be removed
if (result.length === mark + 1) {
result.pop()
} else {
result.push(`${depthStr}</${node.tagName}>`)
}
}
} else if (node.type === 'text') {
// Add text only if it doesn't have a highlighted parent
if (hasParentWithHighlightIndex(node)) {
@@ -7,3 +7,7 @@
display: none;
}
.wrapper.visible {
display: block;
}
@@ -5,16 +5,12 @@ import { isPageDark } from './checkDarkMode'
import styles from './SimulatorMask.module.css'
import cursorStyles from './cursor.module.css'
export class SimulatorMask {
export class SimulatorMask extends EventTarget {
shown: boolean = false
wrapper = document.createElement('div')
motion = new Motion({
mode: isPageDark() ? 'dark' : 'light',
styles: {
position: 'absolute',
inset: '0',
},
})
motion: Motion | null = null
#disposed = false
#cursor = document.createElement('div')
@@ -25,13 +21,24 @@ export class SimulatorMask {
#targetCursorY = 0
constructor() {
super()
this.wrapper.id = 'page-agent-runtime_simulator-mask'
this.wrapper.className = styles.wrapper
this.wrapper.setAttribute('data-browser-use-ignore', 'true')
this.wrapper.setAttribute('data-page-agent-ignore', 'true')
this.wrapper.appendChild(this.motion.element)
this.motion.autoResize(this.wrapper)
try {
const motion = new Motion({
mode: isPageDark() ? 'dark' : 'light',
styles: { position: 'absolute', inset: '0' },
})
this.motion = motion
this.wrapper.appendChild(motion.element)
motion.autoResize(this.wrapper)
} catch (e) {
console.warn('[SimulatorMask] Motion overlay unavailable:', e)
}
// Capture all mouse, keyboard, and wheel events
this.wrapper.addEventListener('click', (e) => {
@@ -71,13 +78,34 @@ export class SimulatorMask {
this.#moveCursorToTarget()
window.addEventListener('PageAgent::MovePointerTo', (event: Event) => {
// global events
// @note Mask should be isolated from the rest of the code.
// Global events are easier to manage and cleanup.
const movePointerToListener = (event: Event) => {
const { x, y } = (event as CustomEvent).detail
this.setCursorPosition(x, y)
})
window.addEventListener('PageAgent::ClickPointer', (event: Event) => {
}
const clickPointerListener = () => {
this.triggerClickAnimation()
}
const enablePassThroughListener = () => {
this.wrapper.style.pointerEvents = 'none'
}
const disablePassThroughListener = () => {
this.wrapper.style.pointerEvents = 'auto'
}
window.addEventListener('PageAgent::MovePointerTo', movePointerToListener)
window.addEventListener('PageAgent::ClickPointer', clickPointerListener)
window.addEventListener('PageAgent::EnablePassThrough', enablePassThroughListener)
window.addEventListener('PageAgent::DisablePassThrough', disablePassThroughListener)
this.addEventListener('dispose', () => {
window.removeEventListener('PageAgent::MovePointerTo', movePointerToListener)
window.removeEventListener('PageAgent::ClickPointer', clickPointerListener)
window.removeEventListener('PageAgent::EnablePassThrough', enablePassThroughListener)
window.removeEventListener('PageAgent::DisablePassThrough', disablePassThroughListener)
})
}
@@ -103,6 +131,8 @@ export class SimulatorMask {
}
#moveCursorToTarget() {
if (this.#disposed) return
const newX = this.#currentCursorX + (this.#targetCursorX - this.#currentCursorX) * 0.2
const newY = this.#currentCursorY + (this.#targetCursorY - this.#currentCursorY) * 0.2
@@ -130,11 +160,15 @@ export class SimulatorMask {
}
setCursorPosition(x: number, y: number) {
if (this.#disposed) return
this.#targetCursorX = x
this.#targetCursorY = y
}
triggerClickAnimation() {
if (this.#disposed) return
this.#cursor.classList.remove(cursorStyles.clicking)
// Force reflow to restart animation
void this.#cursor.offsetHeight
@@ -142,13 +176,13 @@ export class SimulatorMask {
}
show() {
if (this.shown) return
if (this.shown || this.#disposed) return
this.shown = true
this.motion.start()
this.motion.fadeIn()
this.motion?.start()
this.motion?.fadeIn()
this.wrapper.style.display = 'block'
this.wrapper.classList.add(styles.visible)
// Initialize cursor position
this.#currentCursorX = window.innerWidth / 2
@@ -160,21 +194,24 @@ export class SimulatorMask {
}
hide() {
if (!this.shown) return
if (!this.shown || this.#disposed) return
this.shown = false
this.motion.fadeOut()
this.motion.pause()
this.motion?.fadeOut()
this.motion?.pause()
this.#cursor.classList.remove(cursorStyles.clicking)
setTimeout(() => {
this.wrapper.style.display = 'none'
this.wrapper.classList.remove(styles.visible)
}, 800) // Match the animation duration
}
dispose() {
this.motion.dispose()
this.#disposed = true
console.log('dispose SimulatorMask')
this.motion?.dispose()
this.wrapper.remove()
this.dispatchEvent(new Event('dispose'))
}
}
@@ -0,0 +1,80 @@
// ======= type guards =======
// @note instanceof fails for elements inside iframes
export function isHTMLElement(el: unknown): el is HTMLElement {
// @todo either specify to HTMLElement or allow Element here.
return !!el && (el as Node).nodeType === 1
}
export function isInputElement(el: Element): el is HTMLInputElement {
return el?.nodeType === 1 && el.tagName === 'INPUT'
}
export function isTextAreaElement(el: Element): el is HTMLTextAreaElement {
return el?.nodeType === 1 && el.tagName === 'TEXTAREA'
}
export function isSelectElement(el: Element): el is HTMLSelectElement {
return el?.nodeType === 1 && el.tagName === 'SELECT'
}
export function isAnchorElement(el: Element): el is HTMLAnchorElement {
return el?.nodeType === 1 && el.tagName === 'A'
}
// ======= iframe helpers =======
/** Iframe offset for translating element coordinates to top-frame viewport. */
export function getIframeOffset(element: HTMLElement): { x: number; y: number } {
const frame = element.ownerDocument.defaultView?.frameElement as HTMLElement | null
if (!frame) return { x: 0, y: 0 }
const rect = frame.getBoundingClientRect()
return { x: rect.left, y: rect.top }
}
/**
* Get native value setter from the element's own prototype (iframe-safe).
* @note for React
*/
export function getNativeValueSetter(element: HTMLInputElement | HTMLTextAreaElement) {
// eslint-disable-next-line @typescript-eslint/unbound-method
return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element) as object, 'value')!
.set as (v: string) => void
}
// ======= general utils =======
export async function waitFor(seconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
}
// ======= mask events =======
/**
* Move the visual pointer to a position within an element.
* @param x - x coordinate in the element's document viewport
* @param y - y coordinate in the element's document viewport
*/
export async function movePointerToElement(element: HTMLElement, x: number, y: number) {
const offset = getIframeOffset(element)
window.dispatchEvent(
new CustomEvent('PageAgent::MovePointerTo', {
detail: { x: x + offset.x, y: y + offset.y },
})
)
await waitFor(0.3)
}
export async function clickPointer() {
window.dispatchEvent(new CustomEvent('PageAgent::ClickPointer'))
}
export async function enablePassThrough() {
window.dispatchEvent(new CustomEvent('PageAgent::EnablePassThrough'))
}
export async function disablePassThrough() {
window.dispatchEvent(new CustomEvent('PageAgent::DisablePassThrough'))
}
@@ -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": {}
}
}
+1 -5
View File
@@ -1,11 +1,7 @@
{
"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", "**/*.js"],
"exclude": ["dist", "node_modules"]
+9 -4
View File
@@ -13,13 +13,18 @@ console.log(chalk.cyan(`📦 Building @page-agent/page-controller`))
export default defineConfig({
clearScreen: false,
plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
dts({
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/PageController.ts'),
+19 -9
View File
@@ -1,15 +1,25 @@
{
"name": "@page-agent/ui",
"version": "1.5.1",
"version": "1.8.0",
"type": "module",
"main": "./dist/lib/page-agent-ui.js",
"module": "./dist/lib/page-agent-ui.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-ui.js",
"default": "./dist/lib/page-agent-ui.js"
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"publishConfig": {
"main": "./dist/lib/page-agent-ui.js",
"module": "./dist/lib/page-agent-ui.js",
"types": "./dist/lib/index.d.ts",
"exports": {
".": {
"types": "./dist/lib/index.d.ts",
"import": "./dist/lib/page-agent-ui.js",
"default": "./dist/lib/page-agent-ui.js"
}
}
},
"files": [
@@ -32,7 +42,7 @@
"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{}})\""
"prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node ../../scripts/post-publish.js"
}
}
+1 -1
View File
@@ -291,7 +291,7 @@
transition: max-height 0.2s;
.expanded & {
max-height: 400px;
max-height: min(500px, calc(100vh - 200px - var(--height)));
}
.historyItem {

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