Compare commits

...

556 Commits

Author SHA1 Message Date
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
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
Simon dc8b38ccb9 chore(ext): bump extension version to 0.1.11 2026-03-05 20:48:00 +08:00
Simon e1c288aaeb docs(changelog): 1.5.1 2026-03-05 20:44:26 +08:00
Simon 7a97de2a37 feat(ext): expose more config 2026-03-05 20:34:55 +08:00
Simon 19b03b83ea chore(version): bump version to 1.5.1 2026-03-05 19:28:19 +08:00
Simon 598b144d06 feat: change maxSteps to 40 2026-03-05 19:26:56 +08:00
Simon 1cd8a8de60 Merge pull request #158 from alibaba/fix/zod-v3
feat: import zod properly
2026-03-05 19:23:47 +08:00
Simon 9beffca985 docs: update doc for Zod v4 support 2026-03-05 19:15:03 +08:00
Simon 53db2069ce feat: zod support v3/4 2026-03-05 19:13:18 +08:00
Simon 121104e13d feat: truncate llms.txt results 2026-03-05 16:59:08 +08:00
Simon 0dacbda9da chore: improve logging 2026-03-05 16:54:41 +08:00
Simon 8bc27414a4 chore(version): bump version to 1.4.4 2026-03-05 16:25:16 +08:00
Simon 76509d93f0 feat: replace data-browser-use-ignore with data-page-agent-ignore 2026-03-05 16:24:50 +08:00
Simon ff31b0c03e chore: adjust website structure 2026-03-05 16:21:58 +08:00
Simon 6b2ab73d65 docs: model recommendation 2026-03-05 16:10:03 +08:00
Simon 2754023a80 chore(version): bump version to 1.4.3 2026-03-04 21:18:51 +08:00
Simon fa5c65db53 docs: update custom-tool docs 2026-03-04 21:17:32 +08:00
Simon 53c2c19d69 docs: update advanced docs; rm best-practice 2026-03-04 21:13:10 +08:00
Simon e5437b445a fix(core): Core and PageAgent use different config types; improve code structure 2026-03-04 20:05:48 +08:00
Simon 09bdf9ddaf feat(core): experimental support for llms.txt (#157)
* feat(core): experimental support for llms.txt

* docs: experimentalLlmsTxt
2026-03-04 18:53:24 +08:00
Simon b58d2a09ef Merge pull request #152 from alibaba/dependabot/npm_and_yarn/development-dependencies-7e2b43fd30
chore(deps-dev): bump the development-dependencies group with 7 updates
2026-03-03 22:04:35 +08:00
Simon ce442742ba Merge pull request #153 from alibaba/dependabot/npm_and_yarn/vite-plugin-css-injected-by-js-4.0.1
chore(deps-dev): bump vite-plugin-css-injected-by-js from 3.5.2 to 4.0.1
2026-03-03 22:04:17 +08:00
Simon 632f22962e docs: add recommended startup parameters for Ollama 2026-03-03 21:59:02 +08:00
dependabot[bot] 72c3b12fb4 chore(deps-dev): bump the development-dependencies group with 7 updates
Bumps the development-dependencies group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.57.3` | `7.57.6` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.3.0` | `25.3.3` |
| [globals](https://github.com/sindresorhus/globals) | `17.3.0` | `17.4.0` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `16.2.7` | `16.3.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.575.0` | `0.576.0` |
| [motion](https://github.com/motiondivision/motion) | `12.34.3` | `12.34.4` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.9.0` | `16.10.0` |


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

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

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

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

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

Updates `motion` from 12.34.3 to 12.34.4
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.34.3...v12.34.4)

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

---
updated-dependencies:
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.57.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: globals
  dependency-version: 17.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lint-staged
  dependency-version: 16.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 0.576.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: motion
  dependency-version: 12.34.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.10.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-03 13:58:04 +00:00
Simon 4807d550a3 chore(version): bump version to 1.4.2 2026-03-03 21:54:41 +08:00
Simon f9722f0619 Merge branch 'main' of https://github.com/alibaba/page-agent 2026-03-03 21:53:35 +08:00
Simon e1557a5d2e Merge pull request #156 from hobostay/main
fix(actions): remove debug console.log statements
2026-03-03 21:53:05 +08:00
Simon 06280c2ba5 chore(core): add wait time between steps for the page to react 2026-03-03 21:51:10 +08:00
Simon 645a7ceb52 feat(website): loading time optimize 2026-03-03 21:38:29 +08:00
hobostay a47c72ce84 fix(actions): remove debug console.log statements
Remove [SCROLL DEBUG] console.log statements that were left over
from development in scrollVertically and scrollHorizontally
functions. These debug statements should not be in production code.
2026-03-03 21:36:59 +08:00
Simon dcc6cd1cf3 feat(website): reduce white screen time 2026-03-03 21:05:16 +08:00
Simon b4b9f6ce2d chore(version): bump version to 1.4.1 2026-03-03 20:56:24 +08:00
Simon 14e31e6ec4 Merge pull request #155 from alibaba/fix/panel-time
fix(panel): remove timestamp (not well defined)
2026-03-03 20:55:26 +08:00
Simon 69f2478fd7 fix(panel): remove timestamp (not well defined) 2026-03-03 20:52:55 +08:00
dependabot[bot] 5fec6846b0 chore(deps-dev): bump vite-plugin-css-injected-by-js from 3.5.2 to 4.0.1
Bumps [vite-plugin-css-injected-by-js](https://github.com/marco-prontera/vite-plugin-css-injected-by-js) from 3.5.2 to 4.0.1.
- [Release notes](https://github.com/marco-prontera/vite-plugin-css-injected-by-js/releases)
- [Commits](https://github.com/marco-prontera/vite-plugin-css-injected-by-js/compare/v3.5.2...v4.0.1)

---
updated-dependencies:
- dependency-name: vite-plugin-css-injected-by-js
  dependency-version: 4.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 21:00:45 +00:00
Simon fe792f1ceb chore(docs): remove description files 2026-03-02 17:36:10 +08:00
Simon d1d27a76de style: homepage misc 2026-02-27 22:24:55 +08:00
Simon 3e7e99420a docs: changelog 2026-02-27 22:05:55 +08:00
Simon 0ffd221a61 chore(version): bump version to 1.4.0 2026-02-27 21:59:00 +08:00
Simon ee208b9779 Merge pull request #148 from alibaba/feat/drop-hash-router
feat(website): drop hash route; upgrade styling
2026-02-27 21:33:30 +08:00
Simon 6ad20c8f45 feat(website): styling 2026-02-27 21:30:21 +08:00
Simon ff4caaec22 docs(website): update AGENTS.md 2026-02-27 20:45:36 +08:00
Simon 91ae86f56e style(website): fix height; clean up code 2026-02-27 20:42:24 +08:00
Simon c2daf13ead feat(website): simplify heading api 2026-02-27 20:36:00 +08:00
Simon ce2258fe16 feat(website): implement SPA route handling and remove 404.html 2026-02-27 20:07:46 +08:00
Simon 68d68182e2 feat(website): drop hash-based router 2026-02-27 19:46:44 +08:00
Simon 5f74f98a97 feat: update Qwen model list and add new model versions 2026-02-27 18:47:02 +08:00
Simon 07b4dc32f7 feat: add legacy endpoint migration and testing endpoint notice 2026-02-27 17:50:10 +08:00
Simon 9d5c8b2302 chore: update CI configuration 2026-02-27 17:23:41 +08:00
Simon 0c61ead38f chore: remove mention of release notes 2026-02-27 17:23:25 +08:00
Simon d92ad8f959 Merge pull request #145 from alibaba/feat/free-qwen-by-default
feat: update testing LLM endpoint and terms of use
2026-02-27 17:15:47 +08:00
Simon a51772e191 style(website): fix layout 2026-02-27 17:12:02 +08:00
Simon 89c83f454d feat: update terms and default llm endpoint 2026-02-27 17:09:54 +08:00
Simon 7f67463a24 Merge branch 'main' into feat/free-qwen-by-default 2026-02-27 15:51:19 +08:00
Simon 2311c1918a Merge pull request #146 from alibaba/dependabot/npm_and_yarn/development-dependencies-10a0d14ef2 2026-02-25 17:50:11 +08:00
Simon b45a2af9bf Merge pull request #147 from alibaba/feat/robust-llm-toolcall 2026-02-25 17:25:05 +08:00
Simon b4377ed152 chore: remove unused env.d.ts file 2026-02-25 17:19:09 +08:00
Simon b7be29d792 feat(core): enhance tollcall validation with better error reporting 2026-02-25 17:17:35 +08:00
Simon cbe223dac5 fix(core): auto-wrap primitive action input in normalizeResponse
Small models sometimes output single-field tool args as primitives
(e.g. {"click_element_by_index": 2} instead of {"index": 2}).
normalizeResponse now accepts an optional tools map and wraps
such values using the tool schema. Renamed invoke tools to macroTool.
2026-02-25 17:01:45 +08:00
Simon 22fe448d95 refactor: unify zod imports 2026-02-25 16:18:56 +08:00
Simon 49bdc70c78 chore: improve error logging 2026-02-25 16:13:39 +08:00
dependabot[bot] d6bd6a7170 chore(deps-dev): bump the development-dependencies group across 1 directory with 18 updates
Bumps the development-dependencies group with 17 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.4.1` | `20.4.2` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.4.1` | `20.4.2` |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.56.3` | `7.57.3` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.1.18` | `4.2.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.2.2` | `25.3.0` |
| [dotenv](https://github.com/motdotla/dotenv) | `17.2.4` | `17.3.1` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.12.2` | `2.13.0` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.0` | `0.5.2` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.12.2` | `2.13.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.55.0` | `8.56.1` |
| [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) | `0.1.36` | `0.1.37` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.13` | `19.2.14` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.563.0` | `0.575.0` |
| [motion](https://github.com/motiondivision/motion) | `12.34.0` | `12.34.3` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.8.0` | `16.9.0` |
| [tailwind-merge](https://github.com/dcastil/tailwind-merge) | `3.4.0` | `3.5.0` |
| [wxt](https://github.com/wxt-dev/wxt) | `0.20.14` | `0.20.18` |



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

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

Updates `@microsoft/api-extractor` from 7.56.3 to 7.57.3
- [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.3/apps/api-extractor)

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

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

Updates `dotenv` from 17.2.4 to 17.3.1
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v17.2.4...v17.3.1)

Updates `eslint-plugin-react-dom` from 2.12.2 to 2.13.0
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.13.0/packages/plugins/eslint-plugin-react-dom)

Updates `eslint-plugin-react-refresh` from 0.5.0 to 0.5.2
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.0...v0.5.2)

Updates `eslint-plugin-react-x` from 2.12.2 to 2.13.0
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.13.0/packages/plugins/eslint-plugin-react-x)

Updates `typescript-eslint` from 8.55.0 to 8.56.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.56.1/packages/typescript-eslint)

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

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

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

Updates `motion` from 12.34.0 to 12.34.3
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.34.0...v12.34.3)

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

Updates `tailwind-merge` from 3.4.0 to 3.5.0
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v3.4.0...v3.5.0)

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

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

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.57.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: dotenv
  dependency-version: 17.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.56.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@types/chrome"
  dependency-version: 0.1.37
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 0.575.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: motion
  dependency-version: 12.34.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: tailwind-merge
  dependency-version: 3.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: wxt
  dependency-version: 0.20.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 20:54:10 +00:00
Simon 2b99620de3 feat: add qwen3.5-plus for testing and make it default 2026-02-24 00:59:42 +08:00
Simon d913dd7bd0 chore(docs): move docs 2026-02-24 00:13:36 +08:00
Simon 8cb57befb3 feat(model): disable thinking for Qwen model patch 2026-02-23 17:30:35 +08:00
Simon b1be05309b feat(website): wording and style 2026-02-14 18:54:43 +08:00
Simon e5cf60df1b feat(website): loading speed; model list 2026-02-14 17:41:14 +08:00
Simon 58a3d5fbc6 feat(website): add "One More Thing" section; auto scroll 2026-02-14 17:30:00 +08:00
Simon 6abe75f199 docs(ext): clarify data handling and privacy in PRIVACY.md 2026-02-14 17:03:41 +08:00
Simon e27ebda960 chore(ext): docs and locales 2026-02-14 16:51:37 +08:00
Simon 9b603d90ba docs: update limitations page 2026-02-14 16:23:34 +08:00
Simon b89228693d feat(ext): add lang setting 2026-02-14 16:10:46 +08:00
Simon 66cc9e0a3c style: pretty up; fix extension shadcn 2026-02-14 15:12:45 +08:00
Simon 53144a8021 chore(ext): rm zh doc 2026-02-14 15:12:03 +08:00
Simon 43b7c1b136 style(ext): make over 2026-02-13 19:09:46 +08:00
Simon 3e7851849f chore(version): bump version to 1.3.0 2026-02-13 18:50:32 +08:00
Simon dffcb53db9 feat!: Refine lifecycle hooks; fix abortSignal
- add `stop` method. agent can be reused after stopped
- agent can not be reused after disposed
- extension DO NOT exposes `dispose` anymore. only `stop`.
- update panel for new `stop` method
- fix MultiPageAgent dispose event
- better handling abortSignal
2026-02-13 17:57:12 +08:00
Simon 4dc332a32c docs: update changelog 2026-02-12 17:23:35 +08:00
Simon f19b3cc2cc docs: update extension related docs 2026-02-12 17:19:14 +08:00
Simon 11d66f42c4 chore(ext): version 0.1.5 2026-02-11 20:12:57 +08:00
Simon 20e947a1c1 chore(version): bump version to 1.2.0 2026-02-11 20:09:23 +08:00
Simon 7c87c90258 fix(ext): fix multi-thread logic; extensive logging and error handling 2026-02-11 19:51:19 +08:00
Simon fcb9ec4e57 chore: clean up 2026-02-11 19:27:14 +08:00
Simon 1fcf931685 feat(core): add a observe phase in a step; improve abortSignal 2026-02-10 22:20:42 +08:00
Simon c71ffcec1c refactor(ext): tidy up background code 2026-02-10 21:59:58 +08:00
Simon 430be466fc feat(ext): add chat history 2026-02-10 18:43:54 +08:00
Simon 3f284cf2e6 chore(controller): remove link hint 2026-02-10 17:19:02 +08:00
Simon 6f0655478a fix(core): remove trimLines to fix indentation lost 2026-02-10 16:30:03 +08:00
Simon 7d5449967b feat(core): improve observation logic; mark states as private 2026-02-10 16:28:25 +08:00
Simon fd47888d1b chore(core): tidy up PageAgentCore members 2026-02-10 15:45:04 +08:00
Simon 10f470ce7d chore: improve package.json 2026-02-10 15:28:50 +08:00
Simon cb29db1cc4 Merge pull request #138 from alibaba/dependabot/npm_and_yarn/production-dependencies-58bd30f460
chore(deps): bump the production-dependencies group with 2 updates
2026-02-10 14:21:11 +08:00
Simon 8c08f639dd Merge pull request #139 from alibaba/dependabot/npm_and_yarn/development-dependencies-dee6027aab
chore(deps-dev): bump the development-dependencies group with 8 updates
2026-02-10 14:18:18 +08:00
dependabot[bot] 488f080e68 chore(deps-dev): bump the development-dependencies group with 8 updates
Bumps the development-dependencies group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.56.0` | `7.56.3` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.2.0` | `25.2.2` |
| [dotenv](https://github.com/motdotla/dotenv) | `17.2.3` | `17.2.4` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.9.3` | `2.12.2` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.9.3` | `2.12.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.54.0` | `8.55.0` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.10` | `19.2.13` |
| [wxt](https://github.com/wxt-dev/wxt) | `0.20.13` | `0.20.14` |


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

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

Updates `dotenv` from 17.2.3 to 17.2.4
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/commits)

Updates `eslint-plugin-react-dom` from 2.9.3 to 2.12.2
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.12.2/packages/plugins/eslint-plugin-react-dom)

Updates `eslint-plugin-react-x` from 2.9.3 to 2.12.2
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.12.2/packages/plugins/eslint-plugin-react-x)

Updates `typescript-eslint` from 8.54.0 to 8.55.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.55.0/packages/typescript-eslint)

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

Updates `wxt` from 0.20.13 to 0.20.14
- [Release notes](https://github.com/wxt-dev/wxt/releases)
- [Commits](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.13...wxt-v0.20.14)

---
updated-dependencies:
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.56.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: dotenv
  dependency-version: 17.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.12.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.12.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.55.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: wxt
  dependency-version: 0.20.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 22:25:35 +00:00
dependabot[bot] 6e07af422b chore(deps): bump the production-dependencies group with 2 updates
Bumps the production-dependencies group with 2 updates: [motion](https://github.com/motiondivision/motion) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `motion` from 12.30.0 to 12.34.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.30.0...v12.34.0)

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

---
updated-dependencies:
- dependency-name: motion
  dependency-version: 12.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: simple-icons
  dependency-version: 16.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 22:23:28 +00:00
Simon e9d721f6b9 chore(ext): bump extension version to 0.1.4 2026-02-09 21:12:11 +08:00
Simon 840162df8c feat: change observation timing 2026-02-09 21:05:29 +08:00
Simon 1517bc4868 feat: prune prompts 2026-02-09 21:04:39 +08:00
Simon 67f6a56a87 chore(version): bump version to 1.1.2 2026-02-09 17:50:34 +08:00
Simon 6f0ff1fd33 refactor: zod tree-shaking; better error handling in agent steps 2026-02-09 17:49:10 +08:00
Simon 6fdb8d03f2 docs: trouble shooting 2026-02-09 16:32:58 +08:00
Simon d00a8dcc21 feat: improve prompt; update model list 2026-02-09 15:50:20 +08:00
Simon 851e9ef158 docs: model list 2026-02-06 18:49:06 +08:00
Simon 8ea8ce9acd fix(llms): gpt-5-mini temperature 2026-02-06 17:19:06 +08:00
Simon 9bd4a47d35 feat(ext): add includeInitialTab option; change main world API 2026-02-04 19:22:06 +08:00
Simon 71ca554108 feat(ext): use PAGE_AGENT_EXT namespace; add viber instructions 2026-02-03 19:09:37 +08:00
Simon 648a0c1bda feat(ext): expose ext meta data to main world 2026-02-03 16:39:24 +08:00
Simon 1a0422f99d Merge pull request #135 from alibaba/dependabot/npm_and_yarn/development-dependencies-ad640cb911
chore(deps-dev): bump eslint-plugin-react-refresh from 0.4.26 to 0.5.0 in the development-dependencies group across 1 directory
2026-02-03 14:20:04 +08:00
dependabot[bot] 2f72c677a0 chore(deps-dev): bump eslint-plugin-react-refresh
Bumps the development-dependencies group with 1 update in the / directory: [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh).


Updates `eslint-plugin-react-refresh` from 0.4.26 to 0.5.0
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.4.26...v0.5.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 06:00:00 +00:00
Simon 18019d390a Merge pull request #132 from alibaba/dependabot/npm_and_yarn/production-dependencies-8604a495b2
chore(deps): bump the production-dependencies group with 2 updates
2026-02-03 13:48:12 +08:00
Simon bb3605b3df chore: clean up dep 2026-02-03 13:39:47 +08:00
Simon 2b3bb8ab9e chore(ext): rm React.FormEvent 2026-02-03 13:18:03 +08:00
dependabot[bot] 7a09bc559d chore(deps): bump the production-dependencies group with 2 updates
Bumps the production-dependencies group with 2 updates: [motion](https://github.com/motiondivision/motion) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `motion` from 12.29.2 to 12.30.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.29.2...v12.30.0)

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

---
updated-dependencies:
- dependency-name: motion
  dependency-version: 12.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: simple-icons
  dependency-version: 16.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 01:15:58 +00:00
Simon a834d37f28 fix(ext): inject versions 2026-02-02 21:11:23 +08:00
Simon 0c3be817c9 style(ext): attribution; dark mode 2026-02-02 21:07:31 +08:00
Simon b8999fdb7c style(ext): improve config panel 2026-02-02 19:51:48 +08:00
Simon 9ba95e96b6 feat(ext): detect user lang 2026-02-02 19:29:30 +08:00
Simon 4459026297 feat: set default lang to english; fix ext lang setting 2026-02-02 19:17:15 +08:00
Simon 98d3016395 style(ext): remove raw section from ResultCard 2026-02-02 19:16:07 +08:00
Simon 15ea488719 fix(ext): save config to storage 2026-02-02 18:48:16 +08:00
Simon 5b2a11c0b2 chore(version): bump version to 1.1.0 2026-02-02 17:28:40 +08:00
Simon c5c6340eb6 chore: ext version 2026-02-02 17:27:58 +08:00
Simon 74aea50875 feat: rename include_attributes to includeAttributes 2026-02-02 17:24:08 +08:00
Simon f86bb03745 Merge branch 'main' of https://github.com/alibaba/page-agent 2026-02-02 16:53:45 +08:00
Simon f6b4447ef7 docs: tips about Ollama 2026-02-02 16:51:51 +08:00
Simon d8ec4cf1ab feat: better action key in prompt example 2026-02-02 13:39:04 +08:00
Simon da440f63c3 chore(version): bump version to 1.0.1 2026-01-30 16:59:11 +08:00
Simon 622fbff790 docs: simplify layout; rm useless CDN-setup 2026-01-30 16:54:31 +08:00
Simon 68c944c3a4 docs: remove i18n dep 2026-01-30 16:47:23 +08:00
Simon 6cd3e3726c docs: typography 2026-01-30 16:34:27 +08:00
Simon d11a2593b3 docs: pretty chrome extension entry 2026-01-30 15:19:54 +08:00
Simon 5e936c35d7 docs: simplify i18n 2026-01-30 15:13:31 +08:00
Simon 97cdb1e1cd docs: Add Custom UI doc 2026-01-30 14:58:33 +08:00
Simon 7470c1987b chore(ext): update extension version 2026-01-29 22:34:15 +08:00
Simon b767f10a85 feat(ext): extending execute api 2026-01-29 22:26:31 +08:00
Simon 8a03391c95 fix(core): change emitActivity to #emitActivity 2026-01-29 21:15:24 +08:00
Simon 5cfaa292d3 feat(ext): add agent heart beat check 2026-01-29 19:21:57 +08:00
Simon 4e87940127 fix(ext): update message handlers
> If multiple listeners are registered for onMessage,
> only the first listener to respond, reject, or throw
> an error will affect the sender; all other listeners
> will run, but their results will be ignored.
2026-01-29 18:37:42 +08:00
Simon 5d77990187 feat(docs): add Chrome extension documentation 2026-01-28 23:27:11 +08:00
Simon b0e07d5180 feat(ext): monitor to tab change 2026-01-28 23:03:03 +08:00
Simon cf4eeb9481 fix(docs): update extension reference in README 2026-01-28 20:48:22 +08:00
Simon d67ca95925 chore(ext): build extension 0.1.0-beta.1 2026-01-28 20:35:42 +08:00
Simon f67ec5a2c2 feat(ext): require llmConfig when calling from main world 2026-01-28 20:28:21 +08:00
Simon 0f3ae38cb3 fix(ConfigPanel): correct auth token storage key 2026-01-28 20:19:16 +08:00
Simon d6323f1488 feat(ext): improve token validation and error handling 2026-01-28 19:59:55 +08:00
Simon ef2d115742 feat(ext): expose extension to main-world and auth with token 2026-01-28 19:47:15 +08:00
Simon ff75aebc20 feat(core): remove unused .disposed 2026-01-28 19:17:23 +08:00
Simon 04d51c566d feat: delay mask import 2026-01-28 17:16:39 +08:00
Simon 142231a16c fix(PageController): use local svg for cursor mask to fix CSP errors 2026-01-28 17:04:54 +08:00
Simon d712ea7041 chore(scripts): skip version update for extension package 2026-01-28 15:43:54 +08:00
Simon 9d37a452e4 feat(ext): improve tabs sum 2026-01-28 15:38:42 +08:00
Simon 94ae80ff6a chore: move title line from Agent to PageController 2026-01-28 15:38:05 +08:00
Simon d7b17facb6 feat(ext): UI for debugging 2026-01-28 15:26:11 +08:00
Simon 501411b254 feat: improve date format and error message for llm 2026-01-28 15:24:08 +08:00
Simon f6394a04b1 feat(core): add rawRequest in history for debug 2026-01-28 14:30:04 +08:00
Simon c80de93d3b fix(ext): sw calling robust 2026-01-28 14:13:28 +08:00
Simon 0cad8a1159 feat(ext): use full task for tab group name 2026-01-28 14:12:30 +08:00
Simon 77a771f0b6 chore(ext): react misc 2026-01-28 14:11:44 +08:00
Simon dd593f77e9 chore(ext): refine MultiPageAgent api and clean up 2026-01-28 13:36:46 +08:00
Simon 55ffbb8f08 refactor(ext): improve folder structure 2026-01-28 13:15:34 +08:00
Simon e425a3ed9f fix(ext): wait for new tab to load 2026-01-27 21:49:06 +08:00
Simon 87c3fe36d2 feat(ext): new prompt 2026-01-27 21:45:51 +08:00
Simon d64e144ab4 chore(ext): clean up package.json; rebuild lock 2026-01-27 18:47:32 +08:00
Simon 5c3e3d1066 Merge pull request #127 from alibaba/dependabot/npm_and_yarn/production-dependencies-e7b19cf5cb
chore(deps): bump the production-dependencies group across 1 directory with 2 updates
2026-01-27 18:39:21 +08:00
Simon a243f158d7 Merge pull request #128 from alibaba/dependabot/npm_and_yarn/development-dependencies-7b2f699698
chore(deps-dev): bump the development-dependencies group across 1 directory with 3 updates
2026-01-27 18:39:02 +08:00
Simon 96a51d3674 fix(ext): prevent submit on composing state 2026-01-27 17:47:56 +08:00
dependabot[bot] 06f2f8be6c chore(deps-dev): bump the development-dependencies group across 1 directory with 3 updates
Bumps the development-dependencies group with 3 updates in the / directory: [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint), [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom).


Updates `typescript-eslint` from 8.53.1 to 8.54.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.54.0/packages/typescript-eslint)

Updates `react` from 19.2.3 to 19.2.4
- [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.4/packages/react)

Updates `react-dom` from 19.2.3 to 19.2.4
- [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.4/packages/react-dom)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.54.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: react
  dependency-version: 19.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: react-dom
  dependency-version: 19.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 09:44:01 +00:00
dependabot[bot] bb68d218cc chore(deps): bump the production-dependencies group across 1 directory with 2 updates
Bumps the production-dependencies group with 2 updates in the / directory: [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) and [motion](https://github.com/motiondivision/motion).


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

Updates `motion` from 12.29.0 to 12.29.2
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.29.0...v12.29.2)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 0.563.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: motion
  dependency-version: 12.29.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 09:42:47 +00:00
Simon 6ff2a032d6 Merge pull request #122 from alibaba/feat/ext
[WIP] feat: the browser extension
2026-01-27 17:41:10 +08:00
Simon fdc3cf4e6d feat(ext): handcraft the whole ext from scratch
AI coding doesn't work for MV3 extensions.
Threading was an unfixable mess.
Removed everything and rebuilt by hand.
2026-01-27 17:21:32 +08:00
Simon 8efa8e18c1 refactor(ext): rewrite ext. totally-broken -> still-broken; THIS IS NOT WORKING 2026-01-26 21:03:51 +08:00
Simon cdecf3cc3d refactor(ext): mv files 2026-01-26 19:33:57 +08:00
Simon 89d6835a99 chore(ext): highlight cleanup with mask 2026-01-26 19:25:00 +08:00
Simon 3bf616dde1 fix: remove ensureResponseListener; remove isPageAgentMessage 2026-01-26 19:17:52 +08:00
Simon f18b756390 chore: add isPageAgentMessage to message 2026-01-26 18:55:54 +08:00
Simon 969dd31db5 chore: update AGENTS.md for error handling 2026-01-26 16:27:20 +08:00
Simon c4bcad04bd chore(ext): debug logging 2026-01-26 16:16:04 +08:00
Simon 4f170b674e fix: rebuild lock 2026-01-26 15:32:51 +08:00
Simon 5826ff47cd Merge branch 'main' into feat/ext 2026-01-26 15:26:08 +08:00
Simon 0bac51b67b chore: typo 2026-01-26 15:25:56 +08:00
Simon 8a7b9b39ce Merge branch 'main' into feat/ext 2026-01-26 15:22:12 +08:00
Simon 3cfc830d14 feat: ignore alias to AGENTS.md 2026-01-26 15:21:48 +08:00
Simon 6141c0f669 feat(ext): update mask controller 2026-01-24 19:37:46 +08:00
Simon fa5ab9d567 feat: multi tabs control 2026-01-24 19:29:27 +08:00
Simon 2aa9c3b978 Merge branch 'main' into feat/ext 2026-01-23 00:56:24 +08:00
Simon f4eec86e10 chore: clean up 2026-01-23 00:55:58 +08:00
Simon 60b401d86c Merge branch 'main' into feat/ext 2026-01-22 20:22:01 +08:00
Simon dad837e173 fix: don't retry or show AbortError; remove reason from dispose; don't clean history in dispose 2026-01-22 20:19:51 +08:00
Simon efe1f33144 feat: add custom system prompt configuration 2026-01-22 17:21:10 +08:00
Simon 7a560341aa chore(ext): add privacy policy document 2026-01-22 16:08:21 +08:00
Simon 2024a417c6 chore: sync version 2026-01-22 15:45:17 +08:00
Simon 9738545b2c Merge branch 'main' into feat/ext 2026-01-22 15:43:38 +08:00
Simon 299a5b2075 chore(version): bump version to 1.0.0 2026-01-22 15:38:21 +08:00
Simon 1f3e51eeb4 chore: sync version 2026-01-22 15:31:52 +08:00
Simon e96dac28fb Merge branch 'main' into feat/ext 2026-01-22 15:31:07 +08:00
Simon 785a2d194e chore(version): bump version to 1.0.0-beta.5 2026-01-22 15:17:19 +08:00
Simon 391e70dca4 feat: update historical event types 2026-01-22 15:16:15 +08:00
Simon 8c43fe1409 feat: update historical event types 2026-01-22 15:15:18 +08:00
Simon d861f7b7f2 Merge branch 'main' into feat/ext 2026-01-22 15:02:53 +08:00
Simon 3ffcd6482d refactor: rename AgentStep to AgentStepEventErrorEvent to AgentErrorEvent 2026-01-22 14:59:35 +08:00
Simon 53d3aaa67f refactor: update event types 2026-01-22 14:57:21 +08:00
Simon 5c4777fe7a chore: remove unnecessary permissions from extension config 2026-01-22 14:46:14 +08:00
Simon 4c7de54365 Merge branch 'main' into feat/ext 2026-01-22 14:15:15 +08:00
Simon 0cd04db40f fix: rebuild lock 2026-01-22 14:12:37 +08:00
Simon 2d1b2bbb85 Merge branch 'main' into feat/ext 2026-01-21 20:15:49 +08:00
Simon 23f1818503 docs: update README for clarity and integration details 2026-01-21 20:15:33 +08:00
Simon c1ae196bdc feat: update extension description and homepage URL 2026-01-21 19:54:11 +08:00
Simon c06742b8ed chore(ext): sync version to 1.0.0-beta.4 2026-01-21 19:21:09 +08:00
Simon 0df512add7 Merge branch 'main' into feat/ext 2026-01-21 19:20:37 +08:00
Simon 5fdb49fecb fix(sync-version): update version check to use pkgJson.version 2026-01-21 19:20:23 +08:00
Simon af133470e1 feat: add error handling for content scripts 2026-01-21 19:17:54 +08:00
Simon 4153db866f Merge branch 'main' into feat/ext 2026-01-21 19:06:12 +08:00
Simon fe1893a382 fix: rebuild lock 2026-01-21 19:04:26 +08:00
Simon b2260e9484 Merge branch 'main' into feat/ext 2026-01-21 19:01:46 +08:00
Simon 8c1c1ed9fe fix: correct prettier extension recommendation 2026-01-21 19:01:27 +08:00
Simon fb2ffa1de3 Merge branch 'main' into feat/ext 2026-01-21 18:55:50 +08:00
Simon 6cbb4e99c4 chore(version): bump version to 1.0.0-beta.4 2026-01-21 18:55:07 +08:00
Simon 34ce56b446 feat(PageController): make sure page is indexed before calling actions on elements 2026-01-21 18:54:17 +08:00
Simon 3fea74faa9 feat(ext): handling page reload/redirect/close 2026-01-21 18:46:50 +08:00
Simon 570c79623b feat(ext): update API Key input to use password type for enhanced security 2026-01-21 17:37:27 +08:00
Simon f0cb69ec7f Merge branch 'main' into feat/ext 2026-01-21 17:26:56 +08:00
Simon 23ec4602c2 feat: export types from './types' in PageAgentCore 2026-01-21 17:26:31 +08:00
Simon 7dcfad42f5 feat(ext): refactor event card; the result also counts as a step 2026-01-21 17:25:44 +08:00
Simon 189e9f3d39 feat(ext): display step index in event card 2026-01-21 17:14:51 +08:00
Simon 908ffae77c Merge branch 'main' into feat/ext 2026-01-21 17:00:45 +08:00
Simon 536e5076da chore(version): bump version to 1.0.0-beta.3 2026-01-21 17:00:08 +08:00
Simon 81807e17ca feat: add stepIndex to history events 2026-01-21 16:59:22 +08:00
Simon 9531fcff5a feat(ext): enhance error handling and improve UI structure 2026-01-21 16:50:55 +08:00
Simon 52454305fb chore(vscode): update settings 2026-01-21 14:32:08 +08:00
Simon a18ebfd6c1 chore(ext): wxt configuration 2026-01-21 14:04:26 +08:00
Simon 5402cdecc2 style: update text sizes and spacing in components 2026-01-21 02:22:12 +08:00
Simon 969c00b2d1 feat(ext): auto fold reflections 2026-01-21 01:54:44 +08:00
Simon 266eec1474 Merge branch 'main' into feat/ext 2026-01-21 01:30:42 +08:00
Simon 6944bedf85 chore: correct formatting in markdown 2026-01-21 01:30:27 +08:00
Simon 5210a6edff Merge branch 'main' into feat/ext 2026-01-21 01:26:56 +08:00
Simon 897b919044 chore(version): bump version to 1.0.0-beta.2 2026-01-21 01:23:42 +08:00
Simon 3a3bb0e58c fix: use spaces for JSON formatting in version sync script 2026-01-21 01:23:07 +08:00
Simon bc7ccf47ff fix: handle version in sync script 2026-01-21 01:15:58 +08:00
Simon c781eba986 feat: fixed version in CDN URL 2026-01-21 01:08:19 +08:00
Simon efe131660a fix: demo not showing panel; docs updated 2026-01-21 00:44:09 +08:00
Simon 4200c78327 feat(ext): style; code split 2026-01-20 21:13:44 +08:00
Simon f500445965 feat: show rawResponse in cards 2026-01-20 20:38:02 +08:00
Simon a47dfef659 Merge branch 'main' into feat/ext 2026-01-20 20:17:00 +08:00
Simon 22516dec74 feat: unify InvokeError; add rawResponse to historical ErrorEvent 2026-01-20 20:16:34 +08:00
Simon 32a9973e49 Merge branch 'main' into feat/ext 2026-01-20 19:57:05 +08:00
Simon 1f8953c450 feat: add llm rawResponse to history 2026-01-20 19:56:40 +08:00
Simon d6a57f3e20 chore: merge .gitignore from feat/ext 2026-01-20 19:53:52 +08:00
Simon ba7e79251f docs(ext): update PageController lifecycle and tab binding 2026-01-20 19:17:39 +08:00
Simon b4d17aa907 fix: no mask for new task after stop prev task 2026-01-20 19:08:52 +08:00
Simon 1a0c533cb4 fix: lost control of tab when window inactive 2026-01-20 19:03:15 +08:00
Simon 37332cbb9d feat(ext): icon; set chrome profile 2026-01-20 18:56:39 +08:00
Simon be85d63ef8 chore: update instructions 2026-01-20 17:41:45 +08:00
Simon db3b24a5ea feat(ext): draft extension structure (single-page mode) 2026-01-20 17:36:33 +08:00
Simon e0027f99c4 Merge pull request #121 from alibaba/dependabot/npm_and_yarn/development-dependencies-0b42ca76aa
chore(deps-dev): bump the development-dependencies group with 6 updates
2026-01-20 14:14:40 +08:00
Simon b234868bc1 Merge pull request #120 from alibaba/dependabot/npm_and_yarn/production-dependencies-f96e18e569
chore(deps): bump the production-dependencies group with 2 updates
2026-01-20 14:13:47 +08:00
dependabot[bot] 10ae34a677 chore(deps-dev): bump the development-dependencies group with 6 updates
Bumps the development-dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.7` | `25.0.9` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.5.7` | `2.7.2` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.5.7` | `2.7.2` |
| [prettier](https://github.com/prettier/prettier) | `3.7.4` | `3.8.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.53.0` | `8.53.1` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.5.2` | `16.5.3` |


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

Updates `eslint-plugin-react-dom` from 2.5.7 to 2.7.2
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.7.2/packages/plugins/eslint-plugin-react-dom)

Updates `eslint-plugin-react-x` from 2.5.7 to 2.7.2
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.7.2/packages/plugins/eslint-plugin-react-x)

Updates `prettier` from 3.7.4 to 3.8.0
- [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.7.4...3.8.0)

Updates `typescript-eslint` from 8.53.0 to 8.53.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.53.1/packages/typescript-eslint)

Updates `react-i18next` from 16.5.2 to 16.5.3
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.5.2...v16.5.3)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.7.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.7.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: prettier
  dependency-version: 3.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.53.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: react-i18next
  dependency-version: 16.5.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 06:11:50 +00:00
dependabot[bot] 2fb01b7315 chore(deps): bump the production-dependencies group with 2 updates
Bumps the production-dependencies group with 2 updates: [motion](https://github.com/motiondivision/motion) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `motion` from 12.26.1 to 12.27.1
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.26.1...v12.27.1)

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

---
updated-dependencies:
- dependency-name: motion
  dependency-version: 12.27.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: simple-icons
  dependency-version: 16.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 06:11:18 +00:00
Simon 5c80f2b002 chore: prettier json indentation 2026-01-20 14:08:42 +08:00
Simon 28c128645e chore(version): bump version to 1.0.0-beta.1 2026-01-19 20:51:37 +08:00
Simon 250cb47e44 fix: wait time second not ms 2026-01-19 20:50:50 +08:00
Simon f83a4174cc chore(version): bump version to 1.0.0-beta.0 2026-01-19 20:39:57 +08:00
Simon 334512210d feat: improve lifecycle hooks API; wait a little after url change 2026-01-19 20:39:03 +08:00
Simon 3df71788ff docs: update package descriptions 2026-01-19 20:14:40 +08:00
Simon 60450ea7be chore: export tool from PageAgent package 2026-01-19 20:12:46 +08:00
Simon 80c93bd668 feat: expose maxSteps config 2026-01-19 20:11:53 +08:00
Simon 6d53f4cb44 chore(version): bump version to 0.3.0 2026-01-19 19:47:21 +08:00
Simon b35489d6a5 chore(version): bump version to 0.3.0-beta.2 2026-01-19 19:41:57 +08:00
Simon d78cbb6b6e chore(release): enhance npm publish for prerelease tags 2026-01-19 19:41:39 +08:00
Simon e9c5cece4f docs: update CDN URLs in README files 2026-01-19 19:39:13 +08:00
Simon 4b69a7ee00 chore(version): bump version to 0.3.0-beta.1 2026-01-19 19:35:27 +08:00
Simon 25e4d07fc5 chore(demo): clean up 2026-01-19 19:33:15 +08:00
Simon 6a381dede1 chore(version): bump version to 0.3.0-beta 2026-01-19 19:31:45 +08:00
Simon b3bfbb96bb docs: update cdn urls 2026-01-19 19:31:07 +08:00
Simon 3d10953324 feat: remove iife-full build 2026-01-19 19:18:19 +08:00
Simon 8dd4c5d78e feat: remove @page-agent/cdn 2026-01-19 19:10:00 +08:00
Simon 8bde218f28 Merge pull request #119 from alibaba/feat/decouple-dom
feat!: decouple `DOM` from `Agent`
2026-01-19 17:22:57 +08:00
Simon 0643cc4ff9 feat: enhance headless mode documentation and usage 2026-01-19 17:14:36 +08:00
Simon 4c5dd23fcd feat: decouple PageController from PageAgent 2026-01-19 17:14:09 +08:00
Simon 3ce4d8e3fe docs: update instructions. add advanced section 2026-01-19 16:47:48 +08:00
Simon b217e6a2ca feat!: PageAgent with UI by default 2026-01-19 16:07:17 +08:00
Simon c9f049a733 refactor: rename page-agent to page-agent-core 2026-01-19 16:06:07 +08:00
Simon 09c3084629 chore: typo 2026-01-19 15:21:43 +08:00
Simon 38c8020e4a chore: typo 2026-01-19 15:15:23 +08:00
Simon 5057f7472b feat: pass language to formatTime for localization 2026-01-19 14:25:25 +08:00
Simon 6820eec676 feat(PageController): make showMask and hideMask async 2026-01-19 12:54:33 +08:00
Simon ef2777b521 feat: custom PageController; remove beforeunload; node-safe PageController 2026-01-18 00:07:23 +08:00
Simon 8bba3a63f5 chore: ui folder structure 2026-01-17 23:35:07 +08:00
Simon 722eb10286 feat: change onAfterStep params 2026-01-17 23:31:02 +08:00
Simon 15ffc27059 chore: agent event docs 2026-01-17 23:30:37 +08:00
Simon 5f162b1a1c chore: rm event bus; separate agent types 2026-01-17 23:30:12 +08:00
Simon 083c002ce0 fix: type 2026-01-17 23:20:09 +08:00
Simon 1ede1d9911 feat!: redo Panel; decouple Panel from Agent
BREAKING CHANGES: Agent and Panel API Changes
2026-01-17 23:14:26 +08:00
Simon 8abe3afed9 fix: type 2026-01-17 18:07:59 +08:00
Simon af36f43104 chore(version): bump version to 0.2.5 2026-01-17 17:47:50 +08:00
Simon b5f260207c chore: wording 2026-01-17 17:47:04 +08:00
Simon 9075204c29 Merge pull request #118 from alibaba/feat/make-panel-optional
feat: make all ui optional
2026-01-17 17:43:40 +08:00
Simon ce3a80bc53 docs: typo 2026-01-17 17:30:16 +08:00
Simon 64c36f67bb docs: update config; instructions for programmatic usage 2026-01-17 17:28:28 +08:00
Simon 78d4919289 feat: make panel optional 2026-01-17 17:24:17 +08:00
Simon 1af2607cca feat: remove ask_user tool from sys_prompt 2026-01-17 17:18:31 +08:00
Simon ab713f3d3a chore(version): bump version to 0.2.4 2026-01-17 16:22:45 +08:00
Simon 05cc4f36c8 chore: add .prettierignore for system_prompt.md 2026-01-17 16:22:28 +08:00
Simon 54e9bdad7d chore: prettier json and markdowns 2026-01-17 16:15:36 +08:00
Simon 0fc88c0080 chore(version): bump version to 0.2.3 2026-01-17 16:03:11 +08:00
Simon 02f97ef2fa chore: banner 2026-01-17 15:57:22 +08:00
Simon 0853ffbbc6 chore: banner 2026-01-17 15:48:30 +08:00
Simon 60ac6da09f chore: new demo video 2026-01-17 15:17:14 +08:00
Simon 11a9fb1181 chore: new demo video 2026-01-17 15:15:48 +08:00
Simon 3762d0f1e0 chore: refine docs 2026-01-17 14:59:09 +08:00
Simon ebbe92f7ca fix(website): update links to use relative paths 2026-01-17 02:13:19 +08:00
Simon 9f239b2de3 chore: comment out unused config 2026-01-17 02:05:30 +08:00
Simon 2410c27b55 chore(website): update config docs 2026-01-17 02:05:09 +08:00
Simon 6d6c82648c chore: cleanup 2026-01-17 01:46:35 +08:00
Simon f41119c1ad fix(website): fix router 2026-01-17 01:36:15 +08:00
Simon 9e6e1b996c chore(website): simplify chunks 2026-01-17 01:27:19 +08:00
Simon 1ab23e181f chore: clean up 2026-01-17 01:23:23 +08:00
Simon a1b485691f chore(version): bump version to 0.2.2 2026-01-17 00:59:55 +08:00
Simon 694e05b800 chore: npm audit fix 2026-01-17 00:54:20 +08:00
Simon b761795c1a feat!: local iife testing
BREAKING CHANGES: remove umd build from npm package and cdn
2026-01-16 19:04:26 +08:00
Simon 8cc54983aa chore(version): bump version to 0.2.1 2026-01-16 16:16:18 +08:00
Simon 59a599285e Merge pull request #115 from alibaba/fix/phantom-panel
fix(panel): hide panel by default
2026-01-16 16:15:43 +08:00
Simon 2f8c2b395c fix(panel): hide panel by default 2026-01-16 16:06:57 +08:00
Simon 28f836674c feat: edge feedback for scroll actions 2026-01-16 15:53:47 +08:00
Simon 6f01632231 fix(website): build error 2026-01-16 15:09:30 +08:00
Simon 41123ac561 chore(website): optimize bundle size 2026-01-16 15:05:09 +08:00
Simon c8bf0dd20f chore: get rid of build warnings 2026-01-16 14:50:56 +08:00
Simon ec2b744bab Merge pull request #114 from alibaba/feat/user-takeover
feat: remove `pause`
2026-01-15 21:51:01 +08:00
Simon eab9cf64e8 feat: includes history events in panel 2026-01-15 21:48:14 +08:00
Simon 42130d6f1d feat: remove pause 2026-01-15 21:37:03 +08:00
Simon c0ce6e7d14 chore(version): bump version to 0.2.0 2026-01-15 20:47:39 +08:00
Simon 8f5f98f889 feat(agent)!: rename brain to reflection; fix undefined lines in panel 2026-01-15 20:45:40 +08:00
Simon ee4719cdcf feat: observation 2026-01-15 20:28:39 +08:00
Simon b4c82b7833 refactor: mv getBrowserState to Controller; simplify Agent 2026-01-15 20:17:50 +08:00
Simon aefc3cfb89 Merge pull request #111 from alibaba/feat/extend-agent-history-for-observation
feat: extend history type
2026-01-15 19:57:41 +08:00
Simon 7f2b8c2766 feat: extend history type 2026-01-15 19:18:25 +08:00
Simon 62e24a3050 chore(version): bump version to 0.1.0 2026-01-14 18:50:06 +08:00
Simon 4f6249a252 feat!: require user to provide LLM api. do not fallback to demo LLM
BREAKING CHANGE: LLM API will be required for agent constructor
2026-01-14 18:49:23 +08:00
Simon 62519416bb chore(version): bump version to 0.0.24 2026-01-14 18:34:55 +08:00
Simon a1e70e663d feat(PageController): exclude aria-hidden elements 2026-01-14 18:04:31 +08:00
Simon 338e317be1 chore(website): fix homepage overflow 2026-01-14 17:45:49 +08:00
Simon 9a66d5205f feat(website): display version in header and load from page-agent package 2026-01-14 17:42:18 +08:00
Simon 7d45bf6598 feat(website): update CDN links and usage 2026-01-14 17:27:35 +08:00
Simon 78ee72eea4 chore(version): bump version to 0.0.23 2026-01-14 15:46:10 +08:00
Simon 3b71c535b1 fix: clean up debug code 2026-01-14 15:23:02 +08:00
Simon 6615f7b300 Merge pull request #110 from alibaba/fix/panel-text-uncompleted
feat: add HTML escaping to Panel
2026-01-14 15:17:43 +08:00
Simon 38caae5e23 feat: add HTML escaping to Panel 2026-01-14 15:15:51 +08:00
Simon 1751ba325d chore(version): bump version to 0.0.22 2026-01-13 21:01:16 +08:00
Simon 066b045822 Merge pull request #108 from alibaba/feat/dual-umd
feat: add `cdn` packages for iife builds
2026-01-13 20:58:37 +08:00
Simon 3b21c42cdb feat: add cdn packages for iife builds 2026-01-13 20:52:41 +08:00
Simon 8b1e91d665 Merge pull request #107 from alibaba/refactor/mv-simulator-mask-to-page-controller
Refactor: move simulator mask to page controller
2026-01-13 17:38:37 +08:00
Simon aa5909264a chore: mv mask code to folder 2026-01-13 17:35:24 +08:00
Simon b6232d4e21 refactor: mv SimulatorMask from ui to page-controller 2026-01-13 17:33:38 +08:00
Simon 0d3cef5578 chore(version): bump version to 0.0.21 2026-01-13 15:18:50 +08:00
Simon 1e737f3a71 docs: models recommendation 2026-01-13 15:17:43 +08:00
Simon 2f2bdb4697 fix: update Qwen patch condition for model name 2026-01-13 15:07:01 +08:00
Simon 3fa7c8b85a feat: improve qwen3 compatibility (#106)
* feat: improve qwen3 compatibility

* fix: qwen

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-13 15:04:03 +08:00
dependabot[bot] 7efce91a91 chore(deps-dev): bump the development-dependencies group with 11 updates (#101)
Bumps the development-dependencies group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.3.0` | `20.3.1` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.3.0` | `20.3.1` |
| [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports) | `6.0.1` | `6.0.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.3` | `25.0.6` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.5.1` | `2.5.5` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.5.1` | `2.5.5` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.52.0` | `8.53.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.3.0` | `7.3.1` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.7` | `19.2.8` |
| [i18next](https://github.com/i18next/i18next) | `25.7.3` | `25.7.4` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.5.1` | `16.5.2` |


Updates `@commitlint/cli` from 20.3.0 to 20.3.1
- [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.3.1/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.3.0 to 20.3.1
- [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.3.1/@commitlint/config-conventional)

Updates `@trivago/prettier-plugin-sort-imports` from 6.0.1 to 6.0.2
- [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases)
- [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/main/CHANGELOG.md)
- [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v6.0.1...v6.0.2)

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

Updates `eslint-plugin-react-dom` from 2.5.1 to 2.5.5
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.5.5/packages/plugins/eslint-plugin-react-dom)

Updates `eslint-plugin-react-x` from 2.5.1 to 2.5.5
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.5.5/packages/plugins/eslint-plugin-react-x)

Updates `typescript-eslint` from 8.52.0 to 8.53.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.53.0/packages/typescript-eslint)

Updates `vite` from 7.3.0 to 7.3.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.1/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite)

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

Updates `i18next` from 25.7.3 to 25.7.4
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v25.7.3...v25.7.4)

Updates `react-i18next` from 16.5.1 to 16.5.2
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.5.1...v16.5.2)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@trivago/prettier-plugin-sort-imports"
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.0.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.53.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: vite
  dependency-version: 7.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: i18next
  dependency-version: 25.7.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: react-i18next
  dependency-version: 16.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 14:30:02 +08:00
dependabot[bot] a11d370f2b chore(deps): bump the production-dependencies group with 3 updates (#100)
Bumps the production-dependencies group with 3 updates: [ai-motion](https://github.com/gaomeng1900/ai-motion), [motion](https://github.com/motiondivision/motion) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `ai-motion` from 0.4.7 to 0.4.8
- [Release notes](https://github.com/gaomeng1900/ai-motion/releases)
- [Commits](https://github.com/gaomeng1900/ai-motion/compare/v0.4.7...v0.4.8)

Updates `motion` from 12.24.0 to 12.26.1
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.24.0...v12.26.1)

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

---
updated-dependencies:
- dependency-name: ai-motion
  dependency-version: 0.4.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: motion
  dependency-version: 12.26.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: simple-icons
  dependency-version: 16.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 14:27:23 +08:00
dependabot[bot] d7bfa42f85 chore(deps-dev): bump globals from 16.5.0 to 17.0.0 (#102)
Bumps [globals](https://github.com/sindresorhus/globals) from 16.5.0 to 17.0.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.5.0...v17.0.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 14:26:37 +08:00
Simon 551ba49280 Merge pull request #104 from alibaba/refactor/mv-brain-outof-llms
refactor: move agent logic out of `llms`
2026-01-13 14:24:29 +08:00
Simon adab24bb22 Merge branch 'refactor/mv-brain-outof-llms' of https://github.com/alibaba/page-agent into refactor/mv-brain-outof-llms 2026-01-13 14:22:48 +08:00
Simon 64aea7e84c chore: typing 2026-01-13 14:21:57 +08:00
Simon 67f6bd619e chore: rename autoFixer for better understanding 2026-01-13 14:20:52 +08:00
Simon 35d1fd1166 chore: better erroring
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-13 14:16:51 +08:00
Simon d5b8019fb1 chore: better docs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-13 14:14:56 +08:00
Simon 526cb4dbb3 chore: rename OpenAIClient; clean up old notes 2026-01-13 13:58:21 +08:00
Simon 6dc56c57c6 chore: rm legacy llm client 2026-01-13 13:56:30 +08:00
Simon a3263fdd3a chore: rename OpenAICompatibleClient 2026-01-13 13:51:09 +08:00
Simon 14974c0257 feat!: mv brain from llms to agent; redo toolCall auto fixer 2026-01-13 13:49:19 +08:00
Simon e70ae40096 chore: guardrail 2026-01-12 13:55:39 +08:00
Simon 3e04a131f1 chore(version): bump version to 0.0.20 2026-01-11 01:40:54 +08:00
Simon b487a7900b feat(website): layout change 2026-01-11 01:40:21 +08:00
Simon 78b49c8b7a Merge pull request #99 from alibaba/refactor/website-structure
refactor: website structure
2026-01-11 01:21:55 +08:00
Simon 949e511dcf Merge pull request #98 from alibaba/feat/data-masking
feat: data masking
2026-01-11 01:17:08 +08:00
Simon d9faca5d01 refactor: normalize website structure 2026-01-11 01:16:57 +08:00
Simon 398c6d0869 chore: rm test-pages 2026-01-11 01:04:43 +08:00
Simon 54d478df1e docs: update data masking docs 2026-01-11 00:58:42 +08:00
Simon 43c80f5418 feat: transformPageContent hook for data-masking 2026-01-11 00:54:59 +08:00
Simon b186ffcaa6 chore: info about CDN 2026-01-11 00:05:21 +08:00
Simon 778bbda4b8 chore: remove roadmap 2026-01-10 23:53:19 +08:00
Simon f5900bd096 chore(version): bump version to 0.0.19 2026-01-10 23:35:02 +08:00
Simon 09772d79bd fix: panel language not found 2026-01-10 23:33:36 +08:00
Simon bfa6a5347e fix: rm maxTokens from api 2026-01-10 23:27:08 +08:00
Simon da5cf44012 Merge pull request #97 from alibaba/feat/instructions
feat: custom instructions
2026-01-10 19:27:27 +08:00
Simon 1666c08e58 refactor: rename instructions to custom-instructions 2026-01-10 19:25:56 +08:00
Simon 0c0c76fdd8 feat: wrap user callback in try catch
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-10 19:23:02 +08:00
Simon 6b0b1218ac chore: cleanup 2026-01-10 19:23:01 +08:00
Simon d70f652285 chore(website): add instructions for PageAgent demo page 2026-01-10 19:08:50 +08:00
Simon c3c01b63fb docs: upgrade docs for custom instructions 2026-01-10 19:02:18 +08:00
Simon 463d010c24 feat(agent): add instructions api 2026-01-10 18:44:09 +08:00
Simon 4e52f17b9f feat(agent): mv <agent_state> in front of <agent_history> 2026-01-10 17:44:08 +08:00
Simon 5237edfcca Merge branch 'main' of https://github.com/alibaba/page-agent 2026-01-07 19:58:56 +08:00
Simon 79dcf9d9e4 docs: fix api doc, should be model instead of modelName 2026-01-07 19:58:43 +08:00
Simon 09544205ed Merge pull request #91 from alibaba/dependabot/npm_and_yarn/production-dependencies-d50d67c463
chore(deps): bump the production-dependencies group across 1 directory with 3 updates
2026-01-07 19:28:54 +08:00
Simon 2c74c3170a Merge pull request #92 from alibaba/dependabot/npm_and_yarn/development-dependencies-f82d5f2ef0
chore(deps-dev): bump the development-dependencies group across 1 directory with 7 updates
2026-01-07 19:28:40 +08:00
dependabot[bot] 9cee8f4174 chore(deps-dev): bump the development-dependencies group across 1 directory with 7 updates
Bumps the development-dependencies group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.2.0` | `20.3.0` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.2.0` | `20.3.0` |
| [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports) | `6.0.0` | `6.0.1` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.3.13` | `2.5.1` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.3.13` | `2.5.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.50.1` | `8.52.0` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.5.0` | `16.5.1` |



Updates `@commitlint/cli` from 20.2.0 to 20.3.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.3.0/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.2.0 to 20.3.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.3.0/@commitlint/config-conventional)

Updates `@trivago/prettier-plugin-sort-imports` from 6.0.0 to 6.0.1
- [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases)
- [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/main/CHANGELOG.md)
- [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v6.0.0...v6.0.1)

Updates `eslint-plugin-react-dom` from 2.3.13 to 2.5.1
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.5.1/packages/plugins/eslint-plugin-react-dom)

Updates `eslint-plugin-react-x` from 2.3.13 to 2.5.1
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.5.1/packages/plugins/eslint-plugin-react-x)

Updates `typescript-eslint` from 8.50.1 to 8.52.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.52.0/packages/typescript-eslint)

Updates `react-i18next` from 16.5.0 to 16.5.1
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.5.0...v16.5.1)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@trivago/prettier-plugin-sort-imports"
  dependency-version: 6.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.52.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: react-i18next
  dependency-version: 16.5.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-05 19:38:18 +00:00
dependabot[bot] d05b43cf50 chore(deps): bump the production-dependencies group across 1 directory with 3 updates
Bumps the production-dependencies group with 3 updates in the / directory: [zod](https://github.com/colinhacks/zod), [motion](https://github.com/motiondivision/motion) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `zod` from 4.2.1 to 4.3.5
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.2.1...v4.3.5)

Updates `motion` from 12.23.26 to 12.24.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.23.26...v12.24.0)

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

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.3.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: motion
  dependency-version: 12.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: simple-icons
  dependency-version: 16.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-05 19:37:33 +00:00
247 changed files with 27978 additions and 18021 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ body:
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 read our guidelines.
感谢您对改进项目的兴趣!提交前请阅读我们的指南。 感谢您对改进项目的兴趣!提交前请阅读我们的指南。
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md) - [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) - [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
- type: textarea - type: textarea
+1 -1
View File
@@ -10,5 +10,5 @@ contact_links:
url: https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md url: https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md
about: How to contribute code and ideas. 如何进行贡献与提交代码。 about: How to contribute code and ideas. 如何进行贡献与提交代码。
- name: Code of Conduct / 行为准则 - name: Code of Conduct / 行为准则
url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md url: https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md
about: Community expectations and standards. 社区行为期望与标准。 about: Community expectations and standards. 社区行为期望与标准。
+1 -1
View File
@@ -9,7 +9,7 @@ body:
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 read our guidelines.
感谢您对改进项目的兴趣!提交前请阅读我们的指南。 感谢您对改进项目的兴趣!提交前请阅读我们的指南。
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md) - [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) - [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
- type: textarea - type: textarea
+2 -1
View File
@@ -22,4 +22,5 @@ Closes #(issue)
## Requirements / 要求 ## Requirements / 要求
- [ ] I have read and follow the [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) . / 我已阅读并遵守行为准则。 - [ ] 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 自主生成的,我已亲自编写或充分审查了每一处变更。
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [20, 24, 25] node-version: [24]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
+10 -2
View File
@@ -6,7 +6,7 @@ on:
- 'v*' - 'v*'
permissions: permissions:
id-token: write # Required for OIDC id-token: write # Required for OIDC
contents: read contents: read
jobs: jobs:
@@ -32,4 +32,12 @@ jobs:
run: npm run build:libs run: npm run build:libs
- name: Publish all public packages - name: Publish all public packages
run: npm publish --workspaces --access public run: |
VERSION=${GITHUB_REF#refs/tags/v}
if [[ "$VERSION" == *"-"* ]]; then
# Prerelease version (e.g., 0.3.0-beta.1) -> extract tag name before the dot
TAG=$(echo "$VERSION" | sed 's/.*-\([a-zA-Z]*\).*/\1/')
npm publish --workspaces --access public --tag "$TAG"
else
npm publish --workspaces --access public
fi
+12 -6
View File
@@ -25,11 +25,17 @@ dist-ssr
*.sw? *.sw?
.qoder .qoder
# env files
#
.env .env
.env.*
# Copied files for npm publish (generated by prepublishOnly) # extension
packages/page-agent/README.md .output
packages/page-agent/README-zh.md .wxt
packages/page-agent/LICENSE
# AI
.agent
.claude
.cursor
.gemini
CLAUDE.md
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"recommendations": ["dbaeumer.vscode-eslint", "prettier.prettier-vscode"] "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
} }
+37 -26
View File
@@ -1,28 +1,39 @@
{ {
"editor.fontLigatures": true, "cSpell.words": [
"cSpell.words": [ "contenteditable",
"deepseek", "deepseek",
"HITL", "historychange",
"innerhtml", "HITL",
"llms", "innerhtml",
"opensource", "languagedetector",
"qwen", "llms",
"retryable", "magicui",
"shadcn", "npmmirror",
"wouter" "onwarn",
], "opensource",
"markdownlint.config": { "qwen",
// "comment": "Relaxed rules", "retryable",
"default": true, "shadcn",
"whitespace": false, "sidepanel",
"line_length": false, "statuschange",
"ul-indent": false, "wouter"
"no-inline-html": false, ],
"no-bare-urls": false, "files.exclude": {
"fenced-code-language": false, "packages/*/node_modules": true
"first-line-h1": false, },
"block-spacing": false, "markdownlint.config": {
"blanks-around-lists": false, // "comment": "Relaxed rules",
"ol-prefix": false "default": true,
} "whitespace": false,
"line_length": false,
"ul-indent": false,
"no-inline-html": false,
"no-bare-urls": false,
"fenced-code-language": false,
"first-line-h1": false,
"block-spacing": false,
"blanks-around-lists": false,
"ol-prefix": false,
"no-duplicate-heading": false
}
} }
+41 -33
View File
@@ -4,14 +4,16 @@
This is a **monorepo** with npm workspaces: This is a **monorepo** with npm workspaces:
- **Core Library** (`packages/page-agent/`) - AI agent for browser DOM automation, published as `page-agent` on npm - **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
- **Extension** (`packages/extension/`) - Browser extension (WXT + React) 🚧 WIP
- **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`** - **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**
Internal packages: Internal packages:
- **Core** (`packages/core/`) - PageAgentCore without UI (npm: `@page-agent/core`)
- **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model - **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model
- **Page Controller** (`packages/page-controller/`) - DOM operations, independent of LLM - **Page Controller** (`packages/page-controller/`) - DOM operations and visual feedback (SimulatorMask), independent of LLM
- **UI** (`packages/ui/`) - Panel, SimulatorMask, i18n. Decoupled from PageAgent - **UI** (`packages/ui/`) - Panel and i18n. Decoupled from PageAgent
## Development Commands ## Development Commands
@@ -20,6 +22,7 @@ npm start # Start website dev server
npm run build # Build all packages npm run build # Build all packages
npm run build:libs # Build all libraries npm run build:libs # Build all libraries
npm run lint # ESLint with TypeScript strict rules npm run lint # ESLint with TypeScript strict rules
npm run zip -w @page-agent/ext # Zip the extension package
``` ```
## Architecture ## Architecture
@@ -30,9 +33,11 @@ Simple monorepo solution: TypeScript references + Vite aliases. Update tsconfig
``` ```
packages/ packages/
├── page-agent/ # npm: "page-agent" ⭐ MAIN ├── core/ # npm: "@page-agent/core" ⭐ Core agent logic (headless)
├── page-agent/ # npm: "page-agent" entry class (with UI + controller + demo builds)
├── website/ # @page-agent/website (private) ├── website/ # @page-agent/website (private)
├── llms/ # @page-agent/llms ├── llms/ # @page-agent/llms
├── extension/ # Browser extension (WXT + React)
├── page-controller/ # @page-agent/page-controller ├── page-controller/ # @page-agent/page-controller
└── ui/ # @page-agent/ui └── ui/ # @page-agent/ui
``` ```
@@ -41,10 +46,11 @@ packages/
### Module Boundaries ### Module Boundaries
- **Page Agent**: Core lib. Imports from `@page-agent/llms`, `@page-agent/page-controller`, `@page-agent/ui` - **Page Agent**: Main entry with UI. Extends PageAgentCore and adds Panel. Imports from `@page-agent/core`, `@page-agent/ui`
- **Core**: PageAgentCore without UI. Imports from `@page-agent/llms`, `@page-agent/page-controller`
- **LLMs**: LLM client with MacroToolInput contract. No dependency on page-agent - **LLMs**: LLM client with MacroToolInput contract. No dependency on page-agent
- **UI**: Panel, Mask, i18n. No dependency on page-agent - **UI**: Panel and i18n. Decoupled from PageAgent via PanelAgentAdapter interface
- **Page Controller**: Pure DOM operations. No LLM or UI dependency - **Page Controller**: DOM operations with optional visual feedback (SimulatorMask). No LLM dependency. Enable mask via `enableMask: true` config
### PageController ↔ PageAgent Communication ### PageController ↔ PageAgent Communication
@@ -69,47 +75,46 @@ const pageInfo = await this.pageController.getPageInfo()
3. **LLM Processing**: AI returns action plans (page-agent) 3. **LLM Processing**: AI returns action plans (page-agent)
4. **Indexed Operations**: PageAgent calls PageController by element index 4. **Indexed Operations**: PageAgent calls PageController by element index
### CDN Auto-Injection
Library auto-initializes via script tag:
```html
<script src="page-agent.js?model=gpt-4"></script>
```
Query params configure `PageAgentConfig` in `src/umd.ts`.
## Key Files Reference ## Key Files Reference
### Page Agent (`packages/page-agent/`) ### Page Agent (`packages/page-agent/`)
| File | Description | | File | Description |
|------|-------------| | ------------------ | -------------------------------------------- |
| `src/PageAgent.ts` | ⭐ Main AI agent class | | `src/PageAgent.ts` | ⭐ Main class with UI, extends PageAgentCore |
| `src/umd.ts` | CDN/UMD entry with auto-init | | `src/demo.ts` | IIFE demo entry (auto-init with demo API) |
| `src/tools/` | Tool definitions calling PageController |
### Core (`packages/core/`)
| File | Description |
| ---------------------- | --------------------------------------- |
| `src/PageAgentCore.ts` | ⭐ Core agent class without UI |
| `src/tools/` | Tool definitions calling PageController |
| `src/config/` | Configuration types and constants |
| `src/prompts/` | System prompt templates |
### LLMs (`packages/llms/`) ### LLMs (`packages/llms/`)
| File | Description | | File | Description |
|------|-------------| | --------------------- | ------------------------------------- |
| `src/index.ts` | ⭐ LLM class with retry logic | | `src/index.ts` | ⭐ LLM class with retry logic |
| `src/types.ts` | MacroToolInput, AgentBrain, LLMConfig | | `src/types.ts` | MacroToolInput, AgentBrain, LLMConfig |
| `src/OpenAILenientClient.ts` | OpenAI-compatible client | | `src/OpenAIClient.ts` | OpenAI-compatible client |
### Page Controller (`packages/page-controller/`) ### Page Controller (`packages/page-controller/`)
| File | Description | | File | Description |
|------|-------------| | --------------------------- | ---------------------------------------------------------- |
| `src/PageController.ts` | ⭐ Main controller class | | `src/PageController.ts` | ⭐ Main controller class with optional mask support |
| `src/actions.ts` | Element interactions (click, input, scroll) | | `src/SimulatorMask.ts` | Visual overlay blocking user interaction during automation |
| `src/dom/dom_tree/index.js` | Core DOM extraction engine | | `src/actions.ts` | Element interactions (click, input, scroll) |
| `src/dom/dom_tree/index.js` | Core DOM extraction engine |
## Adding New Features ## Adding New Features
### New Agent Tool ### New Agent Tool
1. Implement in `packages/page-agent/src/tools/index.ts` 1. Implement in `packages/core/src/tools/index.ts`
2. If tool needs DOM ops, add method to PageController first 2. If tool needs DOM ops, add method to PageController first
3. Tool calls `this.pageController.methodName()` for DOM interactions 3. Tool calls `this.pageController.methodName()` for DOM interactions
@@ -123,4 +128,7 @@ Query params configure `PageAgentConfig` in `src/umd.ts`.
- Explicit typing for exported/public APIs - Explicit typing for exported/public APIs
- ESLint relaxes some unsafe rules for rapid iteration - ESLint relaxes some unsafe rules for rapid iteration
- Every change you make should not only implement the desired functionality but also improve the quality of the codebase
- All code and comments must be in English. - All code and comments must be in English.
- Do not try to hide errors or risks. They are valuable feedbacks for developers and users. Make them visible and actionable.
- Traceability and predictability is more important than success rate.
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
+120 -84
View File
@@ -1,35 +1,41 @@
# Contributing to Page-Agent # Contributing to PageAgent
Thank you for your interest in contributing to Page-Agent! We welcome contributions from everyone. > **[A note from the maintainer](https://github.com/alibaba/page-agent/issues/349)** — Please read before opening issues or PRs.
♥️ We welcome contributions from everyone.
## 🚀 Quick Start ## 🚀 Quick Start
### Development Setup ### Development Setup
1. **Prerequisites** 1. **Prerequisites**
- `node.js >= 20` with `npm >= 10` - `macOS` / `Linux` / `WSL`
- An editor that supports `ts/eslint/prettier` - `node.js >= 20` with `npm >= 10`
- Make sure `eslint`, `prettier` and `commitlint` work well - 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** 2. **Setup**
```bash ```bash
npm install npm i
npm start # Start demo and documentation site npm start # Start demo and documentation site
``` npm run build # Build libs and website
```
### Project Structure ### Project Structure
This is a **monorepo** with npm workspaces containing **two main packages**: This is a **monorepo** with npm workspaces containing **4 main packages**:
1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm - **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website` - **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. > 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 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. Use external and disable ts `paths` alias.
- When bundling `UMD` and `Website`. Bundle everything including local packages. > - When bundling `IIFE` and `Website`. Bundle everything together.
## 🤝 How to Contribute ## 🤝 How to Contribute
@@ -44,40 +50,40 @@ We use a simplified monorepo solution with `native npm-workspace + ts reference
1. **Fork and Clone** 1. **Fork and Clone**
```bash ```bash
git clone https://github.com/your-username/page-agent.git git clone https://github.com/your-username/page-agent.git
cd page-agent cd page-agent
``` ```
2. **Create Feature Branch** 2. **Create Feature Branch**
```bash ```bash
git checkout -b feat/your-feature-name git checkout -b feat/your-feature-name
``` ```
3. **Make Changes** 3. **Make Changes**
- Follow existing code style and patterns - Follow existing code style and patterns
- Add tests for new functionality - Add tests for new functionality
- Update documentation as needed - Update documentation as needed
4. **Test Your Changes** 4. **Test Your Changes**
- Build and lint everything.
```bash - Test in our demo website
# TODO - Test it on other websites if applicable
``` - `@TODO: test suite`
5. **Commit and Push** 5. **Commit and Push**
```bash ```bash
git add . git add .
git commit -m "feat: add awesome feature" git commit -m "feat: add awesome feature"
git push origin feat/your-feature-name git push origin feat/your-feature-name
``` ```
6. **Create Pull Request** 6. **Create Pull Request**
- Provide clear description of changes - Provide clear description of changes
- Link related issues - Link related issues
- Include screenshots for UI changes - Include screenshots for UI changes
## 📝 Code Style ## 📝 Code Style
@@ -89,78 +95,108 @@ We use a simplified monorepo solution with `native npm-workspace + ts reference
- Keep functions small and focused - Keep functions small and focused
- Add JSDoc for public APIs - Add JSDoc for public APIs
### Vibe coding with AI ### Vibe Coding with AI
- It's **recommended** to heavily rely on AI (aka "vibe coding") when maintaining **demo pages and tests**. > [Vibe coding](https://en.wikipedia.org/wiki/Vibe_coding)
- BUT **NOT the core lib!!!** Be very careful if AI ever touched the core lib!!!
- Vibe coding is **RECOMMENDED** when maintaining **the demo, the website, the UI and tests**.
- We have a [website/AGENTS.md](packages/website/AGENTS.md) for that.
- Vibe coding is **NOT** allowed for the core lib!!!
- NEVER try to vibe coding the MV3 extension!!! It is HELL.
- Review anything AI wrote before make a commit. You are the author of anything you commit. NOT AI. - Review anything AI wrote before make a commit. You are the author of anything you commit. NOT AI.
- Update the AI instructions when structure changed.
- Cursor and Cline: `./.cursor/rules` If your AI assistant does not support [AGENTS.md](https://agents.md/). Add an alias for it:
- Github Copilot: `./.github/copilot-instructions.md`
- Claude Code: `./CLAUDE.md` - claude-code (`CLAUDE.md`)
```markdown
@AGENTS.md
```
- antigravity (`.agent/rules/alias.md`)
```markdown
---
trigger: always_on
---
@../../AGENTS.md
```
## 🔧 Development Workflows ## 🔧 Development Workflows
### Website Development ### Test With Your Own LLM API
- Create a `.env` file in the repo root with your LLM API config
```env
LLM_MODEL_NAME=gpt-5.2
LLM_API_KEY=your-api-key
LLM_BASE_URL=https://api.your-llm-provider.com/v1
```
- **Ollama example** (tested on 0.15 + qwen3:14b, RTX3090 24GB):
```env
LLM_BASE_URL="http://localhost:11434/v1"
LLM_API_KEY="NA"
LLM_MODEL_NAME="qwen3:14b"
```
> @see https://alibaba.github.io/page-agent/docs/features/models#ollama for configuration
- **Restart the dev server** to load new env vars
- If not provided, the demo will use the free testing proxy by default. By using it, you agree to its [terms](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md).
### Extension Development
```bash ```bash
npm start # React development server # 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
``` ```
### Core Lib Development and Testing - Update `packages/extension/docs/extension_api.md` for API integration details
> @TODO this part is outdated. Update this. ### Testing on Other Websites
- Config your LLM API - Start and serve a local `iife` script
- Start and serve a local umd script
```bash ```bash
npm run dev:umd # Serving UMD with auto rebuild at http://localhost:5173/page-agent.umd.cjs npm run dev:demo # Serving IIFE with auto rebuild at http://localhost:5174/page-agent.demo.js
``` ```
- Add a new bookmark enable it on other website - Add a new bookmark
``` ```javascript
javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5173/page-agent.umd.cjs?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})(); 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 ### Adding Documentation
1. Create `website/src/docs/section/page-name/page.tsx` Ask an AI to help you add documentation to the `website/` package. Follow the existing style.
2. Add route to `website/src/router.tsx`
3. Add navigation link to `website/src/components/DocsLayout.tsx`
## 🎯 Contribution Areas > Our AGENTS.md file and guardrails are designed for this purpose. But please be careful and review anything AI generated.
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 ## 🚫 What We Don't Accept
- Changes that break existing API compatibility - Breaking changes and large PRs without prior discussion
- Heavy dependencies to core library - Heavy dependencies to core libs
- Contributions without proper testing - Contributions without proper testing
- Code that doesn't follow project conventions - Code that doesn't follow project conventions
- Dependencies or code with licenses incompatible with MIT
- Bot or AI-generated pull requests without meaningful human involvement
## 📄 Legal ## 📄 Legal
By contributing to this project, you agree that your contributions will be licensed under the MIT License. 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. > CLA is optional.
### Browser-Use Attribution
Parts of this project are derived from the [browser-use](https://github.com/browser-use/browser-use) project (MIT License). When contributing to DOM-related functionality:
- Maintain existing attribution comments
- Follow similar patterns for consistency
- Credit browser-use for derived concepts
## 💬 Questions? ## 💬 Questions?
@@ -168,4 +204,4 @@ Parts of this project are derived from the [browser-use](https://github.com/brow
- Check existing documentation and issues first - Check existing documentation and issues first
- Be respectful and constructive in discussions - Be respectful and constructive in discussions
Thank you for helping make Page-Agent better! 🎉 Thank you for helping make PageAgent better! 🎉
+2 -1
View File
@@ -1,6 +1,7 @@
MIT License MIT License
Copyright (c) 2025 Alibaba Copyright (c) 2026 SimonLuvRamen
Copyright (c) 2026 Alibaba Group Holding Limited
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
-128
View File
@@ -1,128 +0,0 @@
# PageAgent 🤖🪄
![banner](https://img.alicdn.com/imgextra/i1/O1CN01RY0Wvh26ATVeDIX7v_!!6000000007621-0-tps-1672-512.jpg)
[![npm version](https://badge.fury.io/js/page-agent.svg)](https://badge.fury.io/js/page-agent) [![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)
纯 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>
<video id="demo-video" src="https://github.com/user-attachments/assets/141bbb01-8022-4d1f-919d-9efc9a1dc1cf" width="640" crossorigin muted autoplay loop></video>
---
## ✨ Features
- **🎯 轻松集成**
- **🔐 端侧运行**
- **🧠 HTML 脱水**
- **💬 自然语言接口**
- **🎨 HITL 交互界面**
## 🗺️ Roadmap
👉 [**Roadmap**](./ROADMAP.md)
## 🚀 快速开始
### CDN 集成
```html
// CDN - https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js
// Mirror(CN) - https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js
<script
src="https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js"
crossorigin="true"
type="text/javascript"
></script>
```
### NPM 安装
```bash
npm install page-agent
```
```javascript
import { PageAgent } from 'page-agent'
// 测试接口
// @note: 限流,限制 prompt 内容,限制来源,随时变更,请替换成你自己的
// @note: 使用 DeepSeek-chat(3.2) 官方版本,使用协议和隐私策略见 DeepSeek 网站
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const agent = new PageAgent({
modelName: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
language: 'zh-CN',
})
await agent.execute('点击登录按钮')
```
## 🏗️ 架构设计
PageAgent adopts a simplified monorepo structure:
```
packages/
├── page-agent/ # AI agent (npm: page-agent)
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
├── page-controller/ # DOM 操作 (npm: @page-agent/page-controller)
├── ui/ # 面板 & 蒙层 & 模拟鼠标 (npm: @page-agent/ui)
└── website/ # 文档站点
```
## 🤝 贡献
欢迎社区贡献!以下是参与方式:
### 开发环境
1. Fork 项目仓库
2. Clone or fork: `git clone https://github.com/alibaba/page-agent.git && cd page-agent`
3. 安装依赖: `npm install`
4. 启动开发: `npm start`
### 贡献指南
请在贡献前阅读我们的[行为准则](CODE_OF_CONDUCT.md)和[贡献指南](CONTRIBUTING.md)。
## 👏 致谢
本项目基于以下优秀项目构建:
- **[browser-use](https://github.com/browser-use/browser-use)**
PageAgent 专为**客户端网页增强**设计,不是服务端自动化工具。
## 📄 许可证
MIT 许可证 - 详见 [LICENSE](LICENSE) 文件。
```
DOM processing components and prompt are derived from 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.
```
---
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
+56 -65
View File
@@ -1,45 +1,57 @@
# PageAgent 🤖🪄 # Page Agent
![banner](https://img.alicdn.com/imgextra/i3/O1CN01MyVCS21EoKkIHUT1s_!!6000000000398-49-tps-1280-353.webp) <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
</picture>
[![npm version](https://badge.fury.io/js/page-agent.svg)](https://badge.fury.io/js/page-agent) [![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. The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
🌐 **English** | [中文](./README-zh.md) 🌐 **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/de8d1964-8bde-494f-a52f-2975469557a5" width="640" crossorigin muted autoplay loop></video> <video id="demo-video" src="https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2" controls crossorigin muted></video>
--- ---
## ✨ Features ## ✨ Features
- **🎯 Easy Integration** - Transform your webpage into an agent with a single script tag. - **🎯 Easy integration**
- **🔐 Client-Side Processing** - No need for `browser extension` / `python` / `headless browser`.
- **🧠 DOM Extraction** - Just in-page javascript. Everything happens in your web page.
- **💬 Natural Language Interface** - **📖 Text-based DOM manipulation**
- **🎨 UI with Human in the loop** - No screenshots. No multi-modal LLMs or special permissions needed.
- **🧠 Bring your own LLMs**
- **🐙 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
## 🗺️ Roadmap ## 💡 Use Cases
👉 [**Roadmap**](./ROADMAP.md) - **SaaS AI Copilot** — Ship an AI copilot in your product in lines of code. No backend rewrite.
- **Smart Form Filling** — Turn 20-click workflows into one sentence. Perfect for ERP, CRM, and admin systems.
- **Accessibility** — Make any web app accessible through natural language. Voice commands, screen readers, zero barrier.
- **Multi-page Agent** — Extend your own agent's reach across browser tabs with the optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension).
## 🚀 Quick Start ## 🚀 Quick Start
### CDN Integration ### One-line integration
Fastest way to try PageAgent with our free Demo LLM:
```html ```html
// CDN - https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js <script src="{URL}" crossorigin="true"></script>
// Mirror(CN) - https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js
<script
src="https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js"
crossorigin="true"
type="text/javascript"
></script>
``` ```
> **⚠️ For technical evaluation only.** This demo CDN uses our free [testing LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api). By using it, you agree to its [terms](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md).
| Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.6.2/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.6.2/files/dist/iife/page-agent.demo.js |
### NPM Installation ### NPM Installation
```bash ```bash
@@ -49,72 +61,39 @@ npm install page-agent
```javascript ```javascript
import { PageAgent } from 'page-agent' import { PageAgent } from 'page-agent'
// test server
// @note: rate limit. prompt limit. Origin limit. May change anytime. Use your own llm!
// @note Using official DeepSeek-chat(3.2). Go to DeepSeek website for privacy policy.
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const agent = new PageAgent({ const agent = new PageAgent({
modelName: DEMO_MODEL, model: 'qwen3.5-plus',
baseURL: DEMO_BASE_URL, baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: DEMO_API_KEY, apiKey: 'YOUR_API_KEY',
language: 'en-US', language: 'en-US',
}) })
await agent.execute('Click the login button') await agent.execute('Click the login button')
``` ```
## 🏗️ Structure For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/docs/introduction/overview).
PageAgent adopts a simplified monorepo structure:
```
packages/
├── page-agent/ # AI agent (npm: page-agent)
├── llms/ # LLM client (npm: @page-agent/llms)
├── page-controller/ # DOM operations (npm: @page-agent/page-controller)
├── ui/ # Panel & Mask & Mouse Animation (npm: @page-agent/ui)
└── website/ # Demo & Documentation site
```
## 🤝 Contributing ## 🤝 Contributing
We welcome contributions from the community! Here's how to get started: We welcome contributions from the community! Follow our instructions in [CONTRIBUTING.md](CONTRIBUTING.md) for setup and guidelines.
### Setup Please read the [maintainer note](https://github.com/alibaba/page-agent/issues/349) and [Code of Conduct](docs/CODE_OF_CONDUCT.md) before opening issues or PRs.
1. Fork the repository Contributions generated entirely by **bots or agents** without substantial human involvement will **not be accepted**.
2. Clone your fork: `git clone https://github.com/alibaba/page-agent.git && cd page-agent`
3. Install dependencies: `npm install`
4. Start development: `npm start`
### Contributing Guidelines
Please read our [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) before contributing.
## 👏 Acknowledgments ## 👏 Acknowledgments
This project builds upon the excellent work of: This project builds upon the excellent work of **[`browser-use`](https://github.com/browser-use/browser-use)**.
- **[browser-use](https://github.com/browser-use/browser-use)** `PageAgent` is designed for **client-side web enhancement**, not server-side automation.
PageAgent is designed for **client-side web enhancement**, not server-side automation.
## 📄 License
MIT License - see the [LICENSE](LICENSE) file for details.
``` ```
DOM processing components and prompt are derived from browser-use: 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 Copyright (c) 2024 Gregor Zunic
Licensed under the MIT License 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 We gratefully acknowledge the browser-use project and its contributors for their
excellent work on web automation and DOM interaction patterns that helped make excellent work on web automation and DOM interaction patterns that helped make
this project possible. this project possible.
@@ -123,6 +102,18 @@ Third-party dependencies and their licenses can be found in the package.json
file and in the node_modules directory after installation. file and in the node_modules directory after installation.
``` ```
## 📄 License
[MIT License](LICENSE)
--- ---
**⭐ Star this repo if you find PageAgent helpful!** **⭐ Star this repo if you find PageAgent helpful!**
<a href="https://www.star-history.com/?repos=alibaba%2Fpage-agent&type=date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&theme=dark&legend=top-left&v=7" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
</picture>
</a>
-61
View File
@@ -1,61 +0,0 @@
# 🗺️ PageAgent Roadmap
The development progress and future plans for PageAgent.
## 🚀 Current Works
- [x] **MVP**
- Core functionality implemented.
- [x] **SPA interaction**
- [x] **Reasoning and (short) memory**
- [x] **Multi model provider integration and testing**
- [x] **UI with HITL**
- Human-in-the-loop user interface. Agent can ask user questions.
- [x] **Landing and doc pages**
- [x] **Remove `ai-sdk`**
- Only one function of AI-ADK is being used.
- Our agent memory and thinking mechanism does not suite ai-sdk.
- [x] **Robust LLM output**
- Auto-fix incomplete output format of DeepSeek and QWen.
- [x] **Working homepage with live LLM API**
- [x] **~~free~~ CDN**
- [x] **Free evaluation plan**
- [x] **Custom actions and HITL**
- [ ] **Hooks and Events**
- [x] **lifecycle hooks**
- [ ] **lifecycle events**
- [ ] **User takeover**
- [#64](https://github.com/alibaba/page-agent/issues/64)
- [ ] **❗Hijack `page_open/page_change/page_unload` behavior**
- [ ] **Custom knowledge base and instructions**
- [#45](https://github.com/alibaba/page-agent/issues/45)
- [ ] **Black/white-list safeguard**
- [ ] **Data-masking**
- [#44](https://github.com/alibaba/page-agent/issues/44)
- [ ] **Improve Memory**
- [#66](https://github.com/alibaba/page-agent/issues/66)
- [#65](https://github.com/alibaba/page-agent/issues/65)
- [ ] **Optimize for popular UI frameworks**
- [x] **i18n of the website**
- [x] Chinese version
- [x] English version
- [x] **Refactor: Separate `Agent` and `PageController`**
- [ ] **Chrome-ext wrapper for multi-page tasks**
♻️ Following browser-use's update and contribute back.
## 📋 Pending Features
- [ ] **Tools for more complex tasks**
- todo list
- file sys
- [x] **Support custom llm fetch**
- [ ] **Testing suits**
- [ ] **Same-origin multi-page-app rally**
- [ ] **Local MCP proxy**
## 🤔 To Be Decided
- [ ] **Cross-origin multi-page?**
- Tricky
- Need some kind of "memory rally"
+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.
+200
View File
@@ -0,0 +1,200 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.6.0] - 2026-03-21
### Features
- **Beta MCP support** - New `@page-agent/mcp` package lets MCP clients such as Claude Desktop and Copilot control the browser through the Page Agent extension
- **Better iframe handling** - Same-origin iframe elements are handled more reliably during DOM extraction and actions
- **Extension history workflows** - Users can rerun past tasks, export history sessions as JSON, and approve MCP-triggered tasks before execution
### Improvements
- **Unified versioning across packages** - The extension now follows the root workspace version. Changelog entries are no longer split into a separate extension version section
- **Configurable `stepDelay`** - Agent pacing between steps is now configurable via `stepDelay`
- **Optional API key** - `apiKey` can now be omitted for compatible deployments that do not require one
- **Optional named tool choice** - Tool invocation can disable named tool choice for providers that behave better without it
- **Better rich-text input support** - Improved `contenteditable` handling with better event dispatching and `execCommand` fallback for more editors
- **More flexible DOM extraction** - `includeAttributes` now supports wildcards, `contenteditable` is included by default, and heuristically interactive elements expose more useful attributes
- **MiniMax model support** - Added MiniMax compatibility, with the default recommendation updated to `MiniMax-M2.7`
### Bug Fixes
- Fixed Safari issues when `requestIdleCallback` is unavailable
- Avoid throwing when `webgl2` initialization fails
- Improved OpenAI-compatible request patches for GPT-5.4 chat tools and MiniMax temperature/tool-call compatibility
- Fixed several UI polish issues in the extension and website, including cursor and layout regressions
## [1.5.1] - 2026-03-05
### Breaking Changes
- **`data-browser-use-ignore``data-page-agent-ignore`** - DOM ignore attribute renamed to match the project identity
- **Config types restructured** - `PageAgentConfig` split into `AgentConfig` + `PageAgentCoreConfig`; config definitions moved from `config/index.ts` to `types.ts`
- **Zod v3/v4 dual support** - Libraries now accept both `zod@^3.25` and `zod@^4.0` as peer dependencies
### Features
- **Experimental `llms.txt` support** - Agent can fetch and include a site's `llms.txt` in context. Enable via `experimentalLlmsTxt: true`
### Improvements
- Default `maxSteps` changed from 20 to 40 for better for complex tasks out of the box
- Added 400ms wait between agent steps for page reactions
- Increased click wait time (100ms → 200ms) for more reliable interactions
- Removed debug `console.log` statements from scroll actions
- Reset observations on new task start
- Improved logging across packages
### Extension v0.1.9
> PageAgent 1.5.1
- **Advanced config panel** - New collapsible section exposing Max Steps, System Instruction, and experimental `llms.txt` toggle
- Streamlined User Auth Token description
- Moved testing API notice below auth token section
---
## [1.4.0] - 2026-02-27
### Features
- Update Terms of Use and Privacy Policy
- **Robust tool-call validation** - Action inputs are now validated against tool schemas individually, producing clear error messages (e.g. `Invalid input for action "click_element_by_index"`) instead of unreadable union parse errors
- **Primitive action input coercion** - Small models that output `{"click_element_by_index": 2}` instead of `{"click_element_by_index": {"index": 2}}` are now auto-corrected using tool schemas
- **Qwen model updates** - Added `qwen3.5-plus` as the default free testing model; disabled `enable_thinking` for Qwen models to avoid incompatible responses
- **Updated default LLM endpoint** - Migrated demo and extension to a new testing endpoint with legacy endpoint auto-migration
### Improvements
- Unified zod imports (`* as z`) across all packages for consistency
- Better Zod error formatting with `z.prettifyError()` in LLM client
- Exported `InvokeError` and `InvokeErrorType` as values (not just types) from `@page-agent/llms`
- Exported `SupportedLanguage` type from `@page-agent/core`
### Extension v0.1.8
- **Language setting** - Added language selector (System / English / 中文) in config panel
- **UI makeover** - New empty state with breathing glow and typing animation; ai-motion glow overlay while running; refined focus styles
- **Testing endpoint notice** - Shows terms of use notice when using the free testing API
- **Legacy endpoint migration** - Auto-migrates old Supabase testing endpoint to new endpoint on startup
---
## [1.3.0] - 2026-02-13
### Breaking Changes
- **Lifecycle: `stop()` vs `dispose()`** - New `stop()` method to cancel the current task while keeping the agent reusable. `dispose()` is now terminal — a disposed agent cannot be reused. This affects both `PageAgentCore` and `PanelAgentAdapter`.
### Features
- **Panel action button** - The panel button now morphs between Stop (■) and Close (X) based on agent status
- **Error history** - Errors and max-step failures are now recorded in `history` as `AgentErrorEvent`, making post-task analysis more complete
### Bug Fixes
- **AbortError handling** - `AbortError` is no longer retried by the LLM client, and shows a clean "Task stopped" message instead of a raw error stack
---
## [1.2.0] - 2026-02-11
### Features
- **Observe Phase** - Agent now observes the page before each action, improving decision accuracy on dynamic pages
- **Better Abort Handling** - Improved `abortSignal` support for cleaner task cancellation
### Improvements
- Pruned system prompts for lower token usage and faster responses
- Improved error handling during agent steps with better error messages
- Zod tree-shaking for smaller bundle size
### Bug Fixes
- Fixed indentation lost in DOM extraction caused by `trimLines`
- Fixed `gpt-5-mini` temperature configuration
---
## [1.1.0] - 2026-02-02
### Features
- **Custom System Prompt** - New `systemPrompt` config option to customize or extend the default system prompt
- **Chrome Extension** - Extension with multi-tab control, main-world API with token auth, and tab lifecycle management
### Improvements
- Renamed `include_attributes` to `includeAttributes` in PageController config (camelCase consistency)
- Lazy-loaded mask module for faster initialization
- Better date formatting and error messages from LLM client
- Added `rawRequest` to step history for easier debugging
### Bug Fixes
- Fixed CSP errors by using local SVGs for cursor mask instead of inline styles
- Fixed `AbortError` being incorrectly retried and shown to users
- Fixed mask not working correctly when starting a new task after stopping a previous one
---
## [1.0.0] - 2026-01-19
### 🎉 First Stable Release
PageAgent is now ready for production use. The API is stable and breaking changes will follow semantic versioning.
### Features
#### Core
- **PageAgent** - Main entry class with built-in UI Panel
- **PageAgentCore** - Headless agent class for custom UI or programmatic use
- **DOM Analysis** - Text-based DOM extraction with high-intensity dehydration
- **LLM Support** - Works with OpenAI, Claude, DeepSeek, Qwen, and other OpenAI-compatible APIs
- **Tool System** - Built-in tools for click, input, scroll, select, and more
- **Custom Tools** - Extend agent capabilities with your own tools (experimental)
- **Lifecycle Hooks** - Hook into agent execution (experimental)
- **Instructions System** - System-level and page-level instructions to guide agent behavior
- **Data Masking** - Transform page content before sending to LLM
#### Page Controller
- **Element Interactions** - Click, input text, select options, scroll
- **Visual Mask** - Blocks user interaction during automation
- **DOM Tree Extraction** - Efficient page structure extraction for LLM consumption
#### UI
- **Interactive Panel** - Real-time task progress and agent thinking display
- **Ask User Tool** - Agent can ask users for clarification
- **i18n Support** - English and Chinese localization
### Packages
| Package | Description |
| ----------------------------- | ---------------------------------- |
| `page-agent` | Main entry with UI Panel |
| `@page-agent/core` | Core agent logic without UI |
| `@page-agent/llms` | LLM client with retry logic |
| `@page-agent/page-controller` | DOM operations and visual feedback |
| `@page-agent/ui` | Panel and i18n |
### Known Limitations
- Single-page application only (cannot navigate across pages)
- No visual recognition (relies on DOM structure)
- Limited interaction support (no hover, drag-drop, canvas operations)
- See [Limitations](https://alibaba.github.io/page-agent/docs/introduction/limitations) for details
### Acknowledgments
This project builds upon the excellent work of [browser-use](https://github.com/browser-use/browser-use). DOM processing components and prompts are adapted from browser-use (MIT License).
@@ -69,10 +69,10 @@ members of the project's leadership.
## Attribution ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][https://www.contributor-covenant.org], version 1.4, This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
---- ---
> Chinese Version > Chinese Version
> 《阿里巴巴开源行为准则》 > 《阿里巴巴开源行为准则》
+118
View File
@@ -0,0 +1,118 @@
# Page Agent
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
</picture>
[![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>📖 Docs</b></a> | <a href="https://news.ycombinator.com/item?id=47264138" target="_blank"><b>📢 HN Discussion</b></a> | <a href="https://x.com/simonluvramen" target="_blank"><b>𝕏 Follow on X</b></a>
<video id="demo-video" src="https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2" controls crossorigin muted></video>
---
## ✨ Features
- **🎯 轻松集成**
- 无需 `浏览器插件` / `Python` / `无头浏览器`,纯页面内 JavaScript
- **📖 基于文本的 DOM 操作**
- 无需截图,无需多模态模型或特殊权限
- **🧠 自备 LLM**
- 🐙 可选的 [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 Copilot** — 几行代码为你的产品加上 AI 副驾驶,无需重写后端。
- **智能表单填写** — 把 20 次点击变成一句话。ERP、CRM、管理后台的最佳拍档。
- **无障碍增强** — 用自然语言让任何网页无障碍。语音指令、屏幕阅读器,零门槛。
- **跨页面 Agent** — 通过可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),让你自己的 Agent 跨标签页工作。
## 🚀 快速开始
### 一行代码集成
通过我们免费的 Demo LLM 快速体验 PageAgent
```html
<script src="{URL}" crossorigin="true"></script>
```
> **⚠️ 仅用于技术评估。** 该 Demo CDN 使用了免费的[测试 LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api),使用即表示您同意其[条款](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md)。
| Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.6.2/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.6.2/files/dist/iife/page-agent.demo.js |
### NPM 安装
```bash
npm install page-agent
```
```javascript
import { PageAgent } from 'page-agent'
const agent = new PageAgent({
model: 'qwen3.5-plus',
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: 'YOUR_API_KEY',
language: 'zh-CN',
})
await agent.execute('点击登录按钮')
```
更多编程用法,请参阅 [📖 文档](https://alibaba.github.io/page-agent/docs/introduction/overview)。
## 🤝 贡献
欢迎社区贡献!请参阅 [CONTRIBUTING.md](../CONTRIBUTING.md) 了解安装与贡献指南。
提交 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)** 的优秀工作构建。
`PageAgent` 专为**客户端网页增强**设计,不是服务端自动化工具。
```
DOM processing components and prompt are derived from browser-use:
Browser Use <https://github.com/browser-use/browser-use>
Copyright (c) 2024 Gregor Zunic
Licensed under the MIT License
We gratefully acknowledge the browser-use project and its contributors for their
excellent work on web automation and DOM interaction patterns that helped make
this project possible.
Third-party dependencies and their licenses can be found in the package.json
file and in the node_modules directory after installation.
```
## 📄 许可证
[MIT License](../LICENSE)
---
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
<a href="https://www.star-history.com/?repos=alibaba%2Fpage-agent&type=date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&theme=dark&legend=top-left&v=7" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
</picture>
</a>
+85
View File
@@ -0,0 +1,85 @@
# Terms of Use & Privacy
**Last updated:** March 2026
"We" in this document refers to the maintainers of the open-source Page Agent project (https://github.com/alibaba/page-agent). "The software" refers to Page Agent (the JavaScript library) and Page Agent Ext (the browser extension). This document covers the software itself and the testing API we provide — **not** any third-party product or service built with it.
---
## 1. Open Source Software Privacy
The software is a **client-side only** tool with a "Bring Your Own Key" (BYOK) architecture. The software itself does **not** include any backend service. The software does **not** collect or transmit any user data on its own, and we do **not** have access to your browsing activity, page content, or task instructions through the software.
All data transmission occurs **only** between your browser and the LLM provider you configure. You are in full control of which provider receives your data.
The project is open source under the [MIT License](https://github.com/alibaba/page-agent/blob/main/LICENSE) and can be audited at: https://github.com/alibaba/page-agent
---
## 2. Testing API and Demo Disclaimer & Terms of Use
To facilitate easy testing and technical evaluation, we provide a free testing LLM API. This API is used in the project homepage's live demo, the pre-built demo CDN bundles, and the browser extension's default configuration. Users may also use it independently for their own technical evaluation of the software.
This free testing API is provided **strictly for technical evaluation and R&D purposes only**. It must not be used in any production environment. By using this API, you agree to the following terms:
- **Permitted Use Only**: This API must be used solely for technical evaluation of the software. Any other use — including integration into other products or services, unlawful activities, violation of the underlying LLM provider's usage policies, or automated scraping at scale — is strictly prohibited.
- **No Sensitive Data**: You are strictly prohibited from inputting any Personal Identifiable Information (PII), confidential business data, financial/medical records, or using this agent on web pages containing such sensitive information.
- **Data Processing**: We do not store or log your prompts, webpage data (HTML), or any submitted content, nor do we use such data for model training. All data is processed in-transit and immediately discarded. We perform in-memory request validation to prevent abuse of the testing API, and temporarily process IP addresses for rate-limiting purposes. No data from these processes is retained. Data is processed through Alibaba Cloud infrastructure, which is subject to its own privacy policy.
- **Independent Infrastructure**: The software is completely frontend-based with a "Bring Your Own Key" (BYOK) architecture and **no built-in backend**. To facilitate easy testing, the maintainers have purchased public cloud services from Alibaba Cloud China ([aliyun.com](https://www.aliyun.com) Function Compute and BaiLian Qwen models). This project is not a product of, nor endorsed by, Alibaba Cloud.
- **No Guaranteed Availability**: This testing API may be rate-limited, degraded, or discontinued at any time without prior notice.
- **"AS IS" & Limitation of Liability**: This service is provided strictly on an "AS IS" and "AS AVAILABLE" basis, without any warranties. The maintainers bear no liability for any data loss, service interruption, or legal consequences arising from your use of this service.
- **Recommendation for Real Usage**: For secure and continuous usage, we strongly advise using the BYOK mode with your own legally compliant commercial LLM API keys, or connecting to local, offline models (e.g., Ollama).
**Note**: This free testing LLM API processes data via servers located in Mainland China. If you are located in a region with strict data localization laws (such as the EU/EEA), please do not use this API.
**Age Requirement**: The software and testing API are not intended for use by individuals under the age of 13 (or the minimum age of digital consent in your jurisdiction).
---
## 3. Browser Extension (Page Agent Ext)
### Data Processing
The extension performs DOM analysis and automation actions **locally in your browser**. Your browsing history, passwords, and form data are not accessed or collected by the extension developer.
Data is transmitted to external servers **only when you initiate an automation task**. When this occurs:
- Your task instructions (natural language commands)
- Simplified page structure (cleaned HTML) of all pages under the extension's control
are sent to the LLM API endpoint configured in **your settings**.
> **Note:** The HTML cleaning process simplifies page structure for AI readability but **does not guarantee removal of sensitive information** (e.g., visible text, form values, or personal data on the page). Please be mindful of the page content when initiating tasks.
**If you configure a third-party LLM provider** (e.g., OpenAI, Anthropic, or others), data is sent directly to that provider. Their privacy policies apply.
**If you use the testing API**, the terms in [Section 2](#2-testing-api-and-demo-disclaimer--terms-of-use) apply. By using the extension with the default testing API, you agree to those terms.
### Data Storage
- **Local storage only**: Your configuration (API endpoint, API key, model selection) is stored in your browser via `chrome.storage.local` (or equivalent browser storage APIs)
- **No cloud sync**: Configuration is not synced to any external server
- **No analytics**: The extension does not include any analytics or tracking code
### Your Control
- The extension is open source and can be audited by anyone
- You choose which LLM provider to use
- You may configure your own API endpoint at any time
- You can clear all stored data by removing the extension
---
## Changes
We may update these terms at our discretion.
## Contact
https://github.com/alibaba/page-agent/issues
+5 -2
View File
@@ -10,9 +10,10 @@ import tseslint from 'typescript-eslint'
export default defineConfig([ export default defineConfig([
globalIgnores([ globalIgnores([
'**/dist', '**/dist',
'**/test-pages',
'**/node_modules', '**/node_modules',
'packages/website/src/components/ui', 'packages/*/src/components/ui',
'**/.wxt',
'**/.output',
]), ]),
{ {
plugins: { plugins: {
@@ -74,6 +75,8 @@ export default defineConfig([
'react-dom/no-missing-button-type': 'off', 'react-dom/no-missing-button-type': 'off',
'react-x/no-nested-component-definitions': 'off', 'react-x/no-nested-component-definitions': 'off',
'@typescript-eslint/prefer-optional-chain': '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', // 'require-await': 'off',
'@typescript-eslint/require-await': 'off', '@typescript-eslint/require-await': 'off',
+11945 -8229
View File
File diff suppressed because it is too large Load Diff
+128 -101
View File
@@ -1,103 +1,130 @@
{ {
"name": "root", "name": "root",
"private": true, "private": true,
"version": "0.0.18", "version": "1.6.2",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
"packages/page-controller", "packages/page-controller",
"packages/ui", "packages/ui",
"packages/llms", "packages/llms",
"packages/page-agent", "packages/core",
"packages/website" "packages/page-agent",
], "packages/mcp",
"description": "AI-powered UI agent for web applications", "packages/extension",
"author": "Simon<gaomeng1900>", "packages/website"
"license": "MIT", ],
"repository": { "description": "AI-powered UI agent for web applications",
"type": "git", "author": "Simon<gaomeng1900>",
"url": "https://github.com/alibaba/page-agent.git" "license": "MIT",
}, "repository": {
"homepage": "https://alibaba.github.io/page-agent/", "type": "git",
"engines": { "url": "https://github.com/alibaba/page-agent.git"
"node": ">=20.0.0", },
"npm": ">=10.0.0" "homepage": "https://alibaba.github.io/page-agent/",
}, "engines": {
"scripts": { "node": "^20.19.0 || ^22.13.0 || >=24"
"cleanup": "rm -rf packages/*/dist", },
"start": "npm run dev --workspace=@page-agent/website", "scripts": {
"build:website": "npm run build:website --workspace=@page-agent/website", "start": "npm run dev --workspace=@page-agent/website",
"build:libs": "npm run build --workspaces --if-present", "dev:ext": "npm run dev -w @page-agent/ext",
"build": "npm run build:libs && npm run build:website", "dev:demo": "npm run dev:demo --workspace=page-agent",
"dev:umd": "npm run dev:umd --workspace=page-agent", "build": "npm run build:libs && npm run build:website",
"version": "node scripts/sync-version.js", "build:libs": "npm run build --workspaces --if-present",
"lint": "eslint .", "build:website": "npm run build:website --workspace=@page-agent/website",
"prepare": "husky" "build:ext": "npm run build:libs && npm run zip -w @page-agent/ext",
}, "version": "node scripts/sync-version.js",
"devDependencies": { "lint": "eslint .",
"@commitlint/cli": "^20.2.0", "cleanup": "rm -rf packages/*/dist",
"@commitlint/config-conventional": "^20.2.0", "prepare": "husky"
"@eslint/js": "^9.39.2", },
"@microsoft/api-extractor": "^7.55.2", "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^6.0.0", "@commitlint/cli": "^20.5.0",
"@types/node": "^25.0.3", "@commitlint/config-conventional": "^20.5.0",
"concurrently": "^9.2.1", "@eslint/js": "^9.39.2",
"dotenv": "^17.2.3", "@microsoft/api-extractor": "^7.57.7",
"eslint": "^9.39.2", "@tailwindcss/vite": "^4.2.2",
"eslint-config-prettier": "^10.1.8", "@trivago/prettier-plugin-sort-imports": "^6.0.2",
"eslint-plugin-react-dom": "^2.3.13", "@types/node": "^25.5.0",
"eslint-plugin-react-hooks": "^7.0.1", "@vitejs/plugin-react-swc": "^4.3.0",
"eslint-plugin-react-refresh": "^0.4.26", "chalk": "^5.6.2",
"eslint-plugin-react-x": "^2.3.13", "concurrently": "^9.2.1",
"globals": "^16.4.0", "dotenv": "^17.3.1",
"husky": "^9.1.7", "eslint": "^9.39.2",
"lint-staged": "^16.2.4", "eslint-config-prettier": "^10.1.8",
"prettier": "^3.7.4", "eslint-plugin-react-dom": "^2.13.0",
"typescript": "^5.9.3", "eslint-plugin-react-hooks": "^7.0.1",
"typescript-eslint": "^8.50.1", "eslint-plugin-react-refresh": "^0.5.2",
"unplugin-dts": "^1.0.0-beta.6", "eslint-plugin-react-x": "^2.13.0",
"vite": "^7.3.0", "globals": "^17.4.0",
"vite-plugin-css-injected-by-js": "^3.5.2" "husky": "^9.1.7",
}, "lint-staged": "^16.4.0",
"lint-staged": { "prettier": "^3.8.0",
"*.{js,ts,cjs,cts,mjs,mts}": [ "typescript": "^5.9.3",
"npx prettier --write --ignore-unknown", "typescript-eslint": "^8.57.2",
"npx eslint --quiet" "unplugin-dts": "^1.0.0-beta.6",
], "vite": "^7.3.1",
"*.{jsx,tsx}": [ "vite-plugin-css-injected-by-js": "^4.0.1",
"npx prettier --write --ignore-unknown", "vite-bundle-analyzer": "^1.3.6"
"npx eslint --quiet" },
], "overrides": {
"*.css": [ "typescript": "^5.9.3"
"npx prettier --write --ignore-unknown" },
] "lint-staged": {
}, "*.{js,ts,cjs,cts,mjs,mts}": [
"commitlint": { "npx prettier --write --ignore-unknown",
"extends": [ "npx eslint --quiet"
"@commitlint/config-conventional" ],
], "*.{jsx,tsx}": [
"rules": { "npx prettier --write --ignore-unknown",
"subject-case": [ "npx eslint --quiet"
0, ],
"never" "*.css": [
] "npx prettier --write --ignore-unknown"
} ]
}, },
"prettier": { "commitlint": {
"singleQuote": true, "extends": [
"semi": false, "@commitlint/config-conventional"
"useTabs": true, ],
"printWidth": 100, "rules": {
"trailingComma": "es5", "subject-case": [
"plugins": [ 0,
"@trivago/prettier-plugin-sort-imports" "never"
], ]
"importOrder": [ }
"<THIRD_PARTY_MODULES>", },
"^(@/).*(?<!css)$", "prettier": {
"^[./].*(?<!css)$", "singleQuote": true,
".css$" "semi": false,
], "useTabs": true,
"importOrderSeparation": true, "printWidth": 100,
"importOrderSortSpecifiers": true "trailingComma": "es5",
} "plugins": [
"@trivago/prettier-plugin-sort-imports"
],
"importOrder": [
"<THIRD_PARTY_MODULES>",
"^(@/).*(?<!css)$",
"^[./].*(?<!css)$",
".css$"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"overrides": [
{
"files": "*.md",
"options": {
"useTabs": false,
"tabWidth": 4
}
},
{
"files": "*.json",
"options": {
"useTabs": false,
"tabWidth": 4
}
}
]
}
} }
+56
View File
@@ -0,0 +1,56 @@
{
"name": "@page-agent/core",
"private": false,
"version": "1.6.2",
"type": "module",
"main": "./dist/esm/page-agent-core.js",
"module": "./dist/esm/page-agent-core.js",
"types": "./dist/esm/PageAgentCore.d.ts",
"exports": {
".": {
"types": "./dist/esm/PageAgentCore.d.ts",
"import": "./dist/esm/page-agent-core.js",
"default": "./dist/esm/page-agent-core.js"
}
},
"files": [
"dist/"
],
"description": "GUI agent for web applications - add intelligent automation to any webpage with a single script",
"keywords": [
"ai",
"automation",
"ui-agent",
"GUI-agent",
"browser-automation",
"web-agent",
"llm",
"dom-interaction",
"web-automation",
"GUI-simulation"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git"
},
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"dev:iife": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"chalk": "^5.6.2",
"@page-agent/llms": "1.6.2",
"@page-agent/page-controller": "1.6.2"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"zod": "^4.3.5"
}
}
+641
View File
@@ -0,0 +1,641 @@
/**
* Copyright (C) 2025 Alibaba Group Holding Limited
* Copyright (C) 2026 SimonLuvRamen
* All rights reserved.
*/
import { InvokeError, LLM, type Tool } from '@page-agent/llms'
import type { BrowserState, PageController } from '@page-agent/page-controller'
import chalk from 'chalk'
import * as z from 'zod/v4'
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
import { tools } from './tools'
import type {
AgentActivity,
AgentConfig,
AgentReflection,
AgentStatus,
AgentStepEvent,
ExecutionResult,
HistoricalEvent,
MacroToolInput,
MacroToolResult,
} from './types'
import { assert, fetchLlmsTxt, normalizeResponse, uid, waitFor } from './utils'
export { tool, type PageAgentTool } from './tools'
export type * from './types'
export type PageAgentCoreConfig = AgentConfig & { pageController: PageController }
/**
* AI agent for browser automation.
*
* @remarks
* ## Re-act Agent Loop
* - step
* - observe (gather information about current environment and context)
* - think (LLM calling)
* - reflection (evaluate history, generate memory, short-term planning)
* - action (give the action to approach the next goal)
* - act (execute the action)
* - loop
*
* ## Event System
* - `statuschange` - Agent status transitions (idle → running → completed/error)
* - `historychange` - History events updated (persistent, part of agent memory)
* - `activity` - Real-time activity feedback (transient, for UI only)
* - `dispose` - Agent cleanup triggered
*
* ## Information Streams
* 1. **History Events** (`history` array)
* - Persistent event stream that forms agent's memory
* - Included in LLM context across steps
* - Types: steps, observations, user takeovers, llm errors
*
* 2. **Activity Events** (via `activity` event)
* - Transient UI feedback during task execution
* - NOT included in LLM context
* - Types: thinking, executing, executed, retrying, error
*/
export class PageAgentCore extends EventTarget {
readonly id = uid()
readonly config: PageAgentCoreConfig & { maxSteps: number }
readonly tools: typeof tools
/** PageController for DOM operations */
readonly pageController: PageController
task = ''
taskId = ''
/** History events */
history: HistoricalEvent[] = []
/** Whether this agent has been disposed */
disposed = false
/**
* Callback for when agent needs user input (ask_user tool)
* If not set, ask_user tool will be disabled
* @example onAskUser: (q) => window.prompt(q) || ''
*/
onAskUser?: (question: string) => Promise<string>
#status: AgentStatus = 'idle'
#llm: LLM
#abortController = new AbortController()
#observations: string[] = []
/** internal states during a single task execution */
#states = {
/** Accumulated wait time in seconds */
totalWaitTime: 0,
/** For detecting navigation */
lastURL: '',
/** Browser state */
browserState: null as BrowserState | null,
}
constructor(config: PageAgentCoreConfig) {
super()
this.config = { ...config, maxSteps: config.maxSteps ?? 40 }
this.#llm = new LLM(this.config)
this.tools = new Map(tools)
this.pageController = config.pageController
// Listen to LLM retry events
this.#llm.addEventListener('retry', (e) => {
const { attempt, maxAttempts } = (e as CustomEvent).detail
this.#emitActivity({ type: 'retrying', attempt, maxAttempts })
// Also push to history for panel rendering
this.history.push({
type: 'retry',
message: `LLM retry attempt ${attempt} of ${maxAttempts}`,
attempt,
maxAttempts,
})
this.#emitHistoryChange()
})
this.#llm.addEventListener('error', (e) => {
const error = (e as CustomEvent).detail.error as Error | InvokeError
if ((error as any)?.rawError?.name === 'AbortError') return
const message = String(error)
this.#emitActivity({ type: 'error', message })
// Also push to history for panel rendering
this.history.push({
type: 'error',
message,
rawResponse: (error as InvokeError).rawResponse,
})
this.#emitHistoryChange()
})
if (this.config.customTools) {
for (const [name, tool] of Object.entries(this.config.customTools)) {
if (tool === null) {
this.tools.delete(name)
continue
}
this.tools.set(name, tool)
}
}
if (!this.config.experimentalScriptExecutionTool) {
this.tools.delete('execute_javascript')
}
}
/** Get current agent status */
get status(): AgentStatus {
return this.#status
}
/** Emit statuschange event */
#emitStatusChange(): void {
this.dispatchEvent(new Event('statuschange'))
}
/** Emit historychange event */
#emitHistoryChange(): void {
this.dispatchEvent(new Event('historychange'))
}
/**
* Emit activity event - for transient UI feedback
* @param activity - Current agent activity
*/
#emitActivity(activity: AgentActivity): void {
this.dispatchEvent(new CustomEvent('activity', { detail: activity }))
}
/** Update status and emit event */
#setStatus(status: AgentStatus): void {
if (this.#status !== status) {
this.#status = status
this.#emitStatusChange()
}
}
/**
* Push an observation message to the history event stream.
* This will be visible in <agent_history> and remain persistent in memory across steps.
* @experimental @internal
* @note history change will be emitted before next step starts
*/
pushObservation(content: string): void {
this.#observations.push(content)
}
/** Stop the current task. Agent remains reusable. */
stop() {
this.pageController.cleanUpHighlights()
this.pageController.hideMask()
this.#abortController.abort()
}
async execute(task: string): Promise<ExecutionResult> {
if (this.disposed) throw new Error('PageAgent has been disposed. Create a new instance.')
if (!task) throw new Error('Task is required')
this.task = task
this.taskId = uid()
// Disable ask_user tool if onAskUser is not set
if (!this.onAskUser) {
this.tools.delete('ask_user')
}
const onBeforeStep = this.config.onBeforeStep
const onAfterStep = this.config.onAfterStep
const onBeforeTask = this.config.onBeforeTask
const onAfterTask = this.config.onAfterTask
await onBeforeTask?.(this)
// Show mask
await this.pageController.showMask()
if (this.#abortController) {
this.#abortController.abort()
this.#abortController = new AbortController()
}
this.history = []
this.#setStatus('running')
this.#emitHistoryChange()
this.#observations = []
// Reset internal states
this.#states = { totalWaitTime: 0, lastURL: '', browserState: null }
let step = 0
while (true) {
try {
console.group(`step: ${step}`)
await onBeforeStep?.(this, step)
// observe
console.log(chalk.blue.bold('👀 Observing...'))
this.#states.browserState = await this.pageController.getBrowserState()
await this.#handleObservations(step)
// assemble prompts
const messages = [
{ role: 'system' as const, content: this.#getSystemPrompt() },
{ role: 'user' as const, content: await this.#assembleUserPrompt() },
]
const macroTool = { AgentOutput: this.#packMacroTool() }
// invoke LLM
console.log(chalk.blue.bold('🧠 Thinking...'))
this.#emitActivity({ type: 'thinking' })
const result = await this.#llm.invoke(messages, macroTool, this.#abortController.signal, {
toolChoiceName: 'AgentOutput',
normalizeResponse: (res) => normalizeResponse(res, this.tools),
})
// assemble history
const macroResult = result.toolResult as MacroToolResult
const input = macroResult.input
const output = macroResult.output
const reflection: Partial<AgentReflection> = {
evaluation_previous_goal: input.evaluation_previous_goal,
memory: input.memory,
next_goal: input.next_goal,
}
const actionName = Object.keys(input.action)[0]
const action: AgentStepEvent['action'] = {
name: actionName,
input: input.action[actionName],
output: output,
}
this.history.push({
type: 'step',
stepIndex: step,
reflection,
action,
usage: result.usage,
rawResponse: result.rawResponse,
rawRequest: result.rawRequest,
} as AgentStepEvent)
this.#emitHistoryChange()
//
await onAfterStep?.(this, this.history)
console.groupEnd()
// finish task if done
if (actionName === 'done') {
const success = action.input?.success ?? false
const text = action.input?.text || 'no text provided'
console.log(chalk.green.bold('Task completed'), success, text)
this.#onDone(success)
const result: ExecutionResult = {
success,
data: text,
history: this.history,
}
await onAfterTask?.(this, result)
return result
}
} catch (error: unknown) {
console.groupEnd() // to prevent nested groups
const isAbortError = (error as any)?.rawError?.name === 'AbortError'
console.error('Task failed', error)
const errorMessage = isAbortError ? 'Task stopped' : String(error)
this.#emitActivity({ type: 'error', message: errorMessage })
this.history.push({ type: 'error', message: errorMessage, rawResponse: error })
this.#emitHistoryChange()
this.#onDone(false)
const result: ExecutionResult = {
success: false,
data: errorMessage,
history: this.history,
}
await onAfterTask?.(this, result)
return result
}
step++
if (step > this.config.maxSteps) {
const errorMessage = 'Step count exceeded maximum limit'
this.history.push({ type: 'error', message: errorMessage })
this.#emitHistoryChange()
this.#onDone(false)
const result: ExecutionResult = {
success: false,
data: errorMessage,
history: this.history,
}
await onAfterTask?.(this, result)
return result
}
await waitFor(this.config.stepDelay ?? 0.4)
}
}
/**
* Merge all tools into a single MacroTool with the following input:
* - thinking: string
* - evaluation_previous_goal: string
* - memory: string
* - next_goal: string
* - action: { toolName: toolInput }
* where action must be selected from tools defined in this.tools
*/
#packMacroTool(): Tool<MacroToolInput, MacroToolResult> {
const tools = this.tools
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
return z.object({ [toolName]: tool.inputSchema }).describe(tool.description)
})
const actionSchema = z.union(actionSchemas as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]])
const macroToolSchema = z.object({
// thinking: z.string().optional(),
evaluation_previous_goal: z.string().optional(),
memory: z.string().optional(),
next_goal: z.string().optional(),
action: actionSchema,
})
return {
description: 'You MUST call this tool every step!',
inputSchema: macroToolSchema as z.ZodType<MacroToolInput>,
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
// abort
if (this.#abortController.signal.aborted) throw new Error('AbortError')
console.log(chalk.blue.bold('MacroTool input'), input)
const action = input.action
const toolName = Object.keys(action)[0]
const toolInput = action[toolName]
// Build reflection text, only include non-empty fields
const reflectionLines: string[] = []
if (input.evaluation_previous_goal)
reflectionLines.push(`✅: ${input.evaluation_previous_goal}`)
if (input.memory) reflectionLines.push(`💾: ${input.memory}`)
if (input.next_goal) reflectionLines.push(`🎯: ${input.next_goal}`)
const reflectionText = reflectionLines.length > 0 ? reflectionLines.join('\n') : ''
if (reflectionText) {
console.log(reflectionText)
}
// Find the corresponding tool
const tool = tools.get(toolName)
assert(tool, `Tool ${toolName} not found`)
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
// Emit executing activity
this.#emitActivity({ type: 'executing', tool: toolName, input: toolInput })
const startTime = Date.now()
// Execute tool, bind `this` to PageAgent
const result = await tool.execute.bind(this)(toolInput)
const duration = Date.now() - startTime
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result)
// Emit executed activity
this.#emitActivity({
type: 'executed',
tool: toolName,
input: toolInput,
output: result,
duration,
})
// counting wait time
if (toolName === 'wait') {
this.#states.totalWaitTime += toolInput?.seconds || 0
} else {
this.#states.totalWaitTime = 0
}
// Return structured result
return {
input,
output: result,
}
},
}
}
/**
* Get system prompt, dynamically replace language settings based on configured language
*/
#getSystemPrompt(): string {
if (this.config.customSystemPrompt) {
return this.config.customSystemPrompt
}
const targetLanguage = this.config.language === 'zh-CN' ? '中文' : 'English'
const systemPrompt = SYSTEM_PROMPT.replace(
/Default working language: \*\*.*?\*\*/,
`Default working language: **${targetLanguage}**`
)
return systemPrompt
}
/**
* Get instructions from config
*/
async #getInstructions(): Promise<string> {
const { instructions, experimentalLlmsTxt } = this.config
const systemInstructions = instructions?.system?.trim()
let pageInstructions: string | undefined
const url = this.#states.browserState?.url || ''
if (instructions?.getPageInstructions && url) {
try {
pageInstructions = instructions.getPageInstructions(url)?.trim()
} catch (error) {
console.error(
chalk.red('[PageAgent] Failed to execute getPageInstructions callback:'),
error
)
}
}
const llmsTxt = experimentalLlmsTxt && url ? await fetchLlmsTxt(url) : undefined
if (!systemInstructions && !pageInstructions && !llmsTxt) return ''
let result = '<instructions>\n'
if (systemInstructions) {
result += `<system_instructions>\n${systemInstructions}\n</system_instructions>\n`
}
if (pageInstructions) {
result += `<page_instructions>\n${pageInstructions}\n</page_instructions>\n`
}
if (llmsTxt) {
result += `<llms_txt>\n${llmsTxt}\n</llms_txt>\n`
}
result += '</instructions>\n\n'
return result
}
/**
* Generate system observations before each step
* @todo loop detection
* @todo console error
*/
async #handleObservations(step: number): Promise<void> {
// Accumulated wait time warning
if (this.#states.totalWaitTime >= 3) {
this.pushObservation(
`You have waited ${this.#states.totalWaitTime} seconds accumulatively. ` +
`DO NOT wait any longer unless you have a good reason.`
)
}
// Detect URL change
const currentURL = this.#states.browserState?.url || ''
if (currentURL !== this.#states.lastURL) {
this.pushObservation(`Page navigated to → ${currentURL}`)
this.#states.lastURL = currentURL
await waitFor(0.5) // wait for page to stabilize
}
// Remaining steps warning
const remaining = this.config.maxSteps - step
if (remaining === 5) {
this.pushObservation(
`⚠️ Only ${remaining} steps remaining. ` +
`Consider wrapping up or calling done with partial results.`
)
} else if (remaining === 2) {
this.pushObservation(
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
)
}
// Push observations to history and emit
if (this.#observations.length > 0) {
for (const content of this.#observations) {
this.history.push({ type: 'observation', content })
console.log(chalk.cyan('Observation:'), content)
}
this.#observations = []
this.#emitHistoryChange()
}
}
async #assembleUserPrompt(): Promise<string> {
const browserState = this.#states.browserState!
let prompt = ''
// <instructions> (optional)
prompt += await this.#getInstructions()
// <agent_state>
// - <user_request>
// - <step_info>
// <agent_state>
const stepCount = this.history.filter((e) => e.type === 'step').length
prompt += '<agent_state>\n'
prompt += '<user_request>\n'
prompt += `${this.task}\n`
prompt += '</user_request>\n'
prompt += '<step_info>\n'
prompt += `Step ${stepCount + 1} of ${this.config.maxSteps} max possible steps\n`
prompt += `Current time: ${new Date().toLocaleString()}\n`
prompt += '</step_info>\n'
prompt += '</agent_state>\n\n'
// <agent_history>
// - <step_N> for steps
// - <sys> for observations and system messages
prompt += '<agent_history>\n'
let stepIndex = 0
for (const event of this.history) {
if (event.type === 'step') {
stepIndex++
prompt += `<step_${stepIndex}>\n`
prompt += `Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}\n`
prompt += `Memory: ${event.reflection.memory}\n`
prompt += `Next Goal: ${event.reflection.next_goal}\n`
prompt += `Action Results: ${event.action.output}\n`
prompt += `</step_${stepIndex}>\n`
} else if (event.type === 'observation') {
prompt += `<sys>${event.content}</sys>\n`
} else if (event.type === 'user_takeover') {
prompt += `<sys>User took over control and made changes to the page</sys>\n`
} else if (event.type === 'error') {
// Error events are mainly for panel rendering, not included in LLM context
// to avoid polluting the agent's reasoning with transient errors
}
}
prompt += '</agent_history>\n\n'
// <browser_state>
let pageContent = browserState.content
if (this.config.transformPageContent) {
pageContent = await this.config.transformPageContent(pageContent)
}
prompt += '<browser_state>\n'
prompt += browserState.header + '\n'
prompt += pageContent + '\n'
prompt += browserState.footer + '\n\n'
prompt += '</browser_state>\n\n'
return prompt
}
#onDone(success = true) {
this.pageController.cleanUpHighlights()
this.pageController.hideMask() // No await - fire and forget
this.#setStatus(success ? 'completed' : 'error')
this.#abortController.abort()
}
dispose() {
console.log('Disposing PageAgent...')
this.disposed = true
this.pageController.dispose()
// this.history = []
this.#abortController.abort()
// Emit dispose event for UI cleanup
this.dispatchEvent(new Event('dispose'))
this.config.onDispose?.(this)
}
}
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.md?raw' {
const content: string
export default content
}
@@ -0,0 +1 @@
system_prompt.md
@@ -10,7 +10,7 @@ You excel at following tasks:
</intro> </intro>
<language_settings> <language_settings>
- Default working language: **中文** - Default working language: **English**
- Use the language that user is using. Return in user's language. - Use the language that user is using. Return in user's language.
</language_settings> </language_settings>
@@ -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. - 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). - 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. - 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 expected elements are missing, try scrolling, or navigating back.
- If the page is not fully loaded, use the `wait` action. - 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. - Do not repeat one action for more than 3 times unless some conditions changed.
@@ -87,12 +87,11 @@ Strictly follow these rules while using the browser and navigating the web:
<capability> <capability>
- You can only handle single page app. Do not jump out of current page. - 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. - 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. - 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. - 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 are not clear about the request or steps. `ask_user` to clarify it.
- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps. - If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.
</capability> </capability>
@@ -120,7 +119,7 @@ Exhibit the following reasoning patterns to successfully achieve the <user_reque
- Analyze all relevant items in <agent_history> and <browser_state> to understand your state. - Analyze all relevant items in <agent_history> and <browser_state> to understand your state.
- Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because it appears to be executed in your last step in <agent_history>. If the expected change is missing, mark the last action as failed (or uncertain) and plan a recovery. - Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because it appears to be executed in your last step in <agent_history>. If the expected change is missing, mark the last action as failed (or uncertain) and plan a recovery.
- Analyze whether you are stuck, e.g. when you repeat the same actions multiple times without any progress. Then consider alternative approaches e.g. scrolling for more context or ask user for help. - Analyze whether you are stuck, e.g. when you repeat the same actions multiple times without any progress. Then consider alternative approaches e.g. scrolling for more context or ask user for help.
- `ask_user` for help if you have any difficulty. Users want to be kept in the loop. - Ask user for help if you have any difficulty. Keep user in the loop.
- If you see information relevant to <user_request>, plan saving the information to memory. - If you see information relevant to <user_request>, plan saving the information to memory.
- Always reason about the <user_request>. Make sure to carefully analyze the specific steps and information required. E.g. specific filters, specific form fields, specific information to search. Make sure to always compare the current trajectory with the user request and think carefully if thats how the user requested it. - Always reason about the <user_request>. Make sure to carefully analyze the specific steps and information required. E.g. specific filters, specific form fields, specific information to search. Make sure to always compare the current trajectory with the user request and think carefully if thats how the user requested it.
</reasoning_rules> </reasoning_rules>
@@ -129,7 +128,6 @@ Exhibit the following reasoning patterns to successfully achieve the <user_reque
Here are examples of good output patterns. Use them as reference but never copy them directly. Here are examples of good output patterns. Use them as reference but never copy them directly.
<evaluation_examples> <evaluation_examples>
- Positive Examples:
"evaluation_previous_goal": "Successfully navigated to the product page and found the target information. Verdict: Success" "evaluation_previous_goal": "Successfully navigated to the product page and found the target information. Verdict: Success"
"evaluation_previous_goal": "Clicked the login button and user authentication form appeared. Verdict: Success" "evaluation_previous_goal": "Clicked the login button and user authentication form appeared. Verdict: Success"
</evaluation_examples> </evaluation_examples>
@@ -140,17 +138,16 @@ Here are examples of good output patterns. Use them as reference but never copy
<next_goal_examples> <next_goal_examples>
"next_goal": "Click on the 'Add to Cart' button to proceed with the purchase flow." "next_goal": "Click on the 'Add to Cart' button to proceed with the purchase flow."
"next_goal": "Extract details from the first item on the page."
</next_goal_examples> </next_goal_examples>
</examples> </examples>
<output> <output>
You must ALWAYS respond with a valid JSON in this exact format:
{ {
"evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.", "evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.",
"memory": "1-3 concise sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.", "memory": "1-3 concise sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.",
"next_goal": "State the next immediate goal and action to achieve it, in one clear sentence." "next_goal": "State the next immediate goal and action to achieve it, in one clear sentence.",
"action":{"one_action_name": {// action-specific parameter}} "action":{
"Action name": {// Action parameters}
}
} }
</output> </output>
+189
View File
@@ -0,0 +1,189 @@
/**
* Internal tools for PageAgent.
* @note Adapted from browser-use
*/
import * as z from 'zod/v4'
import type { PageAgentCore } from '../PageAgentCore'
import { waitFor } from '../utils'
/**
* Internal tool definition that has access to PageAgent `this` context
*/
export interface PageAgentTool<TParams = any> {
// name: string
description: string
inputSchema: z.ZodType<TParams>
execute: (this: PageAgentCore, args: TParams) => Promise<string>
}
export function tool<TParams>(options: PageAgentTool<TParams>): PageAgentTool<TParams> {
return options
}
/**
* Internal tools for PageAgent.
* Note: Using any to allow different parameter types for each tool
*/
export const tools = new Map<string, PageAgentTool>()
tools.set(
'done',
tool({
description:
'Complete task. Text is your final response to the user — keep it concise unless the user explicitly asks for detail.',
inputSchema: z.object({
text: z.string(),
success: z.boolean().default(true),
}),
execute: async function (this: PageAgentCore, input) {
// @note main loop will handle this one
return Promise.resolve('Task completed')
},
})
)
tools.set(
'wait',
tool({
description: 'Wait for x seconds. Can be used to wait until the page or data is fully loaded.',
inputSchema: z.object({
seconds: z.number().min(1).max(10).default(1),
}),
execute: async function (this: PageAgentCore, input) {
// try to subtract LLM calling time from the actual wait time
const lastTimeUpdate = await this.pageController.getLastUpdateTime()
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
console.log(`actualWaitTime: ${actualWaitTime} seconds`)
await waitFor(actualWaitTime)
return `✅ Waited for ${input.seconds} seconds.`
},
})
)
tools.set(
'ask_user',
tool({
description:
'Ask the user a question and wait for their answer. Use this if you need more information or clarification.',
inputSchema: z.object({
question: z.string(),
}),
execute: async function (this: PageAgentCore, input) {
if (!this.onAskUser) {
throw new Error('ask_user tool requires onAskUser callback to be set')
}
const answer = await this.onAskUser(input.question)
return `User answered: ${answer}`
},
})
)
tools.set(
'click_element_by_index',
tool({
description: 'Click element by index',
inputSchema: z.object({
index: z.int().min(0),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.clickElement(input.index)
return result.message
},
})
)
tools.set(
'input_text',
tool({
description: 'Click and type text into an interactive input element',
inputSchema: z.object({
index: z.int().min(0),
text: z.string(),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.inputText(input.index, input.text)
return result.message
},
})
)
tools.set(
'select_dropdown_option',
tool({
description:
'Select dropdown option for interactive element index by the text of the option you want to select',
inputSchema: z.object({
index: z.int().min(0),
text: z.string(),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.selectOption(input.index, input.text)
return result.message
},
})
)
/**
* @note Reference from browser-use
*/
tools.set(
'scroll',
tool({
description: 'Scroll the page vertically. Use index for scroll elements (dropdowns/custom UI).',
inputSchema: z.object({
down: z.boolean().default(true),
num_pages: z.number().min(0).max(10).optional().default(0.1),
pixels: z.number().int().min(0).optional(),
index: z.number().int().min(0).optional(),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.scroll({
...input,
numPages: input.num_pages,
})
return result.message
},
})
)
/**
* @todo Tables need a dedicated parser to extract structured data. This tool is useless.
*/
tools.set(
'scroll_horizontally',
tool({
description:
'Scroll the page horizontally, or within a specific element by index. Useful for wide tables.',
inputSchema: z.object({
right: z.boolean().default(true),
pixels: z.number().int().min(0),
index: z.number().int().min(0).optional(),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.scrollHorizontally(input)
return result.message
},
})
)
tools.set(
'execute_javascript',
tool({
description:
'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution!',
inputSchema: z.object({
script: z.string(),
}),
execute: async function (this: PageAgentCore, input) {
const result = await this.pageController.executeJavascript(input.script)
return result.message
},
})
)
// @todo send_keys
// @todo upload_file
// @todo go_back
// @todo extract_structured_data
+289
View File
@@ -0,0 +1,289 @@
import type { LLMConfig } from '@page-agent/llms'
// @note circular dependency but okay
import type { PageAgentCore } from './PageAgentCore'
import type { PageAgentTool } from './tools'
/** Supported UI languages */
export type SupportedLanguage = 'en-US' | 'zh-CN'
export interface AgentConfig extends LLMConfig {
language?: SupportedLanguage
/**
* Maximum number of steps the agent can take per task.
* @default 40
*/
maxSteps?: number
/**
* Custom tools to extend PageAgent capabilities
* @experimental
* @note You can also override or remove internal tools by using the same name.
* @see PageAgentTool
*
* @example
* // override internal tool
* import { z } from 'zod/v4'
* import { tool } from 'page-agent'
* const customTools = {
* ask_user: tool({
* description:
* 'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
* inputSchema: z.object({
* question: z.string(),
* }),
* execute: async function (this: PageAgent, input) {
* const answer = await do_some_thing(input.question)
* return "✅ Received user answer: " + answer
* },
* })
* }
*
* @example
* // remove internal tool
* const customTools = {
* ask_user: null // never ask user questions
* }
*/
customTools?: Record<string, PageAgentTool | null>
/**
* Instructions to guide the agent's behavior
*/
instructions?: {
/**
* Global system-level instructions, applied to all tasks
*/
system?: string
/**
* Dynamic page-level instructions callback
* Called before each step to get instructions for the current page
* @param url - Current page URL (window.location.href)
* @returns Instructions string, or undefined/null to skip
*/
getPageInstructions?: (url: string) => string | undefined | null
}
/**
* Lifecycle hooks for task execution.
* @experimental API may change in future versions.
*
* All hooks receive the agent instance as first parameter.
*/
/**
* Called before each step execution.
* @experimental
* @param agent - The PageAgentCore instance
* @param stepCount - Current step number (0-indexed)
*/
onBeforeStep?: (agent: PageAgentCore, stepCount: number) => Promise<void> | void
/**
* Called after each step execution.
* @experimental
* @param agent - The PageAgentCore instance
* @param history - Current history of events
*/
onAfterStep?: (agent: PageAgentCore, history: HistoricalEvent[]) => Promise<void> | void
/**
* Called before task execution starts.
* @experimental
* @param agent - The PageAgentCore instance
*/
onBeforeTask?: (agent: PageAgentCore) => Promise<void> | void
/**
* Called after task execution completes (success or failure).
* @experimental
* @param agent - The PageAgentCore instance
* @param result - The execution result
*/
onAfterTask?: (agent: PageAgentCore, result: ExecutionResult) => Promise<void> | void
/**
* Called when the agent is disposed.
* @experimental
* @note This hook can block the disposal process if it's async.
* @param agent - The PageAgentCore instance
* @param reason - Optional reason for disposal
*/
onDispose?: (agent: PageAgentCore, reason?: string) => void
// page behavior hooks
/**
* @experimental
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
* @note Can cause unpredictable side effects.
* @note May bypass some safe guards and data-masking mechanisms.
*/
experimentalScriptExecutionTool?: boolean
/**
* @experimental
* Fetch /llms.txt from current site origin and include as context.
* Only fetched once per origin per task.
* @default false
*/
experimentalLlmsTxt?: boolean
/**
* Transform page content before sending to LLM.
* Called after DOM extraction and simplification, before LLM invocation.
* Use cases: inspect extraction results, modify page info, mask sensitive data.
*
* @param content - Simplified page content that will be sent to LLM
* @returns Transformed content
*
* @example
* // Mask phone numbers
* transformPageContent: async (content) => {
* return content.replace(/1[3-9]\d{9}/g, '***********')
* }
*/
transformPageContent?: (content: string) => Promise<string> | string
/**
* Completely override the default system prompt.
* @experimental Use with caution - incorrect prompts may break agent behavior.
*/
customSystemPrompt?: string
/**
* Delay between steps in seconds.
* @default 0.4
*/
stepDelay?: number
}
/**
* Agent reflection state - the reflection-before-action model
*
* Every tool call must first reflect on:
* - evaluation_previous_goal: How well did the previous action achieve its goal?
* - memory: Key information to remember for future steps
* - next_goal: What should be accomplished in the next action?
*/
export interface AgentReflection {
evaluation_previous_goal: string
memory: string
next_goal: string
}
/**
* MacroTool input structure
*
* This is the core abstraction that enforces the "reflection-before-action" mental model.
* Before executing any action, the LLM must output its reasoning state.
*/
export interface MacroToolInput extends Partial<AgentReflection> {
action: Record<string, any>
}
/**
* MacroTool output structure
*/
export interface MacroToolResult {
input: MacroToolInput
output: string
}
/**
* A single agent step with reflection and action
*/
export interface AgentStepEvent {
type: 'step'
stepIndex: number
reflection: Partial<AgentReflection>
action: {
name: string
input: any
output: string
}
usage: {
promptTokens: number
completionTokens: number
totalTokens: number
cachedTokens?: number
reasoningTokens?: number
}
/** Raw LLM response for debugging */
rawResponse?: unknown
/** Raw LLM request for debugging */
rawRequest?: unknown
}
/**
* Persistent observation event (stays in memory)
*/
export interface ObservationEvent {
type: 'observation'
content: string
}
/**
* User takeover event
*/
export interface UserTakeoverEvent {
type: 'user_takeover'
}
/**
* Retry event - LLM call is being retried
*/
export interface RetryEvent {
type: 'retry'
message: string
attempt: number
maxAttempts: number
}
/**
* Error event - fatal error from LLM or execution
*/
export interface AgentErrorEvent {
type: 'error'
message: string
rawResponse?: unknown
}
/**
* Union type for all history events
*/
export type HistoricalEvent =
| AgentStepEvent
| ObservationEvent
| UserTakeoverEvent
| RetryEvent
| AgentErrorEvent
/**
* Agent execution status
*/
export type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
/**
* Agent activity - transient state for immediate UI feedback.
*
* Unlike historical events (which are persisted), activities are ephemeral
* and represent "what the agent is doing right now". UI components should
* listen to 'activity' events to show real-time feedback.
*
* Note: There is no 'idle' activity - absence of activity events means idle.
*/
export type AgentActivity =
| { type: 'thinking' }
| { type: 'executing'; tool: string; input: unknown }
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
| { type: 'retrying'; attempt: number; maxAttempts: number }
| { type: 'error'; message: string }
export interface ExecutionResult {
success: boolean
data: string
history: HistoricalEvent[]
}
+216
View File
@@ -0,0 +1,216 @@
import { InvokeError, InvokeErrorType } from '@page-agent/llms'
import chalk from 'chalk'
import * as z from 'zod/v4'
import type { PageAgentTool } from '../tools'
const log = console.log.bind(console, chalk.yellow('[autoFixer]'))
/**
* Normalize LLM response and fix common format issues.
*
* Handles:
* - No tool_calls but JSON in message.content (fallback)
* - Model returns action name as tool call instead of AgentOutput
* - Arguments wrapped as double JSON string
* - Nested function call format
* - Missing action field (fallback to wait)
* - Primitive action input for single-field tools (e.g. `{"click_element_by_index": 2}`)
* - etc.
*/
export function normalizeResponse(response: any, tools?: Map<string, PageAgentTool>): any {
let resolvedArguments = null as any
const choice = (response as { choices?: Choice[] }).choices?.[0]
if (!choice) throw new Error('No choices in response')
const message = choice.message
if (!message) throw new Error('No message in choice')
const toolCall = message.tool_calls?.[0]
// fix level and location of arguments
if (toolCall?.function?.arguments) {
resolvedArguments = safeJsonParse(toolCall.function.arguments)
// case: sometimes the model only returns the action level
if (toolCall.function.name && toolCall.function.name !== 'AgentOutput') {
log(`#1: fixing tool_call`)
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
}
} else {
// case: sometimes the model returns json in content instead of tool_calls
if (message.content) {
const content = message.content.trim()
const jsonInContent = retrieveJsonFromString(content)
if (jsonInContent) {
resolvedArguments = safeJsonParse(jsonInContent)
// case: sometimes the content json includes upper level wrapper
if (resolvedArguments?.name === 'AgentOutput') {
log(`#2: fixing tool_call`)
resolvedArguments = safeJsonParse(resolvedArguments.arguments)
}
// case: sometimes even 2-levels of wrapping
if (resolvedArguments?.type === 'function') {
log(`#3: fixing tool_call`)
resolvedArguments = safeJsonParse(resolvedArguments.function.arguments)
}
// case: and sometimes action level only
// todo: needs better detection logic
if (
!resolvedArguments?.action &&
!resolvedArguments?.evaluation_previous_goal &&
!resolvedArguments?.memory &&
!resolvedArguments?.next_goal &&
!resolvedArguments?.thinking
) {
log(`#4: fixing tool_call`)
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
}
} else {
throw new Error('No tool_call and the message content does not contain valid JSON')
}
} else {
throw new Error('No tool_call nor message content is present')
}
}
// fix double stringified arguments
resolvedArguments = safeJsonParse(resolvedArguments)
if (resolvedArguments.action) {
resolvedArguments.action = safeJsonParse(resolvedArguments.action)
}
// validate and fix action input using tool schemas
if (resolvedArguments.action && tools) {
resolvedArguments.action = validateAction(resolvedArguments.action, tools)
}
// fix incomplete formats
if (!resolvedArguments.action) {
log(`#5: fixing tool_call`)
resolvedArguments.action = { name: 'wait', input: { seconds: 1 } }
}
// pack back to standard format
return {
...response,
choices: [
{
...choice,
message: {
...message,
tool_calls: [
{
...(toolCall || {}),
function: {
...(toolCall?.function || {}),
name: 'AgentOutput',
arguments: JSON.stringify(resolvedArguments),
},
},
],
},
},
],
}
}
/**
* Validate action against tool schemas. Provides clear error messages
* instead of letting the union schema produce unreadable errors.
*
* Also coerces primitive inputs for single-field tools:
* e.g. `{"click_element_by_index": 2}` → `{"click_element_by_index": {"index": 2}}`
*/
function validateAction(action: any, tools: Map<string, PageAgentTool>): any {
if (typeof action !== 'object' || action === null) return action
const toolName = Object.keys(action)[0]
if (!toolName) return action
const tool = tools.get(toolName)
if (!tool) {
const available = Array.from(tools.keys()).join(', ')
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Unknown action "${toolName}". Available: ${available}`
)
}
let value = action[toolName]
const schema = tool.inputSchema
// coerce primitive input for single-field tools
if (schema instanceof z.ZodObject && value !== null && typeof value !== 'object') {
const requiredKey = Object.keys(schema.shape).find(
(k) => !(schema.shape as Record<string, z.ZodType>)[k].safeParse(undefined).success
)
if (requiredKey) {
log(`coercing primitive action input for "${toolName}"`)
value = { [requiredKey]: value }
}
}
const result = schema.safeParse(value)
if (!result.success) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Invalid input for action "${toolName}": ${z.prettifyError(result.error)}`
)
}
return { [toolName]: result.data }
}
/**
* Safely parse JSON, return original input if not json.
*/
function safeJsonParse(input: any): any {
if (typeof input === 'string') {
try {
return JSON.parse(input.trim())
} catch {
return input
}
}
return input
}
/**
* Extract and parse JSON from a string.
* - Treat content between the first `{` and the last `}` as JSON.
* - Try to parse that content as JSON and return the parsed value (object/array/primitive) if successful, otherwise return null.
*/
function retrieveJsonFromString(str: string): any {
try {
const json = /({[\s\S]*})/.exec(str) ?? []
if (json.length === 0) {
return null
}
return JSON.parse(json[0]!)
} catch {
return null
}
}
interface Choice {
message?: {
role?: 'assistant'
content?: string
tool_calls?: {
id?: string
type?: 'function'
function?: {
name?: string
arguments?: string
}
}[]
}
index?: 0
finish_reason?: 'tool_calls'
}
+111
View File
@@ -0,0 +1,111 @@
import chalk from 'chalk'
export * from './autoFixer'
export async function waitFor(seconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
}
//
export function truncate(text: string, maxLength: number): string {
if (text.length > maxLength) {
return text.substring(0, maxLength) + '...'
}
return text
}
//
export function randomID(existingIDs?: string[]): string {
let id = Math.random().toString(36).substring(2, 11)
if (!existingIDs) {
return id
}
const MAX_TRY = 1000
let tryCount = 0
while (existingIDs.includes(id)) {
id = Math.random().toString(36).substring(2, 11)
tryCount++
if (tryCount > MAX_TRY) {
throw new Error('randomID: too many tries')
}
}
return id
}
//
const _global = globalThis as any
if (!_global.__PAGE_AGENT_IDS__) {
_global.__PAGE_AGENT_IDS__ = []
}
const ids = _global.__PAGE_AGENT_IDS__
/**
* Generate a random ID.
* @note Unique within this window.
*/
export function uid() {
const id = randomID(ids)
ids.push(id)
return id
}
const llmsTxtCache = new Map<string, string | null>()
/** Fetch /llms.txt for a URL's origin. Cached per origin, `null` = tried and not found. */
export async function fetchLlmsTxt(url: string): Promise<string | null> {
let origin: string
try {
origin = new URL(url).origin
} catch {
return null // Invalid URL
}
// about:blank, data:, file:
if (origin === 'null') return null
if (llmsTxtCache.has(origin)) return llmsTxtCache.get(origin)!
const endpoint = `${origin}/llms.txt`
let result: string | null = null
try {
console.log(chalk.gray(`[llms.txt] Fetching ${endpoint}`))
const res = await fetch(endpoint, { signal: AbortSignal.timeout(3000) })
if (res.ok) {
result = await res.text()
console.log(chalk.green(`[llms.txt] Found (${result.length} chars)`))
if (result.length > 1000) {
console.log(chalk.yellow(`[llms.txt] Truncating to 1000 chars`))
result = truncate(result, 1000)
}
} else {
console.debug(chalk.gray(`[llms.txt] ${res.status} for ${endpoint}`))
}
} catch (e) {
console.debug(chalk.gray(`[llms.txt] not found for ${endpoint}`), e)
}
llmsTxtCache.set(origin, result)
return result
}
/**
* Simple assertion function that throws an error if the condition is falsy
* @param condition - The condition to assert
* @param message - Optional error message
* @throws Error if condition is falsy
*/
export function assert(condition: unknown, message?: string, silent?: boolean): asserts condition {
if (!condition) {
const errorMessage = message ?? 'Assertion failed'
if (!silent) console.error(chalk.red(`❌ assert: ${errorMessage}`))
throw new Error(errorMessage)
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist",
"paths": {
//
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"]
}
},
"include": ["**/*.ts"],
"exclude": ["dist", "node_modules"],
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" }
]
}
+45
View File
@@ -0,0 +1,45 @@
// @ts-check
import { dirname, resolve } from 'path'
import dts from 'unplugin-dts/vite'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
// ES Module for NPM Package
export default defineConfig({
clearScreen: false,
plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
],
publicDir: false,
esbuild: {
keepNames: true,
},
build: {
lib: {
entry: resolve(__dirname, 'src/PageAgentCore.ts'),
name: 'PageAgentCore',
fileName: 'page-agent-core',
formats: ['es'],
},
outDir: resolve(__dirname, 'dist', 'esm'),
rollupOptions: {
external: [
'chalk',
'zod',
'zod/v4',
// all the internal packages
/^@page-agent\//,
],
},
minify: false,
sourcemap: true,
cssCodeSplit: true,
},
define: {
'process.env.NODE_ENV': '"production"',
},
})
+2
View File
@@ -0,0 +1,2 @@
.wxt
src/components/ui
+5
View File
@@ -0,0 +1,5 @@
# Privacy Policy for Page Agent Extension
This document has moved. Please see our full **[Terms of Use & Privacy](../../docs/terms-and-privacy.md)**.
Online: https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md
+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/assets/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/lib/hooks"
},
"registries": {
"@magicui": "https://magicui.design/r/{name}.json"
}
}
+226
View File
@@ -0,0 +1,226 @@
# Page Agent Extension API
Integrate the Page Agent extension into your web app and trigger multi-page browser tasks from page JavaScript.
## Installation
### 1. Install the browser extension
Primary channel:
- Chrome Web Store: https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj
Latest updates are often published earlier on:
- GitHub Releases: https://github.com/alibaba/page-agent/releases
### 2. Install type definitions (recommended)
```bash
npm install @page-agent/core --save-dev
```
### 3. Authorization (Token)
The token allows your page JS to call the extension API (`window.PAGE_AGENT_EXT`) and execute multi-page tasks.
Why token-based access is required:
- The extension has broad browser permissions (page access, navigation, multi-tab control).
- If abused, it can harm user privacy and security.
- Users must explicitly provide the token only to applications they trust.
Setup:
1. Open the extension side panel and copy your auth token.
2. Set the token in your page:
```typescript
localStorage.setItem('PageAgentExtUserAuthToken', 'your-token')
```
## Quick Start
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
// Wait for extension injection (up to 1 second)
async function waitForExtension(timeout = 1000): Promise<boolean> {
const start = Date.now()
while (Date.now() - start < timeout) {
if (window.PAGE_AGENT_EXT) return true
await new Promise((r) => setTimeout(r, 100))
}
return false
}
// Usage
if (await waitForExtension()) {
const result = await window.PAGE_AGENT_EXT!.execute('Click the login button', {
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'gpt-5.2',
onStatusChange: (status) => console.log('Status:', status),
onActivity: (activity) => console.log('Activity:', activity),
})
console.log('Result:', result)
}
```
## Global API
After token match, the extension injects APIs into `window`.
### `window.PAGE_AGENT_EXT_VERSION`
Extension version string (for capability checks before using the main API).
### `window.PAGE_AGENT_EXT`
Main namespace object.
#### `PAGE_AGENT_EXT.execute(task, config)`
Execute one agent task.
Parameters:
| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `task` | `string` | Yes | Task description |
| `config` | `ExecuteConfig` | Yes | LLM settings, options, and callbacks |
Returns: `Promise<ExecutionResult>`
#### `PAGE_AGENT_EXT.stop()`
Stop the current task.
## Types
Install `@page-agent/core` for complete types:
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
export interface ExecuteConfig {
baseURL: string
model: string
apiKey?: string
// Include the initial tab where page JS starts. Default: true.
includeInitialTab?: boolean
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
}
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
```
`AgentStatus`
```typescript
type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
```
`AgentActivity`
```typescript
type AgentActivity =
| { type: 'thinking' }
| { type: 'executing'; tool: string; input: unknown }
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
| { type: 'retrying'; attempt: number; maxAttempts: number }
| { type: 'error'; message: string }
```
`HistoricalEvent`
```typescript
type HistoricalEvent =
| { type: 'step'; stepIndex: number; reflection: AgentReflection; action: Action }
| { type: 'observation'; content: string }
| { type: 'user_takeover' }
| { type: 'retry'; message: string; attempt: number; maxAttempts: number }
| { type: 'error'; message: string; rawResponse?: unknown }
```
`ExecutionResult`
```typescript
interface ExecutionResult {
success: boolean
data: string
history: HistoricalEvent[]
}
```
## Usage Examples
### Basic Execution
```typescript
const result = await window.PAGE_AGENT_EXT!.execute(
'Fill in the email field with test@example.com and click Submit',
{
baseURL: 'https://api.openai.com/v1',
apiKey: process.env.OPENAI_API_KEY!,
model: 'gpt-5.2',
includeInitialTab: false, // Optional: exclude current tab
onStatusChange: (status) => console.log(status),
onActivity: (activity) => console.log(activity),
}
)
```
### Stop the Current Task
```typescript
window.PAGE_AGENT_EXT!.stop()
```
## Window Type Declaration
If you are not importing `@page-agent/core`, add:
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
interface ExecuteConfig {
baseURL: string
model: string
apiKey?: string
includeInitialTab?: boolean
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
}
declare global {
interface Window {
PAGE_AGENT_EXT_VERSION?: string
PAGE_AGENT_EXT?: {
version: string
execute: Execute
stop: () => void
}
}
}
```
+50
View File
@@ -0,0 +1,50 @@
{
"name": "@page-agent/ext",
"private": true,
"version": "1.6.2",
"type": "module",
"scripts": {
"dev": "wxt",
"build:ext": "wxt build",
"zip": "wxt zip",
"postinstall": "wxt prepare"
},
"devDependencies": {
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@types/chrome": "^0.1.38",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.1",
"@wxt-dev/module-react": "^1.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"idb": "^8.0.3",
"lucide-react": "^1.0.1",
"motion": "^12.38.0",
"next-themes": "^0.4.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"rough-notation": "^0.5.1",
"simple-icons": "^16.13.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"wxt": "^0.20.20"
},
"dependencies": {
"@page-agent/core": "1.6.2",
"@page-agent/llms": "1.6.2",
"@page-agent/page-controller": "1.6.2",
"@page-agent/ui": "1.6.2",
"ai-motion": "^0.4.8",
"chalk": "^5.6.2"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
}
@@ -0,0 +1,11 @@
{
"extName": {
"message": "Page Agent Ext"
},
"extDescription": {
"message": "AI-powered browser automation assistant. Control web pages with natural language."
},
"extActionTitle": {
"message": "Open Page Agent"
}
}
@@ -0,0 +1,11 @@
{
"extName": {
"message": "Page Agent Ext"
},
"extDescription": {
"message": "AI 驱动的浏览器自动化助手,用自然语言控制网页。"
},
"extActionTitle": {
"message": "打开 Page Agent"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -0,0 +1 @@
system_prompt.md
@@ -0,0 +1,97 @@
import { type AgentConfig, PageAgentCore } from '@page-agent/core'
import { RemotePageController } from './RemotePageController'
import { TabsController } from './TabsController'
import SYSTEM_PROMPT from './system_prompt.md?raw'
import { createTabTools } from './tabTools'
/** Detect user language from browser settings */
function detectLanguage(): 'en-US' | 'zh-CN' {
const lang = navigator.language || navigator.languages?.[0] || 'en-US'
return lang.startsWith('zh') ? 'zh-CN' : 'en-US'
}
/**
* MultiPageAgent
* - use with extension
* - can be used from a side panel or a content script
*/
export class MultiPageAgent extends PageAgentCore {
constructor(config: AgentConfig & { includeInitialTab?: boolean }) {
// multi page controller
const tabsController = new TabsController()
const pageController = new RemotePageController(tabsController)
const customTools = createTabTools(tabsController)
// system prompt - auto-detect language if not specified
const language = config.language ?? detectLanguage()
const targetLanguage = language === 'zh-CN' ? '中文' : 'English'
const systemPrompt = SYSTEM_PROMPT.replace(
/Default working language: \*\*.*?\*\*/,
`Default working language: **${targetLanguage}**`
)
// include initial tab for controlling
const includeInitialTab = config.includeInitialTab ?? true
/**
* When the agent is in side-panel and user closed the side-panel.
* There is no chance for isAgentRunning to be set false.
* (unload event doesn't work well in side panel.)
* (I'm trying not to use long-lived connection because the lifecycle of a sw is hard to predict.)
* This heartbeat mechanism acts as a backup.
*/
let heartBeatInterval: null | number = null
super({
...config,
pageController: pageController as any,
customTools: customTools,
customSystemPrompt: systemPrompt,
onBeforeTask: async (agent) => {
await tabsController.init(agent.task, includeInitialTab)
heartBeatInterval = window.setInterval(() => {
chrome.storage.local.set({
agentHeartbeat: Date.now(),
})
}, 1_000)
await chrome.storage.local.set({
isAgentRunning: true,
})
},
onAfterTask: async () => {
if (heartBeatInterval) {
window.clearInterval(heartBeatInterval)
heartBeatInterval = null
}
await chrome.storage.local.set({
isAgentRunning: false,
})
},
onBeforeStep: async (agent) => {
if (!tabsController.currentTabId) return
// make sure the current tab is loaded before the step starts
await tabsController.waitUntilTabLoaded(tabsController.currentTabId!)
},
onDispose: () => {
if (heartBeatInterval) {
window.clearInterval(heartBeatInterval)
heartBeatInterval = null
}
chrome.storage.local.set({
isAgentRunning: false,
})
tabsController.dispose()
},
})
}
}
@@ -0,0 +1,44 @@
/**
* background logics for RemotePageController
* - redirect messages from RemotePageController(Agent, extension pages) to ContentScript
*/
export function handlePageControlMessage(
message: { type: 'PAGE_CONTROL'; action: string; payload: any; targetTabId: number },
sender: chrome.runtime.MessageSender,
sendResponse: (response: unknown) => void
): true | undefined {
const PREFIX = '[RemotePageController.background]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
const { action, payload, targetTabId } = message
if (action === 'get_my_tab_id') {
debug('get_my_tab_id', sender.tab?.id)
sendResponse({ tabId: sender.tab?.id || null })
return
}
// proxy to content script
chrome.tabs
.sendMessage(targetTabId, {
type: 'PAGE_CONTROL',
action,
payload,
})
.then((result) => {
sendResponse(result)
})
.catch((error) => {
console.error(PREFIX, error)
sendResponse({
success: false,
error: error instanceof Error ? error.message : String(error),
})
})
return true // async response
}
@@ -0,0 +1,132 @@
/**
* content script for RemotePageController
*/
import { PageController } from '@page-agent/page-controller'
export function initPageController() {
let pageController: PageController | null = null
let intervalID: number | null = null
const myTabIdPromise = chrome.runtime
.sendMessage({ type: 'PAGE_CONTROL', action: 'get_my_tab_id' })
.then((response) => {
return (response as { tabId: number | null }).tabId
})
.catch((error) => {
console.error('[RemotePageController.ContentScript]: Failed to get my tab id', error)
return null
})
function getPC(): PageController {
if (!pageController) {
pageController = new PageController({ enableMask: false, viewportExpansion: 400 })
}
return pageController
}
intervalID = window.setInterval(async () => {
const agentHeartbeat = (await chrome.storage.local.get('agentHeartbeat')).agentHeartbeat
const now = Date.now()
const agentInTouch = typeof agentHeartbeat === 'number' && now - agentHeartbeat < 2_000
const isAgentRunning = (await chrome.storage.local.get('isAgentRunning')).isAgentRunning
const currentTabId = (await chrome.storage.local.get('currentTabId')).currentTabId
const shouldShowMask = isAgentRunning && agentInTouch && currentTabId === (await myTabIdPromise)
if (shouldShowMask) {
const pc = getPC()
pc.initMask()
await pc.showMask()
} else {
// await getPC().hideMask()
if (pageController) {
pageController.hideMask()
pageController.cleanUpHighlights()
}
}
if (!isAgentRunning && agentInTouch) {
if (pageController) {
pageController.dispose()
pageController = null
}
}
}, 500)
chrome.runtime.onMessage.addListener((message, sender, sendResponse): true | undefined => {
if (message.type !== 'PAGE_CONTROL') {
// sendResponse({
// success: false,
// error: `[RemotePageController.ContentScript]: Invalid message type: ${message.type}`,
// })
return
}
const { action, payload } = message
const methodName = getMethodName(action)
const pc = getPC() as any
switch (action) {
case 'get_last_update_time':
case 'get_browser_state':
case 'update_tree':
case 'clean_up_highlights':
case 'click_element':
case 'input_text':
case 'select_option':
case 'scroll':
case 'scroll_horizontally':
case 'execute_javascript':
pc[methodName](...(payload || []))
.then((result: any) => sendResponse(result))
.catch((error: any) =>
sendResponse({
success: false,
error: error instanceof Error ? error.message : String(error),
})
)
break
default:
sendResponse({
success: false,
error: `Unknown PAGE_CONTROL action: ${action}`,
})
}
return true
})
}
function getMethodName(action: string): string {
switch (action) {
case 'get_last_update_time':
return 'getLastUpdateTime' as const
case 'get_browser_state':
return 'getBrowserState' as const
case 'update_tree':
return 'updateTree' as const
case 'clean_up_highlights':
return 'cleanUpHighlights' as const
// DOM actions
case 'click_element':
return 'clickElement' as const
case 'input_text':
return 'inputText' as const
case 'select_option':
return 'selectOption' as const
case 'scroll':
return 'scroll' as const
case 'scroll_horizontally':
return 'scrollHorizontally' as const
case 'execute_javascript':
return 'executeJavascript' as const
default:
return action
}
}
@@ -0,0 +1,196 @@
import type { BrowserState } from '@page-agent/page-controller'
import type { TabsController } from './TabsController'
const PREFIX = '[RemotePageController]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
function sendMessage(message: {
type: 'PAGE_CONTROL'
action: string
targetTabId: number
payload?: any
}): Promise<any> {
return chrome.runtime.sendMessage(message).catch((error) => {
console.error(PREFIX, message.action, error)
return null
})
}
/**
* Agent side page controller.
* - live in the agent env (extension page or content script)
* - communicates with remote PageController via sw
*/
export class RemotePageController {
tabsController: TabsController
constructor(tabsController: TabsController) {
this.tabsController = tabsController
}
get currentTabId(): number | null {
return this.tabsController.currentTabId
}
private async getCurrentUrl(): Promise<string> {
if (!this.currentTabId) return ''
const { url } = await this.tabsController.getTabInfo(this.currentTabId)
return url || ''
}
private async getCurrentTitle(): Promise<string> {
if (!this.currentTabId) return ''
const { title } = await this.tabsController.getTabInfo(this.currentTabId)
return title || ''
}
async getLastUpdateTime(): Promise<number> {
if (!this.currentTabId) throw new Error('tabsController not initialized.')
return sendMessage({
type: 'PAGE_CONTROL',
action: 'get_last_update_time',
targetTabId: this.currentTabId,
})
}
async getBrowserState(): Promise<BrowserState> {
let browserState = {} as BrowserState
debug('getBrowserState', this.currentTabId)
const currentUrl = await this.getCurrentUrl()
const currentTitle = await this.getCurrentTitle()
if (!this.currentTabId || !isContentScriptAllowed(currentUrl)) {
browserState = {
url: currentUrl,
title: currentTitle,
header: '',
content: '(empty page. either current page is not readable or not loaded yet.)',
footer: '',
}
} else {
browserState = await sendMessage({
type: 'PAGE_CONTROL',
action: 'get_browser_state',
targetTabId: this.currentTabId,
})
}
const sum = await this.tabsController.summarizeTabs()
browserState.header = sum + '\n\n' + (browserState.header || '')
debug('getBrowserState: success', this.currentTabId, browserState)
return browserState
}
async updateTree(): Promise<void> {
if (!this.currentTabId || !isContentScriptAllowed(await this.getCurrentUrl())) {
return
}
await sendMessage({
type: 'PAGE_CONTROL',
action: 'update_tree',
targetTabId: this.currentTabId,
})
}
async cleanUpHighlights(): Promise<void> {
if (!this.currentTabId || !isContentScriptAllowed(await this.getCurrentUrl())) {
return
}
await sendMessage({
type: 'PAGE_CONTROL',
action: 'clean_up_highlights',
targetTabId: this.currentTabId,
})
}
async clickElement(...args: any[]): Promise<DomActionReturn> {
const res = await this.remoteCallDomAction('click_element', args)
// @note may cause page navigation, wait for 1 second to ensure the page loading started
await new Promise((resolve) => setTimeout(resolve, 1000))
return res
}
async inputText(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('input_text', args)
}
async selectOption(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('select_option', args)
}
async scroll(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('scroll', args)
}
async scrollHorizontally(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('scroll_horizontally', args)
}
async executeJavascript(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('execute_javascript', args)
}
/** @note Managed by content script via storage polling. */
async showMask(): Promise<void> {}
/** @note Managed by content script via storage polling. */
async hideMask(): Promise<void> {}
/** @note Managed by content script via storage polling. */
dispose(): void {}
private async remoteCallDomAction(action: string, payload: any[]): Promise<DomActionReturn> {
if (!this.currentTabId) {
return { success: false, message: 'RemotePageController not initialized.' }
}
if (!isContentScriptAllowed(await this.getCurrentUrl())) {
return {
success: false,
message:
'Operation not allowed on this page. Use open_new_tab to navigate to a web page first.',
}
}
return sendMessage({
type: 'PAGE_CONTROL',
action: action,
targetTabId: this.currentTabId!,
payload,
})
}
}
interface DomActionReturn {
success: boolean
message: string
}
/**
* Check if a URL can run content scripts.
*/
export function isContentScriptAllowed(url: string | undefined): boolean {
if (!url) return false
const restrictedPatterns = [
/^chrome:\/\//,
/^chrome-extension:\/\//,
/^about:/,
/^edge:\/\//,
/^brave:\/\//,
/^opera:\/\//,
/^vivaldi:\/\//,
/^file:\/\//,
/^view-source:/,
/^devtools:\/\//,
]
return !restrictedPatterns.some((pattern) => pattern.test(url))
}
@@ -0,0 +1,160 @@
/**
* background logics for TabsController
*/
import type { TabAction } from './TabsController'
const PREFIX = '[TabsController.background]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
export function handleTabControlMessage(
message: { type: 'TAB_CONTROL'; action: TabAction; payload: any },
sender: chrome.runtime.MessageSender,
sendResponse: (response: unknown) => void
): true | undefined {
const { action, payload } = message
switch (action as TabAction) {
case 'get_active_tab': {
debug('get_active_tab')
chrome.tabs
.query({ active: true, currentWindow: true })
.then((tabs) => {
const tabId = tabs.length > 0 ? tabs[0].id || null : null
debug('get_active_tab: success', tabId)
sendResponse({ success: true, tabId })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
case 'get_tab_info': {
debug('get_tab_info', payload)
chrome.tabs
.get(payload.tabId)
.then((tab) => {
debug('get_tab_info: success', tab)
sendResponse(tab)
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
case 'open_new_tab': {
debug('open_new_tab', payload)
chrome.tabs
.create({ url: payload.url, active: false })
.then((newTab) => {
debug('open_new_tab: success', newTab)
sendResponse({ success: true, tabId: newTab.id })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
case 'create_tab_group': {
debug('create_tab_group', payload)
chrome.tabs
.group({ tabIds: payload.tabIds })
.then((groupId) => {
debug('create_tab_group: success', groupId)
sendResponse({ success: true, groupId })
})
.catch((error) => {
console.error(PREFIX, 'Failed to create tab group', error)
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
case 'update_tab_group': {
debug('update_tab_group', payload)
chrome.tabGroups
.update(payload.groupId, payload.properties)
.then(() => {
sendResponse({ success: true })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
case 'add_tab_to_group': {
debug('add_tab_to_group', payload)
chrome.tabs
.group({ tabIds: payload.tabId, groupId: payload.groupId })
.then(() => {
sendResponse({ success: true })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
case 'close_tab': {
debug('close_tab', payload)
chrome.tabs
.remove(payload.tabId)
.then(() => {
sendResponse({ success: true })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
default:
sendResponse({ error: `Unknown action: ${action}` })
return
}
}
export function setupTabChangeEvents() {
console.log('[TabsController.background] setupTabChangeEvents')
chrome.tabs.onCreated.addListener((tab) => {
debug('onCreated', tab)
chrome.runtime
.sendMessage({ type: 'TAB_CHANGE', action: 'created', payload: { tab } })
.catch((error) => {
debug('onCreated error:', error)
})
})
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
debug('onRemoved', tabId, removeInfo)
chrome.runtime
.sendMessage({
type: 'TAB_CHANGE',
action: 'removed',
payload: { tabId, removeInfo },
})
.catch((error) => {
debug('onRemoved error:', error)
})
})
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
debug('onUpdated', tabId, changeInfo)
chrome.runtime
.sendMessage({
type: 'TAB_CHANGE',
action: 'updated',
payload: { tabId, changeInfo, tab },
})
.catch((error) => {
debug('onUpdated error:', error)
})
})
}
@@ -0,0 +1,350 @@
import { isContentScriptAllowed } from './RemotePageController'
const PREFIX = '[TabsController]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
function sendMessage(message: {
type: 'TAB_CONTROL'
action: TabAction
payload?: any
}): Promise<any> {
return chrome.runtime.sendMessage(message).catch((error) => {
console.error(PREFIX, message.action, error)
return null
})
}
/**
* Controller for managing browser tabs.
* - live in the agent env (extension page or content script)
* - no chrome apis. call sw for tab operations
*/
export class TabsController extends EventTarget {
currentTabId: number | null = null
private tabs: TabMeta[] = []
private initialTabId: number | null = null
private tabGroupId: number | null = null
private task: string = ''
async init(task: string, includeInitialTab: boolean = true) {
debug('init', task, includeInitialTab)
this.task = task
this.tabs = []
this.currentTabId = null
this.tabGroupId = null
this.initialTabId = null
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_active_tab',
})
this.initialTabId = result.tabId
if (!this.initialTabId) {
throw new Error('Failed to get initial tab ID')
}
if (includeInitialTab) {
const info = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_tab_info',
payload: { tabId: this.initialTabId },
})
if (isContentScriptAllowed(info.url)) {
this.currentTabId = this.initialTabId
this.tabs.push({
id: result.tabId,
isInitial: true,
url: info.url,
title: info.title,
status: info.status,
})
await this.createTabGroup([this.initialTabId])
}
}
await this.updateCurrentTabId(this.currentTabId)
const tabChangeHandler = (message: any): void => {
if (message.type !== 'TAB_CHANGE') {
// throw new Error(`[TabsController]: Invalid message type: ${message.type}`)
return
}
if (message.action === 'created') {
const tab = message.payload.tab as chrome.tabs.Tab
if (tab.groupId === this.tabGroupId && tab.id != null) {
// Tab created in our controlled group
if (!this.tabs.find((t) => t.id === tab.id)) {
this.tabs.push({ id: tab.id, isInitial: false })
}
this.switchToTab(tab.id)
}
} else if (message.action === 'removed') {
const { tabId } = message.payload as { tabId: number }
const targetTab = this.tabs.find((t) => t.id === tabId)
if (targetTab) {
this.tabs = this.tabs.filter((t) => t.id !== tabId)
if (this.currentTabId === tabId) {
const newCurrentTab = this.tabs[this.tabs.length - 1] || null
if (newCurrentTab) {
this.switchToTab(newCurrentTab.id)
} else {
this.updateCurrentTabId(null)
}
}
}
} else if (message.action === 'updated') {
const { tabId, tab } = message.payload as { tabId: number; tab: chrome.tabs.Tab }
const targetTab = this.tabs.find((t) => t.id === tabId)
if (targetTab) {
targetTab.url = tab.url
targetTab.title = tab.title
targetTab.status = tab.status
}
}
}
chrome.runtime.onMessage.addListener(tabChangeHandler)
this.addEventListener('dispose', () => {
chrome.runtime.onMessage.removeListener(tabChangeHandler)
})
}
async openNewTab(url: string): Promise<string> {
debug('openNewTab', url)
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'open_new_tab',
payload: { url },
})
if (!result.success) {
throw new Error(`Failed to open new tab: ${result.error}`)
}
const tabId = result.tabId as number
this.tabs.push({
id: tabId,
isInitial: false,
})
await this.switchToTab(tabId)
if (!this.tabGroupId) {
await this.createTabGroup([tabId])
} else {
await sendMessage({
type: 'TAB_CONTROL',
action: 'add_tab_to_group',
payload: { tabId: result.tabId, groupId: this.tabGroupId },
})
}
await this.waitUntilTabLoaded(tabId)
return `✅ Opened new tab ID ${tabId} with URL ${url}`
}
async switchToTab(tabId: number): Promise<string> {
debug('switchToTab', tabId)
const targetTab = this.tabs.find((t) => t.id === tabId)
if (!targetTab) {
throw new Error(`Tab ID ${tabId} not found in tab list.`)
}
await this.updateCurrentTabId(tabId)
return `✅ Switched to tab ID ${tabId}.`
}
async closeTab(tabId: number): Promise<string> {
debug('closeTab', tabId)
const targetTab = this.tabs.find((t) => t.id === tabId)
if (!targetTab) {
throw new Error(`Tab ID ${tabId} not found in tab list.`)
}
if (targetTab.isInitial) {
throw new Error(`Cannot close the initial tab ID ${tabId}.`)
}
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'close_tab',
payload: { tabId },
})
if (result.success) {
this.tabs = this.tabs.filter((t) => t.id !== tabId)
if (this.currentTabId === tabId) {
const newCurrentTab = this.tabs[this.tabs.length - 1] || null
if (newCurrentTab) {
await this.switchToTab(newCurrentTab.id)
} else {
await this.updateCurrentTabId(null)
}
}
return `✅ Closed tab ID ${tabId}.`
} else {
throw new Error(`Failed to close tab ID ${tabId}: ${result.error}`)
}
}
private async createTabGroup(tabIds: number[]) {
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'create_tab_group',
payload: { tabIds },
})
if (!result?.success) {
throw new Error(`Failed to create tab group: ${result?.error}`)
}
this.tabGroupId = result.groupId as number
await sendMessage({
type: 'TAB_CONTROL',
action: 'update_tab_group',
payload: {
groupId: this.tabGroupId,
properties: {
title: `PageAgent(${this.task})`,
color: randomColor(),
collapsed: false,
},
},
})
}
async updateCurrentTabId(tabId: number | null) {
debug('updateCurrentTabId', tabId)
this.currentTabId = tabId
await chrome.storage.local.set({ currentTabId: tabId })
}
async getTabInfo(tabId: number): Promise<{ title: string; url: string }> {
// use cached tab info if available
const tabMeta = this.tabs.find((t) => t.id === tabId)
if (tabMeta && tabMeta.url && tabMeta.title) {
return { title: tabMeta.title, url: tabMeta.url }
}
// otherwise, pull the latest tab info from the background script
debug('getTabInfo: pulling from background script', tabId)
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_tab_info',
payload: { tabId },
})
if (tabMeta) {
tabMeta.url = result.url
tabMeta.title = result.title
}
return result
}
async summarizeTabs(): Promise<string> {
const summaries = [`| Tab ID | URL | Title | Current |`, `|-----|-----|-----|-----|`]
for (const tab of this.tabs) {
const { title, url } = await this.getTabInfo(tab.id)
summaries.push(
`| ${tab.id} | ${url} | ${title} | ${this.currentTabId === tab.id ? '✅' : ''} |`
)
}
if (!this.tabs.length) {
summaries.push('\nNo tabs available. Open a tab if needed.')
}
return summaries.join('\n')
}
async waitUntilTabLoaded(tabId: number): Promise<void> {
const tab = this.tabs.find((t) => t.id === tabId)
if (!tab) throw new Error(`Tab ID ${tabId} not found in tab list.`)
if (tab.status === 'unloaded') throw new Error(`Tab ID ${tabId} is unloaded.`)
if (tab.status === 'complete') return
debug('waitUntilTabLoaded', tabId)
await waitUntil(() => tab.status === 'complete', 4_000)
}
dispose() {
this.dispatchEvent(new Event('dispose'))
}
}
export type TabAction =
| 'get_active_tab'
| 'get_tab_info'
| 'open_new_tab'
| 'create_tab_group'
| 'update_tab_group'
| 'add_tab_to_group'
| 'close_tab'
| 'get_tab_title'
interface TabMeta {
id: number
isInitial: boolean
url?: string
title?: string
status?: 'loading' | 'unloaded' | 'complete'
}
const TAB_GROUP_COLORS = ['blue', 'red', 'yellow', 'green', 'pink', 'purple', 'cyan'] as const
type TabGroupColor = (typeof TAB_GROUP_COLORS)[number]
function randomColor(): TabGroupColor {
return TAB_GROUP_COLORS[Math.floor(Math.random() * TAB_GROUP_COLORS.length)]
}
/**
* Wait until condition becomes true
* @returns Returns when condition becomes true, throws otherwise
* @param timeoutMS Timeout in milliseconds, default 1 minutes, throws error on timeout
* @param error Error object to reject on timeout. If not provided, will resolve with false
*/
export async function waitUntil(
check: () => boolean | Promise<boolean>,
timeoutMS = 60_000,
error?: string
): Promise<boolean> {
if (await check()) return true
return new Promise((resolve, reject) => {
const start = Date.now()
const poll = async () => {
if (await check()) return resolve(true)
if (Date.now() - start > timeoutMS) {
if (error) {
return reject(new Error(error))
} else {
return resolve(false)
}
}
setTimeout(poll, 100)
}
setTimeout(poll, 100)
})
}
+30
View File
@@ -0,0 +1,30 @@
import type { LLMConfig } from '@page-agent/llms'
// Demo LLM for testing
export const DEMO_MODEL = 'qwen3.5-plus'
export const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
// export const DEMO_API_KEY = 'NA'
export const DEMO_CONFIG: LLMConfig = {
baseURL: DEMO_BASE_URL,
model: DEMO_MODEL,
// apiKey: DEMO_API_KEY,
}
/** Legacy testing endpoints that should be auto-migrated to DEMO_BASE_URL */
export const LEGACY_TESTING_ENDPOINTS = [
'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy',
]
export function isTestingEndpoint(url: string): boolean {
const normalized = url.replace(/\/+$/, '')
return normalized === DEMO_BASE_URL || LEGACY_TESTING_ENDPOINTS.some((ep) => normalized === ep)
}
export function migrateLegacyEndpoint(config: LLMConfig): LLMConfig {
const normalized = config.baseURL.replace(/\/+$/, '')
if (LEGACY_TESTING_ENDPOINTS.some((ep) => normalized === ep)) {
return { ...DEMO_CONFIG }
}
return config
}
@@ -0,0 +1,145 @@
You are an AI agent designed to operate in an iterative loop to automate browser tasks. Your ultimate goal is accomplishing the task provided in <user_request>.
<intro>
You excel at following tasks:
1. Navigating complex websites and extracting precise information
2. Automating form submissions and interactive web actions
3. Gathering and saving information
4. Operate effectively in an agent loop
5. Efficiently performing diverse web tasks
</intro>
<language_settings>
- Default working language: **English**
- Use the language that user is using. Return in user's language.
</language_settings>
<input>
At every step, your input will consist of:
1. <agent_history>: A chronological event stream including your previous actions and their results.
2. <agent_state>: Current <user_request> and <step_info>.
3. <browser_state>: Tabs, Current Tab, Current URL, interactive elements indexed for actions, and visible page content.
</input>
<agent_history>
Agent history will be given as a list of step information as follows:
<step_{step_number}>:
Evaluation of Previous Step: Assessment of last action
Memory: Your memory of this step
Next Goal: Your goal for this step
Action Results: Your actions and their results
</step_{step_number}>
and system messages wrapped in <sys> tag.
</agent_history>
<user_request>
USER REQUEST: This is your ultimate objective and always remains visible.
- This has the highest priority. Make the user happy.
- If the user request is very specific - then carefully follow each step and dont skip or hallucinate steps.
- If the task is open ended you can plan yourself how to get it done.
</user_request>
<browser_state>
1. Browser State will be given as:
Open Tabs: Open tabs with their ids.
Current Tab: The tab you are currently viewing.
Current URL: URL of the page you are currently viewing.
Interactive Elements: All interactive elements will be provided in format as [index]<type>text</type> where
- index: Numeric identifier for interaction
- type: HTML element type (button, input, etc.)
- text: Element description
Examples:
[33]<div>User form</div>
\t*[35]<button aria-label='Submit form'>Submit</button>
Note that:
- Only elements with numeric indexes in [] are interactive
- (stacked) indentation (with \t) is important and means that the element is a (html) child of the element above (with a lower index)
- Elements tagged with `*[` are the new clickable elements that appeared on the website since the last step - if url has not changed.
- Pure text elements without [] are not interactive.
</browser_state>
<browser_rules>
Strictly follow these rules while using the browser and navigating the web:
- Only interact with elements that have a numeric [index] assigned.
- Only use indexes that are explicitly provided.
- If the page changes after, for example, an input text action, analyze if you need to interact with new elements, e.g. selecting the right option from the list.
- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.
- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).
- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.
- If a captcha appears, tell user you can not solve captcha. Finish the task and ask user to solve it.
- If expected elements are missing, try scrolling, or navigating back.
- If the page is not fully loaded, use the `wait` action.
- Do not repeat one action for more than 3 times unless some conditions changed.
- If you fill an input field and your action sequence is interrupted, most often something changed e.g. suggestions popped up under the field.
- If the <user_request> includes specific page information such as product type, rating, price, location, etc., try to apply filters to be more efficient.
- The <user_request> is the ultimate goal. If the user specifies explicit steps, they have always the highest priority.
- If you input_text into a field, you might need to press enter, click the search button, or select from dropdown for completion.
- Don't login into a page if you don't have to. Don't login if you don't have the credentials.
- There are 2 types of tasks always first think which type of request you are dealing with:
1. Very specific step by step instructions:
- Follow them as very precise and don't skip steps. Try to complete everything as requested.
2. Open ended tasks. Plan yourself, be creative in achieving them.
- If you get stuck e.g. with logins or captcha in open-ended tasks you can re-evaluate the task and try alternative ways, e.g. sometimes accidentally login pops up, even though there some part of the page is accessible or you get some information via web search.
</browser_rules>
<task_completion_rules>
You must call the `done` action in one of three cases:
- When you have fully completed the USER REQUEST.
- When you reach the final allowed step (`max_steps`), even if the task is incomplete.
- When you feel stuck or unable to solve user request. Or user request is not clear or contains inappropriate content.
- When it is ABSOLUTELY IMPOSSIBLE to continue.
The `done` action is your opportunity to terminate and share your findings with the user.
- Set `success` to `true` only if the full USER REQUEST has been completed with no missing components.
- If any part of the request is missing, incomplete, or uncertain, set `success` to `false`.
- You can use the `text` field of the `done` action to communicate your findings and to provide a coherent reply to the user and fulfill the USER REQUEST.
- You are ONLY ALLOWED to call `done` as a single action. Don't call it together with other actions.
- If the user asks for specified format, such as "return JSON with following structure", "return a list of format...", MAKE sure to use the right format in your answer.
- If the user asks for a structured output, your `done` action's schema may be modified. Take this schema into account when solving the task!
</task_completion_rules>
<reasoning_rules>
Exhibit the following reasoning patterns to successfully achieve the <user_request>:
- Reason about <agent_history> to track progress and context toward <user_request>.
- Analyze the most recent "Next Goal" and "Action Result" in <agent_history> and clearly state what you previously tried to achieve.
- Analyze all relevant items in <agent_history> and <browser_state> to understand your state.
- Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because it appears to be executed in your last step in <agent_history>. If the expected change is missing, mark the last action as failed (or uncertain) and plan a recovery.
- Analyze whether you are stuck, e.g. when you repeat the same actions multiple times without any progress. Then consider alternative approaches e.g. scrolling for more context or ask user for help.
- Ask user for help if you have any difficulty. Keep user in the loop.
- If you see information relevant to <user_request>, plan saving the information to memory.
- Always reason about the <user_request>. Make sure to carefully analyze the specific steps and information required. E.g. specific filters, specific form fields, specific information to search. Make sure to always compare the current trajectory with the user request and think carefully if thats how the user requested it.
</reasoning_rules>
<examples>
Here are examples of good output patterns. Use them as reference but never copy them directly.
<evaluation_examples>
"evaluation_previous_goal": "Successfully navigated to the product page and found the target information. Verdict: Success"
"evaluation_previous_goal": "Clicked the login button and user authentication form appeared. Verdict: Success"
</evaluation_examples>
<memory_examples>
"memory": "Found many pending reports that need to be analyzed in the main page. Successfully processed the first 2 reports on quarterly sales data and moving on to inventory analysis and customer feedback reports."
</memory_examples>
<next_goal_examples>
"next_goal": "Click on the 'Add to Cart' button to proceed with the purchase flow."
</next_goal_examples>
</examples>
<output>
{
"evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.",
"memory": "1-3 concise sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.",
"next_goal": "State the next immediate goal and action to achieve it, in one clear sentence.",
"action":{
"Action name": {// Action parameters}
}
}
</output>
+74
View File
@@ -0,0 +1,74 @@
/**
* Tab control tools for browser extension
*
* These tools allow the agent to manage multiple browser tabs:
* - open_new_tab: Open a new tab and set it as current
* - switch_to_tab: Switch to an existing tab
* - close_tab: Close a tab (optionally switch to another)
*/
import * as z from 'zod/v4'
import type { TabsController } from './TabsController'
/** Tool definition compatible with PageAgentCore customTools */
interface TabTool {
description: string
inputSchema: z.ZodType
execute: (input: unknown) => Promise<string>
}
/**
* Create tab control tools bound to a TabsManager instance.
* These tools are injected into PageAgentCore via customTools config.
*/
export function createTabTools(tabsController: TabsController): Record<string, TabTool> {
return {
open_new_tab: {
description:
'Open a new browser tab with the specified URL. The new tab becomes the current tab for all subsequent page operations.',
inputSchema: z.object({
url: z.string().describe('The URL to open in the new tab'),
}),
execute: async (input: unknown) => {
const { url } = input as { url: string }
try {
return await tabsController.openNewTab(url)
} catch (error) {
return `❌ Failed: ${error instanceof Error ? error.message : String(error)}`
}
},
},
switch_to_tab: {
description:
'Switch to an existing tab by its ID. After switching, all page operations will target the new current tab. You can only switch to tabs in the tab list shown in browser state.',
inputSchema: z.object({
tab_id: z.number().int().describe('The tab ID to switch to'),
}),
execute: async (input: unknown) => {
const { tab_id } = input as { tab_id: number }
try {
return await tabsController.switchToTab(tab_id)
} catch (error) {
return `❌ Failed: ${error instanceof Error ? error.message : String(error)}`
}
},
},
close_tab: {
description:
'Close a tab by its ID. Cannot close the initial tab. Optionally specify which tab to switch to after closing.',
inputSchema: z.object({
tab_id: z.number().int().describe('The tab ID to close'),
}),
execute: async (input: unknown) => {
const { tab_id } = input as { tab_id: number }
try {
return await tabsController.closeTab(tab_id)
} catch (error) {
return `❌ Failed: ${error instanceof Error ? error.message : String(error)}`
}
},
},
}
}
+159
View File
@@ -0,0 +1,159 @@
/**
* React hook for using AgentController
*/
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
SupportedLanguage,
} from '@page-agent/core'
import type { LLMConfig } from '@page-agent/llms'
import { useCallback, useEffect, useRef, useState } from 'react'
import { MultiPageAgent } from './MultiPageAgent'
import { DEMO_CONFIG, migrateLegacyEndpoint } from './constants'
/** Language preference: undefined means follow system */
export type LanguagePreference = SupportedLanguage | undefined
export interface AdvancedConfig {
maxSteps?: number
systemInstruction?: string
experimentalLlmsTxt?: boolean
disableNamedToolChoice?: boolean
}
export interface ExtConfig extends LLMConfig, AdvancedConfig {
language?: LanguagePreference
}
export interface UseAgentResult {
status: AgentStatus
history: HistoricalEvent[]
activity: AgentActivity | null
currentTask: string
config: ExtConfig | null
execute: (task: string) => Promise<ExecutionResult>
stop: () => void
configure: (config: ExtConfig) => Promise<void>
}
export function useAgent(): UseAgentResult {
const agentRef = useRef<MultiPageAgent | null>(null)
const [status, setStatus] = useState<AgentStatus>('idle')
const [history, setHistory] = useState<HistoricalEvent[]>([])
const [activity, setActivity] = useState<AgentActivity | null>(null)
const [currentTask, setCurrentTask] = useState('')
const [config, setConfig] = useState<ExtConfig | null>(null)
useEffect(() => {
chrome.storage.local.get(['llmConfig', 'language', 'advancedConfig']).then((result) => {
let llmConfig = (result.llmConfig as LLMConfig) ?? DEMO_CONFIG
const language = (result.language as SupportedLanguage) || undefined
const advancedConfig = (result.advancedConfig as AdvancedConfig) ?? {}
// Auto-migrate legacy testing endpoints
const migrated = migrateLegacyEndpoint(llmConfig)
if (migrated !== llmConfig) {
llmConfig = migrated
chrome.storage.local.set({ llmConfig: migrated })
} else if (!result.llmConfig) {
chrome.storage.local.set({ llmConfig: DEMO_CONFIG })
}
setConfig({ ...llmConfig, ...advancedConfig, language })
})
}, [])
useEffect(() => {
if (!config) return
const { systemInstruction, ...agentConfig } = config
const agent = new MultiPageAgent({
...agentConfig,
instructions: systemInstruction ? { system: systemInstruction } : undefined,
})
agentRef.current = agent
const handleStatusChange = (e: Event) => {
const newStatus = agent.status as AgentStatus
setStatus(newStatus)
if (newStatus === 'idle' || newStatus === 'completed' || newStatus === 'error') {
setActivity(null)
}
}
const handleHistoryChange = (e: Event) => {
setHistory([...agent.history])
}
const handleActivity = (e: Event) => {
const newActivity = (e as CustomEvent).detail as AgentActivity
setActivity(newActivity)
}
agent.addEventListener('statuschange', handleStatusChange)
agent.addEventListener('historychange', handleHistoryChange)
agent.addEventListener('activity', handleActivity)
return () => {
agent.removeEventListener('statuschange', handleStatusChange)
agent.removeEventListener('historychange', handleHistoryChange)
agent.removeEventListener('activity', handleActivity)
agent.dispose()
}
}, [config])
const execute = useCallback(async (task: string) => {
const agent = agentRef.current
console.log('🚀 [useAgent] start executing task:', task)
if (!agent) throw new Error('Agent not initialized')
setCurrentTask(task)
setHistory([])
return agent.execute(task)
}, [])
const stop = useCallback(() => {
agentRef.current?.stop()
}, [])
const configure = useCallback(
async ({
language,
maxSteps,
systemInstruction,
experimentalLlmsTxt,
disableNamedToolChoice,
...llmConfig
}: ExtConfig) => {
await chrome.storage.local.set({ llmConfig })
if (language) {
await chrome.storage.local.set({ language })
} else {
await chrome.storage.local.remove('language')
}
const advancedConfig: AdvancedConfig = {
maxSteps,
systemInstruction,
experimentalLlmsTxt,
disableNamedToolChoice,
}
await chrome.storage.local.set({ advancedConfig })
setConfig({ ...llmConfig, ...advancedConfig, language })
},
[]
)
return {
status,
history,
activity,
currentTask,
config,
execute,
stop,
configure,
}
}
+158
View File
@@ -0,0 +1,158 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.19 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-blink-cursor: blink-cursor 1.2s step-end infinite;
@keyframes blink-cursor {
0%,
49% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
}
}
@keyframes glow-a {
0%,
100% {
opacity: 0.45;
transform: scale(1);
}
50% {
opacity: 0;
transform: scale(1.1);
}
}
@keyframes glow-b {
0%,
100% {
opacity: 0;
transform: scale(1.1);
}
50% {
opacity: 0.45;
transform: scale(1);
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -0,0 +1,363 @@
import {
Copy,
CornerUpLeft,
ExternalLink,
Eye,
EyeOff,
FoldVertical,
HatGlasses,
Home,
Loader2,
Scale,
UnfoldVertical,
} from 'lucide-react'
import { useEffect, useState } from 'react'
import { siGithub } from 'simple-icons'
import { DEMO_BASE_URL, DEMO_MODEL, isTestingEndpoint } from '@/agent/constants'
import type { ExtConfig, LanguagePreference } from '@/agent/useAgent'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
interface ConfigPanelProps {
config: ExtConfig | null
onSave: (config: ExtConfig) => Promise<void>
onClose: () => void
}
export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL)
const [model, setModel] = useState(config?.model || DEMO_MODEL)
const [apiKey, setApiKey] = useState(config?.apiKey)
const [language, setLanguage] = useState<LanguagePreference>(config?.language)
const [maxSteps, setMaxSteps] = useState(config?.maxSteps)
const [systemInstruction, setSystemInstruction] = useState(config?.systemInstruction ?? '')
const [experimentalLlmsTxt, setExperimentalLlmsTxt] = useState(
config?.experimentalLlmsTxt ?? false
)
const [disableNamedToolChoice, setDisableNamedToolChoice] = useState(
config?.disableNamedToolChoice ?? false
)
const [advancedOpen, setAdvancedOpen] = useState(false)
const [saving, setSaving] = useState(false)
const [userAuthToken, setUserAuthToken] = useState('')
const [copied, setCopied] = useState(false)
const [showToken, setShowToken] = useState(false)
const [showApiKey, setShowApiKey] = useState(false)
useEffect(() => {
setBaseURL(config?.baseURL || DEMO_BASE_URL)
setModel(config?.model || DEMO_MODEL)
setApiKey(config?.apiKey)
setLanguage(config?.language)
setMaxSteps(config?.maxSteps)
setSystemInstruction(config?.systemInstruction ?? '')
setExperimentalLlmsTxt(config?.experimentalLlmsTxt ?? false)
setDisableNamedToolChoice(config?.disableNamedToolChoice ?? false)
}, [config])
// Poll for user auth token every second until found
useEffect(() => {
let interval: NodeJS.Timeout | null = null
const fetchToken = async () => {
const result = await chrome.storage.local.get('PageAgentExtUserAuthToken')
const token = result.PageAgentExtUserAuthToken
if (typeof token === 'string' && token) {
setUserAuthToken(token)
if (interval) {
clearInterval(interval)
interval = null
}
}
}
fetchToken()
interval = setInterval(fetchToken, 1000)
return () => {
if (interval) clearInterval(interval)
}
}, [])
const handleCopyToken = async () => {
if (userAuthToken) {
await navigator.clipboard.writeText(userAuthToken)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}
const handleSave = async () => {
setSaving(true)
try {
await onSave({
apiKey,
baseURL,
model,
language,
maxSteps: maxSteps || undefined,
systemInstruction: systemInstruction || undefined,
experimentalLlmsTxt,
disableNamedToolChoice,
})
} finally {
setSaving(false)
}
}
return (
<div className="flex flex-col gap-4 p-4 relative">
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold">Settings</h2>
<Button
variant="ghost"
size="icon-sm"
onClick={onClose}
className="absolute top-2 right-3 cursor-pointer"
>
<CornerUpLeft className="size-3.5" />
</Button>
</div>
{/* User Auth Token Section */}
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
<label className="text-xs font-medium text-muted-foreground">User Auth Token</label>
<p className="text-[10px] text-muted-foreground mb-1">
Give a website the ability to call this extension.
</p>
<div className="flex gap-2 items-center">
<Input
readOnly
value={
userAuthToken
? showToken
? userAuthToken
: `${userAuthToken.slice(0, 4)}${'•'.repeat(userAuthToken.length - 8)}${userAuthToken.slice(-4)}`
: 'Loading...'
}
className="text-xs h-8 font-mono bg-background"
/>
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0 cursor-pointer"
onClick={() => setShowToken(!showToken)}
disabled={!userAuthToken}
>
{showToken ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0 cursor-pointer"
onClick={handleCopyToken}
disabled={!userAuthToken}
>
{copied ? <span className=""></span> : <Copy className="size-3" />}
</Button>
</div>
</div>
{/* Hub link */}
<a
href="/hub.html"
target="_blank"
className="flex items-center justify-between p-3 rounded-md border bg-muted/50 text-xs font-medium text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors"
>
Manage Page Agent Hub
<ExternalLink className="size-3" />
</a>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Base URL</label>
<Input
placeholder="https://api.openai.com/v1"
value={baseURL}
onChange={(e) => setBaseURL(e.target.value)}
className="text-xs h-8"
/>
</div>
{/* Testing API notice */}
{isTestingEndpoint(baseURL) && (
<div className="p-2.5 rounded-md border border-amber-500/30 bg-amber-500/5 text-[11px] text-muted-foreground leading-relaxed">
<Scale className="size-3 inline-block mr-1 -mt-0.5 text-amber-600" />
You are using our testing API. By using this you agree to the{' '}
<a
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
Terms of Use & Privacy Policy
</a>
</div>
)}
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Model</label>
<Input
placeholder="gpt-5.1"
value={model}
onChange={(e) => setModel(e.target.value)}
className="text-xs h-8"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">API Key</label>
<div className="flex gap-2 items-center">
<Input
type={showApiKey ? 'text' : 'password'}
// placeholder="sk-..."
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="text-xs h-8"
/>
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0 cursor-pointer"
onClick={() => setShowApiKey(!showApiKey)}
>
{showApiKey ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
</Button>
</div>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Response Language</label>
<select
value={language ?? ''}
onChange={(e) => setLanguage((e.target.value || undefined) as LanguagePreference)}
className="h-8 text-xs rounded-md border border-input bg-background px-2 cursor-pointer"
>
<option value="">System</option>
<option value="en-US">English</option>
<option value="zh-CN"></option>
</select>
</div>
{/* Advanced Config */}
<button
type="button"
onClick={() => setAdvancedOpen(!advancedOpen)}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground cursor-pointer mt-1 font-bold"
>
Advanced
{advancedOpen ? <FoldVertical className="size-3" /> : <UnfoldVertical className="size-3" />}
</button>
{advancedOpen && (
<>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Max Steps</label>
<Input
type="number"
placeholder="40"
min={1}
max={200}
value={maxSteps ?? ''}
onChange={(e) => setMaxSteps(e.target.value ? Number(e.target.value) : undefined)}
className="text-xs h-8 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [-moz-appearance:textfield]"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">System Instruction</label>
<textarea
placeholder="Additional instructions for the agent..."
value={systemInstruction}
onChange={(e) => setSystemInstruction(e.target.value)}
rows={3}
className="text-xs rounded-md border border-input bg-background px-3 py-2 resize-y min-h-[60px]"
/>
</div>
<label className="flex items-center justify-between cursor-pointer">
<span className="text-xs text-muted-foreground">Disable named tool_choice</span>
<Switch checked={disableNamedToolChoice} onCheckedChange={setDisableNamedToolChoice} />
</label>
<label className="flex items-center justify-between cursor-pointer">
<span className="text-xs text-muted-foreground">Experimental llms.txt support</span>
<Switch checked={experimentalLlmsTxt} onCheckedChange={setExperimentalLlmsTxt} />
</label>
</>
)}
<div className="flex gap-2 mt-2">
<Button variant="outline" onClick={onClose} className="flex-1 h-8 text-xs cursor-pointer">
Cancel
</Button>
<Button
onClick={handleSave}
disabled={saving}
className="flex-1 h-8 text-xs cursor-pointer"
>
{saving ? <Loader2 className="size-3 animate-spin" /> : 'Save'}
</Button>
</div>
{/* Footer */}
<div className="mt-4 mb-4 pt-4 border-t border-border/50 flex gap-2 justify-between text-[10px] text-muted-foreground">
<div className="flex flex-col justify-between">
<span>
Version <span className="font-mono">v{__VERSION__}</span>
</span>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 hover:text-foreground"
>
<svg role="img" viewBox="0 0 24 24" className="size-3 fill-current">
<path d={siGithub.path} />
</svg>
<span>Source Code</span>
</a>
</div>
<div className="flex flex-col items-end">
<a
href="https://alibaba.github.io/page-agent/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 hover:text-foreground"
>
<Home className="size-3" />
<span>Home Page</span>
</a>
<a
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 hover:text-foreground"
>
<HatGlasses className="size-3" />
<span>Privacy</span>
</a>
</div>
</div>
{/* attribute */}
<div className="text-[10px] text-muted-foreground bg-background fixed bottom-0 w-full flex justify-around">
<span className="leading-loose">
Built with by{' '}
<a
href="https://github.com/gaomeng1900"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
@Simon
</a>
</span>
</div>
</div>
)
}
@@ -0,0 +1,60 @@
import { AlertTriangle, Eraser, RotateCcw } from 'lucide-react'
import { Component, type ErrorInfo, type ReactNode } from 'react'
import { Button } from '@/components/ui/button'
interface Props {
children: ReactNode
}
interface State {
hasError: boolean
error: Error | null
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null }
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('[ErrorBoundary]', error, errorInfo.componentStack)
}
handleReload = () => {
window.location.reload()
}
handleResetConfig = async () => {
await chrome.storage.local.remove(['llmConfig', 'language', 'advancedConfig'])
window.location.reload()
}
render() {
if (!this.state.hasError) {
return this.props.children
}
return (
<div className="flex flex-col items-center justify-center h-screen bg-background p-6 text-center">
<AlertTriangle className="size-12 text-destructive mb-4" />
<h2 className="text-lg font-semibold mb-2">Something went wrong</h2>
<p className="text-sm text-muted-foreground mb-4 max-w-xs">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={this.handleResetConfig}>
<Eraser className="size-3.5 mr-2" />
Reset Config
</Button>
<Button variant="outline" size="sm" onClick={this.handleReload}>
<RotateCcw className="size-3.5 mr-2" />
Reload Panel
</Button>
</div>
</div>
)
}
}
@@ -0,0 +1,80 @@
import { ArrowLeft, RotateCcw, Trash2 } from 'lucide-react'
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { type SessionRecord, deleteSession, getSession } from '@/lib/db'
import { EventCard } from './cards'
export function HistoryDetail({
sessionId,
onBack,
onRerun,
}: {
sessionId: string
onBack: () => void
onRerun: (task: string) => void
}) {
const [session, setSession] = useState<SessionRecord | null>(null)
useEffect(() => {
getSession(sessionId).then((s) => setSession(s ?? null))
}, [sessionId])
if (!session) {
return (
<div className="flex items-center justify-center h-screen text-xs text-muted-foreground">
Loading...
</div>
)
}
return (
<div className="flex flex-col h-screen bg-background">
{/* Header */}
<header className="flex items-center gap-2 border-b px-3 py-2">
<Button variant="ghost" size="icon-sm" onClick={onBack} className="cursor-pointer">
<ArrowLeft className="size-3.5" />
</Button>
<span className="text-sm font-medium truncate">History</span>
</header>
{/* Task */}
<div className="border-b px-3 py-2 bg-muted/30">
<div className="text-[10px] text-muted-foreground uppercase tracking-wide">Task</div>
<div className="text-xs font-medium" title={session.task}>
{session.task}
</div>
<div className="mt-2 flex items-center gap-2">
<button
type="button"
onClick={() => onRerun(session.task)}
className="flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<RotateCcw className="size-3" />
Run again
</button>
<button
type="button"
onClick={async () => {
await deleteSession(sessionId)
onBack()
}}
className="flex items-center gap-1 text-[10px] text-muted-foreground hover:text-destructive transition-colors cursor-pointer"
>
<Trash2 className="size-3" />
Delete
</button>
</div>
</div>
{/* Events (read-only) */}
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{session.history.map((event, index) => (
// eslint-disable-next-line react-x/no-array-index-key
<EventCard key={index} event={event} />
))}
</div>
</div>
)
}
@@ -0,0 +1,153 @@
import { ArrowDownToLine, ArrowLeft, CheckCircle, RotateCcw, Trash2, XCircle } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { type SessionRecord, clearSessions, deleteSession, listSessions } from '@/lib/db'
import { downloadHistoryExport } from '@/lib/history-export'
function timeAgo(ts: number): string {
const seconds = Math.floor((Date.now() - ts) / 1000)
if (seconds < 60) return 'just now'
const minutes = Math.floor(seconds / 60)
if (minutes < 60) return `${minutes}m ago`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}h ago`
const days = Math.floor(hours / 24)
return `${days}d ago`
}
export function HistoryList({
onSelect,
onBack,
onRerun,
}: {
onSelect: (id: string) => void
onBack: () => void
onRerun: (task: string) => void
}) {
const [sessions, setSessions] = useState<SessionRecord[]>([])
const [loading, setLoading] = useState(true)
const load = useCallback(async () => {
setSessions(await listSessions())
setLoading(false)
}, [])
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
load()
}, [load])
const handleDelete = async (e: React.MouseEvent, id: string) => {
e.stopPropagation()
await deleteSession(id)
setSessions((prev) => prev.filter((s) => s.id !== id))
}
const handleExport = (e: React.MouseEvent, session: SessionRecord) => {
e.stopPropagation()
downloadHistoryExport(session.task, session.createdAt, session.history)
}
const handleRerun = (e: React.MouseEvent, task: string) => {
e.stopPropagation()
onRerun(task)
}
return (
<div className="flex flex-col h-screen bg-background">
{/* Header */}
<header className="flex items-center gap-2 border-b px-3 py-2">
<Button variant="ghost" size="icon-sm" onClick={onBack} className="cursor-pointer">
<ArrowLeft className="size-3.5" />
</Button>
<span className="text-sm font-medium flex-1">History</span>
{sessions.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={async () => {
await clearSessions()
setSessions([])
}}
className="text-[10px] text-muted-foreground hover:text-destructive cursor-pointer h-6 px-2"
>
<Trash2 className="size-3 mr-1" />
Clear All
</Button>
)}
</header>
{/* List */}
<div className="flex-1 overflow-y-auto">
{loading && (
<div className="flex items-center justify-center h-32 text-xs text-muted-foreground">
Loading...
</div>
)}
{!loading && sessions.length === 0 && (
<div className="flex items-center justify-center h-32 text-xs text-muted-foreground">
No history yet
</div>
)}
{sessions.map((session) => (
<div
key={session.id}
role="button"
tabIndex={0}
onClick={() => onSelect(session.id)}
className="w-full text-left px-3 py-2.5 border-b hover:bg-muted/50 transition-colors cursor-pointer flex items-start gap-2 group"
>
{/* Status icon */}
{session.status === 'completed' ? (
<CheckCircle className="size-3.5 text-green-500 shrink-0 mt-0.5" />
) : (
<XCircle className="size-3.5 text-destructive shrink-0 mt-0.5" />
)}
{/* Content */}
<div className="flex-1 min-w-0">
<p className="text-xs font-medium truncate">{session.task}</p>
<div className="flex items-center mt-0.5">
<p className="text-[10px] text-muted-foreground">
{timeAgo(session.createdAt)} · {session.history.length} steps
</p>
<div className="flex items-center gap-0.5 ml-auto opacity-0 group-hover:opacity-100 transition-opacity">
<button
type="button"
onClick={(e) => handleRerun(e, session.task)}
className="p-0.5 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
title="Run task again"
aria-label={`Run history task again: ${session.task}`}
>
<RotateCcw className="size-3" />
</button>
<button
type="button"
onClick={(e) => handleExport(e, session)}
className="p-1 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
title="Export history JSON"
aria-label={`Export history for ${session.task}`}
>
<ArrowDownToLine className="size-3" />
</button>
<button
type="button"
onClick={(e) => handleDelete(e, session.id)}
className="p-0.5 text-muted-foreground hover:text-destructive transition-colors cursor-pointer"
title="Delete history"
aria-label={`Delete history for ${session.task}`}
>
<Trash2 className="size-3" />
</button>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)
}
+386
View File
@@ -0,0 +1,386 @@
import type {
AgentActivity,
AgentErrorEvent,
AgentStepEvent,
HistoricalEvent,
ObservationEvent,
RetryEvent,
} from '@page-agent/core'
import {
CheckCircle,
Eye,
Globe,
Keyboard,
Mouse,
MoveVertical,
RefreshCw,
Sparkles,
XCircle,
Zap,
} from 'lucide-react'
import { Fragment, useState } from 'react'
import { cn } from '@/lib/utils'
// Result card for done action
function ResultCard({
success,
text,
children,
}: {
success: boolean
text: string
children?: React.ReactNode
}) {
return (
<div
className={cn(
'rounded-lg border p-3',
success ? 'border-green-500/30 bg-green-500/10' : 'border-destructive/30 bg-destructive/10'
)}
>
<div className="flex items-center gap-2 mb-2">
{success ? (
<CheckCircle className="size-3.5 text-green-500" />
) : (
<XCircle className="size-3.5 text-destructive" />
)}
<span
className={cn(
'text-xs font-medium',
success ? 'text-green-600 dark:text-green-400' : 'text-destructive'
)}
>
Result: {success ? 'Success' : 'Failed'}
</span>
</div>
<p className="text-xs text-[11px] text-muted-foreground pl-5 whitespace-pre-wrap">{text}</p>
{children}
</div>
)
}
// Single reflection item with truncation
function ReflectionItem({ icon, value }: { icon: string; value: string }) {
const [expanded, setExpanded] = useState(false)
return (
<Fragment>
<span className="text-xs flex justify-center">{icon}</span>
<span
className={cn(
'text-[11px] text-muted-foreground cursor-pointer hover:text-muted-foreground/70',
!expanded && 'line-clamp-1'
)}
onClick={() => setExpanded(!expanded)}
>
{value}
</span>
</Fragment>
)
}
// Reflection section in step card
function ReflectionSection({
reflection,
}: {
reflection: {
evaluation_previous_goal?: string
memory?: string
next_goal?: string
}
}) {
const items = [
{ icon: '☑️', label: 'eval', value: reflection.evaluation_previous_goal },
{ icon: '🧠', label: 'memory', value: reflection.memory },
{ icon: '🎯', label: 'goal', value: reflection.next_goal },
].filter((item) => item.value)
if (items.length === 0) return null
return (
<div className="mb-2">
{/* <div className="text-[11px] font-semibold text-foreground uppercase tracking-wide mb-2">
Reflection
</div> */}
<div className="grid grid-cols-[14px_1fr] gap-x-2 gap-y-2">
{items.map((item) => (
<ReflectionItem key={item.label} icon={item.icon} value={item.value!} />
))}
</div>
</div>
)
}
// Get icon for action type
function ActionIcon({ name, className }: { name: string; className?: string }) {
const icons: Record<string, React.ReactNode> = {
click_element_by_index: <Mouse className={className} />,
input: <Keyboard className={className} />,
scroll: <MoveVertical className={className} />,
go_to_url: <Globe className={className} />,
}
return icons[name] || <Zap className={className} />
}
// Copy button with "Copied!" feedback
function CopyButton({ text, label }: { text: string; label: string }) {
const [copied, setCopied] = useState(false)
return (
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
}}
className="text-[9px] text-muted-foreground hover:text-foreground transition-colors border px-1 rounded shrink-0 cursor-pointer backdrop-blur-xs"
>
{copied ? 'Copied!' : label}
</button>
)
}
// Extract message content by role from raw request
function extractPrompt(rawRequest: unknown, role: 'system' | 'user'): string | null {
const messages = (rawRequest as { messages?: { role: string; content?: unknown }[] })?.messages
if (!messages) return null
const msg =
role === 'system'
? messages.find((m) => m.role === role)
: messages.findLast((m) => m.role === role)
if (!msg?.content) return null
return typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content, null, 2)
}
// Raw request/response section (collapsible tabs, for debugging)
function RawSection({ rawRequest, rawResponse }: { rawRequest?: unknown; rawResponse?: unknown }) {
const [activeTab, setActiveTab] = useState<'request' | 'response' | null>(null)
if (!rawRequest && !rawResponse) return null
const handleTabClick = (tab: 'request' | 'response') => {
setActiveTab(activeTab === tab ? null : tab)
}
const content =
activeTab === 'request' ? rawRequest : activeTab === 'response' ? rawResponse : null
const systemPrompt = activeTab === 'request' ? extractPrompt(rawRequest, 'system') : null
const userPrompt = activeTab === 'request' ? extractPrompt(rawRequest, 'user') : null
return (
<div className="mt-2 border-t border-dashed pt-2">
<div className="flex items-center gap-3 -my-1">
{rawRequest != null && (
<button
type="button"
onClick={() => handleTabClick('request')}
className={cn(
'text-[10px] mt-0.5 transition-colors border-b cursor-pointer',
activeTab === 'request'
? 'text-foreground border-foreground'
: 'text-muted-foreground border-transparent hover:text-foreground'
)}
>
Raw Request
</button>
)}
{rawResponse != null && (
<button
type="button"
onClick={() => handleTabClick('response')}
className={cn(
'text-[10px] mt-0.5 transition-colors border-b cursor-pointer',
activeTab === 'response'
? 'text-foreground border-foreground'
: 'text-muted-foreground border-transparent hover:text-foreground'
)}
>
Raw Response
</button>
)}
</div>
{content != null && (
<div className="relative mt-1.5">
<div className="absolute top-1 right-1 flex gap-1">
{systemPrompt && <CopyButton text={systemPrompt} label="Copy System" />}
{userPrompt && <CopyButton text={userPrompt} label="Copy User" />}
<CopyButton text={JSON.stringify(content, null, 4)} label="Copy" />
</div>
<pre className="p-2 pt-5 text-[10px] text-foreground/70 bg-muted rounded overflow-x-auto max-h-60 overflow-y-auto">
{JSON.stringify(content, null, 4)}
</pre>
</div>
)}
</div>
)
}
function StepCard({ event }: { event: AgentStepEvent }) {
return (
<div className="rounded-lg border-l-2 border-l-blue-500/50 border bg-muted/40 p-2.5">
<div className="text-[11px] font-semibold text-foreground tracking-wide mb-2">
Step #{event.stepIndex! + 1}
</div>
{/* Reflection */}
{event.reflection && <ReflectionSection reflection={event.reflection} />}
{/* Action */}
{event.action && (
<div>
<div className="text-[11px] font-semibold text-foreground tracking-wide mb-1">
Actions
</div>
<div className="flex items-start gap-2">
<ActionIcon
name={event.action.name}
className="size-3.5 text-blue-500 shrink-0 mt-0.5"
/>
<div className="flex-1 min-w-0">
<p className="text-xs text-foreground/80 mb-0.5 wrap-anywhere break-all line-clamp-1 hover:line-clamp-none">
<span className="font-medium text-foreground/70">{event.action.name}</span>
{event.action.name !== 'done' && (
<span className="text-muted-foreground/70 ml-1.5">
{JSON.stringify(event.action.input)}
</span>
)}
</p>
<p className="text-[11px] text-muted-foreground/70 grid grid-cols-[auto_1fr] gap-1.5">
<span className=""></span>
<span className="wrap-anywhere break-all line-clamp-1 hover:line-clamp-3">
{event.action.output}
</span>
</p>
</div>
</div>
</div>
)}
{/* Raw Response */}
<RawSection rawRequest={event.rawRequest} rawResponse={event.rawResponse} />
</div>
)
}
function ObservationCard({ event }: { event: ObservationEvent }) {
return (
<div className="rounded-lg border-l-2 border-l-green-500/50 border bg-muted/40 p-2.5">
{/* <div className="text-[11px] font-semibold text-foreground uppercase tracking-wide mb-2">
Observation
</div> */}
<div className="flex items-start gap-2">
<Eye className="size-3.5 text-green-500 shrink-0 mt-0.5" />
<span className="text-[11px] text-muted-foreground">{event.content}</span>
</div>
</div>
)
}
function RetryCard({ event }: { event: RetryEvent }) {
return (
<div className="rounded-lg border border-amber-500/30 bg-amber-500/10 p-2.5">
<div className="flex items-start gap-1.5">
<RefreshCw className="size-3 text-amber-500 shrink-0 mt-0.5" />
<span className="text-xs text-amber-600 dark:text-amber-400">
{event.message} ({event.attempt}/{event.maxAttempts})
</span>
</div>
</div>
)
}
function ErrorCard({ event }: { event: AgentErrorEvent }) {
return (
<div className="rounded-lg border border-destructive/30 bg-destructive/10 p-2.5">
<div className="flex items-start gap-1.5">
<XCircle className="size-3 text-destructive shrink-0 mt-0.5" />
<span className="text-xs text-destructive">{event.message}</span>
</div>
<RawSection rawResponse={event.rawResponse} />
</div>
)
}
// History event card component
export function EventCard({ event }: { event: HistoricalEvent }) {
// Done action - show as result card
if (event.type === 'step' && event.action?.name === 'done') {
const input = event.action.input as { text?: string; success?: boolean }
return (
<>
<StepCard event={event as AgentStepEvent} />
<ResultCard
success={input?.success ?? true}
text={input?.text || event.action.output || ''}
/>
</>
)
}
if (event.type === 'step') {
return <StepCard event={event as AgentStepEvent} />
}
if (event.type === 'observation') {
return <ObservationCard event={event as ObservationEvent} />
}
if (event.type === 'retry') {
return <RetryCard event={event as RetryEvent} />
}
if (event.type === 'error') {
return <ErrorCard event={event as AgentErrorEvent} />
}
return null
}
// Activity card with animation
export function ActivityCard({ activity }: { activity: AgentActivity }) {
const getActivityInfo = () => {
switch (activity.type) {
case 'thinking':
return { text: 'Thinking...', color: 'text-blue-500' }
case 'executing':
return { text: `Executing ${activity.tool}...`, color: 'text-amber-500' }
case 'executed':
return { text: `Done: ${activity.tool}`, color: 'text-green-500' }
case 'retrying':
return {
text: `Retrying (${activity.attempt}/${activity.maxAttempts})...`,
color: 'text-amber-500',
}
case 'error':
return { text: activity.message, color: 'text-destructive' }
}
}
const info = getActivityInfo()
return (
<div className="flex items-center gap-2 rounded-lg border bg-muted/40 p-2.5 animate-pulse">
<div className="relative">
<Sparkles className={cn('size-3.5', info.color)} />
<span
className={cn(
'absolute -top-0.5 -right-0.5 size-1.5 rounded-full animate-ping',
activity.type === 'thinking'
? 'bg-blue-500'
: activity.type === 'executing'
? 'bg-amber-500'
: activity.type === 'retrying'
? 'bg-amber-500'
: activity.type === 'error'
? 'bg-destructive'
: 'bg-green-500'
)}
/>
</div>
<span className={cn('text-xs', info.color)}>{info.text}</span>
</div>
)
}
+152
View File
@@ -0,0 +1,152 @@
import type { AgentStatus } from '@page-agent/core'
import { Motion } from 'ai-motion'
import { BookOpen, Globe } from 'lucide-react'
import { useEffect, useRef } from 'react'
import { siGithub } from 'simple-icons'
import { TypingAnimation } from '@/components/ui/typing-animation'
import { cn } from '@/lib/utils'
// Status dot indicator
export function StatusDot({ status }: { status: AgentStatus }) {
const colorClass = {
idle: 'bg-muted-foreground',
running: 'bg-blue-500',
completed: 'bg-green-500',
error: 'bg-destructive',
}[status]
const label = {
idle: 'Ready',
running: 'Running',
completed: 'Done',
error: 'Error',
}[status]
return (
<div className="flex items-center gap-1.5 mr-2">
<span
className={cn('size-2 rounded-full', colorClass, status === 'running' && 'animate-pulse')}
/>
<span className="text-xs text-muted-foreground">{label}</span>
</div>
)
}
export function Logo({ className }: { className?: string }) {
return <img src="/assets/page-agent-256.webp" alt="Page Agent" className={cn('', className)} />
}
// Full-screen ai-motion glow overlay, shown only while running
export function MotionOverlay({ active }: { active: boolean }) {
const containerRef = useRef<HTMLDivElement>(null)
const motionRef = useRef<Motion | null>(null)
useEffect(() => {
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 () => {
motionRef.current?.dispose()
motionRef.current = null
}
}, [])
useEffect(() => {
const motion = motionRef.current
if (!motion) return
let disposed = false
if (active) {
motion.start()
motion.fadeIn()
} else {
motion.fadeOut().then(() => !disposed && motion.pause())
}
return () => {
disposed = true
}
}, [active])
return (
<div
ref={containerRef}
className="pointer-events-none absolute inset-0 z-10 opacity-60 overflow-hidden"
style={{ display: active ? undefined : 'none' }}
/>
)
}
// Empty state with logo and breathing glow
export function EmptyState() {
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-center px-6">
<div className="relative select-none pointer-events-none">
<div className="absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_180deg,oklch(0.55_0.2_280),oklch(0.5_0.15_230),oklch(0.6_0.18_310),oklch(0.55_0.2_280))] blur-2xl animate-[glow-a_5s_ease-in-out_infinite]" />
<div className="absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_0deg,oklch(0.55_0.18_160),oklch(0.5_0.2_200),oklch(0.6_0.15_120),oklch(0.55_0.18_160))] blur-2xl animate-[glow-b_5s_ease-in-out_infinite]" />
<Logo className="relative size-20 opacity-80" />
</div>
<div>
<h2 className="text-base font-medium text-foreground mb-1">Page Agent Ext</h2>
<TypingAnimation
className="text-sm text-muted-foreground"
words={[
'Enter a task to automate this page',
'Execute multi-page tasks',
'Call this extension from your web page',
'Use this extension in your own agents',
]}
cursorStyle="underscore"
loop
typeSpeed={20}
deleteSpeed={10}
pauseDelay={3000}
/>
</div>
<div className="flex items-center gap-3 mt-1 text-muted-foreground">
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground transition-colors"
title="GitHub"
>
<svg role="img" viewBox="0 0 24 24" className="size-4 fill-current">
<path d={siGithub.path} />
</svg>
</a>
<a
href="https://alibaba.github.io/page-agent/docs/features/chrome-extension"
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground transition-colors"
title="Documentation"
>
<BookOpen className="size-4" />
</a>
<a
href="https://alibaba.github.io/page-agent"
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground transition-colors"
title="Website"
>
<Globe className="size-4" />
</a>
</div>
</div>
)
}
@@ -0,0 +1,60 @@
import { Slot } from '@radix-ui/react-slot'
import { type VariantProps, cva } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
function Button({
className,
variant = 'default',
size = 'default',
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
@@ -0,0 +1,75 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
function Card({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card"
className={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-header"
className={cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-title"
className={cn('leading-none font-semibold', className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-description"
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-action"
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="card-content" className={cn('px-6', className)} {...props} />
}
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-footer"
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...props}
/>
)
}
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }
@@ -0,0 +1,232 @@
import { type VariantProps, cva } from 'class-variance-authority'
import { useMemo } from 'react'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
return (
<fieldset
data-slot="field-set"
className={cn(
'flex flex-col gap-6',
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
className
)}
{...props}
/>
)
}
function FieldLegend({
className,
variant = 'legend',
...props
}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
'mb-3 font-medium',
'data-[variant=legend]:text-base',
'data-[variant=label]:text-sm',
className
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="field-group"
className={cn(
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
className
)}
{...props}
/>
)
}
const fieldVariants = cva('group/field flex w-full gap-3 data-[invalid=true]:text-destructive', {
variants: {
orientation: {
vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
horizontal: [
'flex-row items-center',
'[&>[data-slot=field-label]]:flex-auto',
'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
],
responsive: [
'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',
'@md/field-group:[&>[data-slot=field-label]]:flex-auto',
'@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
],
},
},
defaultVariants: {
orientation: 'vertical',
},
})
function Field({
className,
orientation = 'vertical',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
}
function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="field-content"
className={cn('group/field-content flex flex-1 flex-col gap-1.5 leading-snug', className)}
{...props}
/>
)
}
function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
className
)}
{...props}
/>
)
}
function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="field-label"
className={cn(
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
className
)}
{...props}
/>
)
}
function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
<p
data-slot="field-description"
className={cn(
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
className
)}
{...props}
/>
)
}
function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<'div'> & {
children?: React.ReactNode
}) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
className
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<'div'> & {
errors?: Array<{ message?: string } | undefined>
}) {
const content = useMemo(() => {
if (children) {
return children
}
if (!errors?.length) {
return null
}
const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()]
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map((error, index) => error?.message && <li key={index}>{error.message}</li>)}
</ul>
)
}, [children, errors])
if (!content) {
return null
}
return (
<div
role="alert"
data-slot="field-error"
className={cn('text-destructive text-sm font-normal', className)}
{...props}
>
{content}
</div>
)
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}
@@ -0,0 +1,36 @@
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
import * as React from 'react'
import { cn } from '@/lib/utils'
function HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
}
function HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
}
function HoverCardContent({
className,
align = 'center',
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
className
)}
{...props}
/>
</HoverCardPrimitive.Portal>
)
}
export { HoverCard, HoverCardTrigger, HoverCardContent }
@@ -0,0 +1,157 @@
import { type VariantProps, cva } from 'class-variance-authority'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
'group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none',
'h-9 min-w-0 has-[>textarea]:h-auto',
// Variants based on alignment.
'has-[>[data-align=inline-start]]:[&>input]:pl-2',
'has-[>[data-align=inline-end]]:[&>input]:pr-2',
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
// Focus state — soft multi-color glow matching ai-motion palette
'has-[[data-slot=input-group-control]:focus-visible]:border-blue-400/60',
'has-[[data-slot=input-group-control]:focus-visible]:shadow-[0_0_0_1px_rgba(57,182,255,0.2),0_0_8px_rgba(57,182,255,0.15),0_0_16px_rgba(189,69,251,0.1)]',
// Error state.
'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
className
)}
{...props}
/>
)
}
const inputGroupAddonVariants = cva(
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
{
variants: {
align: {
'inline-start': 'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',
'inline-end': 'order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]',
'block-start':
'order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5',
'block-end':
'order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5',
},
},
defaultVariants: {
align: 'inline-start',
},
}
)
function InputGroupAddon({
className,
align = 'inline-start',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest('button')) {
return
}
e.currentTarget.parentElement?.querySelector('input')?.focus()
}}
{...props}
/>
)
}
const inputGroupButtonVariants = cva('text-sm shadow-none flex gap-2 items-center', {
variants: {
size: {
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
sm: 'h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5',
'icon-xs': 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',
'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
},
},
defaultVariants: {
size: 'xs',
},
})
function InputGroupButton({
className,
type = 'button',
variant = 'ghost',
size = 'xs',
...props
}: Omit<React.ComponentProps<typeof Button>, 'size'> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function InputGroupInput({ className, ...props }: React.ComponentProps<'input'>) {
return (
<Input
data-slot="input-group-control"
className={cn(
'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
className
)}
{...props}
/>
)
}
function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
className
)}
{...props}
/>
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}
@@ -0,0 +1,21 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot="input"
className={cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
)}
{...props}
/>
)
}
export { Input }
@@ -0,0 +1,172 @@
import { Slot } from '@radix-ui/react-slot'
import { type VariantProps, cva } from 'class-variance-authority'
import * as React from 'react'
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
role="list"
data-slot="item-group"
className={cn('group/item-group flex flex-col', className)}
{...props}
/>
)
}
function ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn('my-0', className)}
{...props}
/>
)
}
const itemVariants = cva(
'group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
{
variants: {
variant: {
default: 'bg-transparent',
outline: 'border-border',
muted: 'bg-muted/50',
},
size: {
default: 'p-4 gap-4 ',
sm: 'py-3 px-4 gap-2.5',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
function Item({
className,
variant = 'default',
size = 'default',
asChild = false,
...props
}: React.ComponentProps<'div'> & VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'div'
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
)
}
const itemMediaVariants = cva(
'flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5',
{
variants: {
variant: {
default: 'bg-transparent',
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image: 'size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover',
},
},
defaultVariants: {
variant: 'default',
},
}
)
function ItemMedia({
className,
variant = 'default',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
)
}
function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-content"
className={cn('flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none', className)}
{...props}
/>
)
}
function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-title"
className={cn('flex w-fit items-center gap-2 text-sm leading-snug font-medium', className)}
{...props}
/>
)
}
function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
<p
data-slot="item-description"
className={cn(
'text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
className
)}
{...props}
/>
)
}
function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div data-slot="item-actions" className={cn('flex items-center gap-2', className)} {...props} />
)
}
function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-header"
className={cn('flex basis-full items-center justify-between gap-2', className)}
{...props}
/>
)
}
function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-footer"
className={cn('flex basis-full items-center justify-between gap-2', className)}
{...props}
/>
)
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
}
@@ -0,0 +1,19 @@
import * as LabelPrimitive from '@radix-ui/react-label'
import * as React from 'react'
import { cn } from '@/lib/utils'
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className
)}
{...props}
/>
)
}
export { Label }
@@ -0,0 +1,26 @@
import * as SeparatorPrimitive from '@radix-ui/react-separator'
import * as React from 'react'
import { cn } from '@/lib/utils'
function Separator({
className,
orientation = 'horizontal',
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
className
)}
{...props}
/>
)
}
export { Separator }
@@ -0,0 +1,38 @@
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from 'lucide-react'
import { useTheme } from 'next-themes'
import { Toaster as Sonner, type ToasterProps } from 'sonner'
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = 'system' } = useTheme()
return (
<Sonner
theme={theme as ToasterProps['theme']}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
'--normal-bg': 'var(--popover)',
'--normal-text': 'var(--popover-foreground)',
'--normal-border': 'var(--border)',
'--border-radius': 'var(--radius)',
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }
@@ -0,0 +1,16 @@
import { Loader2Icon } from 'lucide-react'
import { cn } from '@/lib/utils'
function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<Loader2Icon
role="status"
aria-label="Loading"
className={cn('size-4 animate-spin', className)}
{...props}
/>
)
}
export { Spinner }
@@ -0,0 +1,26 @@
import * as SwitchPrimitive from '@radix-ui/react-switch'
import * as React from 'react'
import { cn } from '@/lib/utils'
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<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 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}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }
@@ -0,0 +1,18 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
<textarea
data-slot="textarea"
className={cn(
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
{...props}
/>
)
}
export { Textarea }
@@ -0,0 +1,164 @@
import { MotionProps, motion, useInView } from 'motion/react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { cn } from '@/lib/utils'
interface TypingAnimationProps extends MotionProps {
children?: string
words?: string[]
className?: string
duration?: number
typeSpeed?: number
deleteSpeed?: number
delay?: number
pauseDelay?: number
loop?: boolean
as?: React.ElementType
startOnView?: boolean
showCursor?: boolean
blinkCursor?: boolean
cursorStyle?: 'line' | 'block' | 'underscore'
}
export function TypingAnimation({
children,
words,
className,
duration = 100,
typeSpeed,
deleteSpeed,
delay = 0,
pauseDelay = 1000,
loop = false,
as: Component = 'span',
startOnView = true,
showCursor = true,
blinkCursor = true,
cursorStyle = 'line',
...props
}: TypingAnimationProps) {
const MotionComponent = motion.create(Component, {
forwardMotionProps: true,
})
const [displayedText, setDisplayedText] = useState<string>('')
const [currentWordIndex, setCurrentWordIndex] = useState(0)
const [currentCharIndex, setCurrentCharIndex] = useState(0)
const [phase, setPhase] = useState<'typing' | 'pause' | 'deleting'>('typing')
const elementRef = useRef<HTMLElement | null>(null)
const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
})
const wordsToAnimate = useMemo(() => words || (children ? [children] : []), [words, children])
const hasMultipleWords = wordsToAnimate.length > 1
const typingSpeed = typeSpeed || duration
const deletingSpeed = deleteSpeed || typingSpeed / 2
const shouldStart = startOnView ? isInView : true
useEffect(() => {
if (!shouldStart || wordsToAnimate.length === 0) return
const timeoutDelay =
delay > 0 && displayedText === ''
? delay
: phase === 'typing'
? typingSpeed
: phase === 'deleting'
? deletingSpeed
: pauseDelay
const timeout = setTimeout(() => {
const currentWord = wordsToAnimate[currentWordIndex] || ''
const graphemes = Array.from(currentWord)
switch (phase) {
case 'typing':
if (currentCharIndex < graphemes.length) {
setDisplayedText(graphemes.slice(0, currentCharIndex + 1).join(''))
setCurrentCharIndex(currentCharIndex + 1)
} else {
if (hasMultipleWords || loop) {
const isLastWord = currentWordIndex === wordsToAnimate.length - 1
if (!isLastWord || loop) {
setPhase('pause')
}
}
}
break
case 'pause':
setPhase('deleting')
break
case 'deleting':
if (currentCharIndex > 0) {
setDisplayedText(graphemes.slice(0, currentCharIndex - 1).join(''))
setCurrentCharIndex(currentCharIndex - 1)
} else {
const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
setCurrentWordIndex(nextIndex)
setPhase('typing')
}
break
}
}, timeoutDelay)
return () => clearTimeout(timeout)
}, [
shouldStart,
phase,
currentCharIndex,
currentWordIndex,
displayedText,
wordsToAnimate,
hasMultipleWords,
loop,
typingSpeed,
deletingSpeed,
pauseDelay,
delay,
])
const currentWordGraphemes = Array.from(wordsToAnimate[currentWordIndex] || '')
const isComplete =
!loop &&
currentWordIndex === wordsToAnimate.length - 1 &&
currentCharIndex >= currentWordGraphemes.length &&
phase !== 'deleting'
const shouldShowCursor =
showCursor &&
!isComplete &&
(hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)
const getCursorChar = () => {
switch (cursorStyle) {
case 'block':
return '▌'
case 'underscore':
return '_'
case 'line':
default:
return '|'
}
}
return (
<MotionComponent
ref={elementRef}
className={cn('leading-[5rem] tracking-[-0.02em]', className)}
{...props}
>
{displayedText}
{shouldShowCursor && (
<span className={cn('inline-block', blinkCursor && 'animate-blink-cursor')}>
{getCursorChar()}
</span>
)}
</MotionComponent>
)
}
@@ -0,0 +1,63 @@
import { handlePageControlMessage } from '@/agent/RemotePageController.background'
import { handleTabControlMessage, setupTabChangeEvents } from '@/agent/TabsController.background'
export default defineBackground(() => {
console.log('[Background] Service Worker started')
// tab change events
setupTabChangeEvents()
// generate user auth token
chrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => {
if (result.PageAgentExtUserAuthToken) return
const userAuthToken = crypto.randomUUID()
chrome.storage.local.set({ PageAgentExtUserAuthToken: userAuthToken })
})
// message proxy
chrome.runtime.onMessage.addListener((message, sender, sendResponse): true | undefined => {
if (message.type === 'TAB_CONTROL') {
return handleTabControlMessage(message, sender, sendResponse)
} else if (message.type === 'PAGE_CONTROL') {
return handlePageControlMessage(message, sender, sendResponse)
} else {
sendResponse({ error: 'Unknown message type' })
return
}
})
// 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 })
}
@@ -0,0 +1,158 @@
import { initPageController } from '@/agent/RemotePageController.content'
// import { DEMO_CONFIG } from '@/agent/constants'
const DEBUG_PREFIX = '[Content]'
export default defineContentScript({
matches: ['<all_urls>'],
runAt: 'document_end',
main() {
console.debug(`${DEBUG_PREFIX} Loaded on ${window.location.href}`)
initPageController()
// if auth token matches, expose agent to page
chrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => {
// extension side token.
// @note this is isolated world. it is safe to assume user script cannot access it
const extToken = result.PageAgentExtUserAuthToken
if (!extToken) return
// page side token
const pageToken = localStorage.getItem('PageAgentExtUserAuthToken')
if (!pageToken) return
if (pageToken !== extToken) return
console.log('[PageAgentExt]: Auth tokens match. Exposing agent to page.')
// add isolated world script
exposeAgentToPage().then(
// add main-world script
() => injectScript('/main-world.js')
)
})
},
})
async function exposeAgentToPage() {
const { MultiPageAgent } = await import('@/agent/MultiPageAgent')
console.log('[PageAgentExt]: MultiPageAgent loaded')
/**
* singleton MultiPageAgent to handle requests from the page
*/
let multiPageAgent: InstanceType<typeof MultiPageAgent> | null = null
window.addEventListener('message', async (e) => {
const data = e.data
if (typeof data !== 'object' || data === null) return
if (data.channel !== 'PAGE_AGENT_EXT_REQUEST') return
const { action, payload, id } = data
switch (action) {
case 'execute': {
// singleton check
if (multiPageAgent && multiPageAgent.status === 'running') {
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_RESPONSE',
id,
action: 'execute_result',
error: 'Agent is already running a task. Please wait until it finishes.',
},
'*'
)
return
}
try {
const { task, config } = payload
// Dispose old instance before creating new one
multiPageAgent?.dispose()
multiPageAgent = new MultiPageAgent(config)
// events
multiPageAgent.addEventListener('statuschange', (event) => {
if (!multiPageAgent) return
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_RESPONSE',
id,
action: 'status_change_event',
payload: multiPageAgent.status,
},
'*'
)
})
multiPageAgent.addEventListener('activity', (event) => {
if (!multiPageAgent) return
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_RESPONSE',
id,
action: 'activity_event',
payload: (event as CustomEvent).detail,
},
'*'
)
})
multiPageAgent.addEventListener('historychange', (event) => {
if (!multiPageAgent) return
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_RESPONSE',
id,
action: 'history_change_event',
payload: multiPageAgent.history,
},
'*'
)
})
// result
const result = await multiPageAgent.execute(task)
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_RESPONSE',
id,
action: 'execute_result',
payload: result,
},
'*'
)
} catch (error) {
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_RESPONSE',
id,
action: 'execute_result',
error: (error as Error).message,
},
'*'
)
}
break
}
case 'stop': {
multiPageAgent?.stop()
break
}
default:
console.warn(`${DEBUG_PREFIX} Unknown action from page:`, action)
break
}
})
}
@@ -0,0 +1,251 @@
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) => (
// eslint-disable-next-line react-x/no-array-index-key
<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 latest = useRef({ execute, stop, configure, config })
useEffect(() => {
latest.current = { execute, stop, configure, config }
})
useEffect(() => {
if (!wsPort) return
const hubWs = new HubWs(
Number(wsPort),
{
onExecute: async (task, incomingConfig) => {
const { execute, configure, config } = latest.current
if (incomingConfig) {
await configure({ ...config, ...incomingConfig } as ExtConfig)
}
const result = await execute(task)
return { success: result.success, data: result.data }
},
onStop: () => latest.current.stop(),
},
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>
)
@@ -0,0 +1,118 @@
import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
export interface ExecuteConfig {
baseURL: string
model: string
apiKey?: string
/**
* Whether to include the initial tab (that holds this main world script) in the task.
* @default true
*/
includeInitialTab?: boolean
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
}
export default defineUnlistedScript(() => {
let _lastId = 0
function getId() {
_lastId += 1
return _lastId
}
const execute: Execute = async (task, config) => {
if (typeof task !== 'string') throw new Error('Task must be a string')
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.model) throw new Error('Config must have a model')
const id = getId()
const promise = new Promise<ExecutionResult>((resolve, reject) => {
function handleMessage(e: MessageEvent) {
const data = e.data
if (typeof data !== 'object' || data === null) return
if (data.channel !== 'PAGE_AGENT_EXT_RESPONSE') return
if (data.id !== id) return
// events
if (data.action === 'status_change_event' && config.onStatusChange) {
config.onStatusChange(data.payload)
return
}
if (data.action === 'activity_event' && config.onActivity) {
config.onActivity(data.payload)
return
}
if (data.action === 'history_change_event' && config.onHistoryUpdate) {
config.onHistoryUpdate(data.payload)
return
}
if (data.action !== 'execute_result') return
// execute_result
window.removeEventListener('message', handleMessage)
if (data.error) {
reject(new Error(data.error))
} else {
resolve(data.payload)
}
}
// @note will be removed on dispose or result
window.addEventListener('message', handleMessage)
})
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_REQUEST',
id,
action: 'execute',
payload: {
task,
config: {
baseURL: config.baseURL,
model: config.model,
apiKey: config.apiKey,
includeInitialTab: config.includeInitialTab,
},
},
},
'*'
)
return promise
}
const stop = () => {
const id = getId()
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_REQUEST',
id,
action: 'stop',
},
'*'
)
}
;(window as any).PAGE_AGENT_EXT_VERSION = __VERSION__
;(window as any).PAGE_AGENT_EXT = {
version: __VERSION__,
execute,
stop,
}
})
@@ -0,0 +1,228 @@
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,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from '@/components/ui/input-group'
import { saveSession } from '@/lib/db'
import { useAgent } from '../../agent/useAgent'
type View =
| { name: 'chat' }
| { name: 'config' }
| { name: 'history' }
| { name: 'history-detail'; sessionId: string }
export default function App() {
const [view, setView] = useState<View>({ name: 'chat' })
const [inputValue, setInputValue] = useState('')
const historyRef = useRef<HTMLDivElement>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const { status, history, activity, currentTask, config, execute, stop, configure } = useAgent()
// Persist session when task finishes
const prevStatusRef = useRef(status)
useEffect(() => {
const prev = prevStatusRef.current
prevStatusRef.current = status
if (
prev === 'running' &&
(status === 'completed' || status === 'error') &&
history.length > 0 &&
currentTask
) {
saveSession({ task: currentTask, history, status }).catch((err) =>
console.error('[SidePanel] Failed to save session:', err)
)
}
}, [status, history, currentTask])
// Auto-scroll to bottom on new events
useEffect(() => {
if (historyRef.current) {
historyRef.current.scrollTop = historyRef.current.scrollHeight
}
}, [history, activity])
const runTask = useCallback(
(task: string) => {
const normalizedTask = task.trim()
if (!normalizedTask || status === 'running') return
setInputValue('')
setView({ name: 'chat' })
execute(normalizedTask).catch((error) => {
console.error('[SidePanel] Failed to execute task:', error)
})
},
[execute, status]
)
const handleSubmit = useCallback(
(e?: React.SyntheticEvent) => {
e?.preventDefault()
runTask(inputValue)
},
[inputValue, runTask]
)
const handleStop = useCallback(() => {
console.log('[SidePanel] Stopping task...')
stop()
}, [stop])
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
e.preventDefault()
handleSubmit()
}
}
// --- View routing ---
if (view.name === 'config') {
return (
<ConfigPanel
config={config}
onSave={async (newConfig) => {
await configure(newConfig)
setView({ name: 'chat' })
}}
onClose={() => setView({ name: 'chat' })}
/>
)
}
if (view.name === 'history') {
return (
<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' })}
onRerun={runTask}
/>
)
}
// --- Chat view ---
const isRunning = status === 'running'
const showEmptyState = !currentTask && history.length === 0 && !isRunning
return (
<div className="relative flex flex-col h-screen bg-background">
<MotionOverlay active={isRunning} />
{/* Header */}
<header className="flex items-center justify-between border-b px-3 py-2">
<div className="flex items-center gap-2">
<Logo className="size-5" />
<span className="text-sm font-medium">Page Agent Ext</span>
</div>
<div className="flex items-center gap-1">
<StatusDot status={status} />
<Button
variant="ghost"
size="icon-sm"
onClick={() => setView({ name: 'history' })}
className="cursor-pointer"
>
<History className="size-3.5" />
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={() => setView({ name: 'config' })}
className="cursor-pointer"
>
<Settings className="size-3.5" />
</Button>
</div>
</header>
{/* Content */}
<main className="flex-1 overflow-hidden flex flex-col">
{/* Current task */}
{currentTask && (
<div className="border-b px-3 py-2 bg-muted/30">
<div className="text-[10px] text-muted-foreground uppercase tracking-wide">Task</div>
<div className="text-xs font-medium truncate" title={currentTask}>
{currentTask}
</div>
</div>
)}
{/* History */}
<div ref={historyRef} className="flex-1 overflow-y-auto p-3 space-y-2">
{showEmptyState && <EmptyState />}
{history.map((event, index) => (
// eslint-disable-next-line react-x/no-array-index-key
<EventCard key={index} event={event} />
))}
{/* Activity indicator at bottom */}
{activity && <ActivityCard activity={activity} />}
</div>
</main>
{/* Input */}
<footer className="border-t p-3">
<InputGroup className="relative rounded-lg">
<InputGroupTextarea
ref={textareaRef}
placeholder="Describe your task... (Enter to send)"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
disabled={isRunning}
className="text-xs pr-12 min-h-10"
/>
<InputGroupAddon align="inline-end" className="absolute bottom-0 right-0">
{isRunning ? (
<InputGroupButton
size="icon-sm"
variant="destructive"
onClick={handleStop}
className="size-7"
>
<Square className="size-3" />
</InputGroupButton>
) : (
<InputGroupButton
size="icon-sm"
variant="default"
onClick={() => handleSubmit()}
disabled={!inputValue.trim()}
className="size-7 cursor-pointer"
>
<Send className="size-3" />
</InputGroupButton>
)}
</InputGroupAddon>
</InputGroup>
</footer>
</div>
)
}
@@ -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</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
@@ -0,0 +1,26 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ErrorBoundary } from '@/components/ErrorBoundary'
import App from './App'
import '@/assets/index.css'
// Sync dark mode with system preference
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>
)
+70
View File
@@ -0,0 +1,70 @@
import type { HistoricalEvent } from '@page-agent/core'
import { type DBSchema, type IDBPDatabase, openDB } from 'idb'
const DB_NAME = 'page-agent-ext'
const DB_VERSION = 1
export interface SessionRecord {
id: string
task: string
history: HistoricalEvent[]
status: 'completed' | 'error'
createdAt: number
}
interface PageAgentDB extends DBSchema {
sessions: {
key: string
value: SessionRecord
indexes: { 'by-created': number }
}
}
let dbPromise: Promise<IDBPDatabase<PageAgentDB>> | null = null
function getDB() {
if (!dbPromise) {
dbPromise = openDB<PageAgentDB>(DB_NAME, DB_VERSION, {
upgrade(db) {
const store = db.createObjectStore('sessions', { keyPath: 'id' })
store.createIndex('by-created', 'createdAt')
},
})
}
return dbPromise
}
export async function saveSession(
session: Omit<SessionRecord, 'id' | 'createdAt'>
): Promise<SessionRecord> {
const db = await getDB()
const record: SessionRecord = {
...session,
id: crypto.randomUUID(),
createdAt: Date.now(),
}
await db.put('sessions', record)
return record
}
/** List sessions, newest first */
export async function listSessions(): Promise<SessionRecord[]> {
const db = await getDB()
const all = await db.getAllFromIndex('sessions', 'by-created')
return all.reverse()
}
export async function getSession(id: string): Promise<SessionRecord | undefined> {
const db = await getDB()
return db.get('sessions', id)
}
export async function deleteSession(id: string): Promise<void> {
const db = await getDB()
await db.delete('sessions', id)
}
export async function clearSessions(): Promise<void> {
const db = await getDB()
await db.clear('sessions')
}
@@ -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')
}
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+25
View File
@@ -0,0 +1,25 @@
// Asset type declarations
declare module '*.webp' {
const src: string
export default src
}
declare module '*.png' {
const src: string
export default src
}
declare module '*.jpg' {
const src: string
export default src
}
declare module '*.jpeg' {
const src: string
export default src
}
declare module '*.svg' {
const src: string
export default src
}
+1
View File
@@ -0,0 +1 @@
declare const __VERSION__: string
+4
View File
@@ -0,0 +1,4 @@
declare module '*.md?raw' {
const content: string
export default content
}
+28
View File
@@ -0,0 +1,28 @@
{
"extends": "./.wxt/tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"useDefineForClassFields": true,
"noEmit": false,
"allowImportingTsExtensions": false,
"strictNullChecks": true,
"jsx": "react-jsx",
"baseUrl": ".",
"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"]
}
},
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" },
{ "path": "../core" },
{ "path": "../ui" }
]
}
+69
View File
@@ -0,0 +1,69 @@
import tailwindcss from '@tailwindcss/vite'
import { mkdirSync, readFileSync } from 'node:fs'
import { defineConfig } from 'wxt'
const chromeProfile = '.wxt/chrome-data'
mkdirSync(chromeProfile, { recursive: true })
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
// See https://wxt.dev/api/config.html
export default defineConfig({
srcDir: 'src',
modules: ['@wxt-dev/module-react'],
webExt: {
chromiumProfile: chromeProfile,
keepProfileChanges: true,
chromiumArgs: ['--hide-crash-restore-bubble'],
},
vite: () => ({
plugins: [tailwindcss()],
define: {
__VERSION__: JSON.stringify(pkg.version),
},
optimizeDeps: {
force: true,
},
build: {
minify: false,
chunkSizeWarningLimit: 2000,
cssCodeSplit: true,
rollupOptions: {
onwarn: function (message, handler) {
if (message.code === 'EVAL') return
handler(message)
},
},
},
}),
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__',
homepage_url: 'https://alibaba.github.io/page-agent/',
permissions: ['tabs', 'tabGroups', 'sidePanel', 'storage'],
host_permissions: ['<all_urls>'],
icons: {
64: 'assets/page-agent-64.png',
},
action: {
default_title: '__MSG_extActionTitle__',
},
web_accessible_resources: [
{
resources: ['main-world.js'],
matches: ['*://*/*'],
},
],
side_panel: {
default_path: 'sidepanel/index.html',
},
externally_connectable: {
matches: ['http://localhost/*'],
},
},
})
-41
View File
@@ -1,41 +0,0 @@
# @page-agent/llms
LLM client with a **reflection-before-action** mental model for page-agent.
## Why This Package Exists
The LLM module and the agent logic are inherently coupled. This package exists not to decouple them, but to **define the interface contract** between the LLM and the agent.
The core abstraction is the `MacroToolInput` — a structured output format that **forces the model to reflect before acting**.
## The Reflection-Before-Action Model
Every tool call must first output its reasoning state before the actual action:
```typescript
interface MacroToolInput {
// Reflection (mandatory before any action)
evaluation_previous_goal?: string // How well did the previous action work?
memory?: string // Key information to remember
next_goal?: string // What to accomplish next
// Action (the actual operation)
action: Record<string, any>
}
```
This design ensures that:
1. **The model evaluates its previous action** before deciding the next step
2. **Working memory is explicitly maintained** across conversation turns
3. **Goals are clearly stated**, making the agent's reasoning transparent and debuggable
## Key Components
| Export | Description |
|--------|-------------|
| `LLM` | Main LLM client class with retry logic |
| `MacroToolInput` | The reflection-before-action input schema |
| `AgentBrain` | Agent's thinking state (eval, memory, goal) |
| `LLMConfig` | Configuration for LLM connection |
| `parseLLMConfig` | Parse and apply defaults to config |
+46 -41
View File
@@ -1,43 +1,48 @@
{ {
"name": "@page-agent/llms", "name": "@page-agent/llms",
"version": "0.0.18", "version": "1.6.2",
"type": "module", "type": "module",
"main": "./dist/lib/page-agent-llms.js", "main": "./dist/lib/page-agent-llms.js",
"module": "./dist/lib/page-agent-llms.js", "module": "./dist/lib/page-agent-llms.js",
"types": "./dist/lib/index.d.ts", "types": "./dist/lib/index.d.ts",
"exports": { "exports": {
".": { ".": {
"types": "./dist/lib/index.d.ts", "types": "./dist/lib/index.d.ts",
"import": "./dist/lib/page-agent-llms.js", "import": "./dist/lib/page-agent-llms.js",
"default": "./dist/lib/page-agent-llms.js" "default": "./dist/lib/page-agent-llms.js"
} }
}, },
"files": [ "files": [
"dist/" "dist/"
], ],
"description": "LLM client with reflection-before-action mental model for page-agent", "description": "LLM client with reflection-before-action mental model for page-agent",
"keywords": [ "keywords": [
"page-agent", "page-agent",
"llm", "llm",
"openai", "openai",
"tool-calling", "tool-calling",
"agent" "agent"
], ],
"author": "Simon<gaomeng1900>", "author": "Simon<gaomeng1900>",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/alibaba/page-agent.git", "url": "https://github.com/alibaba/page-agent.git",
"directory": "packages/llms" "directory": "packages/llms"
}, },
"homepage": "https://alibaba.github.io/page-agent/", "homepage": "https://alibaba.github.io/page-agent/",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"", "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{}})\"" "postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
}, },
"dependencies": { "dependencies": {
"chalk": "^5.6.2", "chalk": "^5.6.2"
"zod": "^4.2.1" },
} "peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"zod": "^4.3.5"
}
} }
+107 -69
View File
@@ -1,62 +1,68 @@
/** /**
* OpenAI Client implementation * OpenAI Client implementation
* @note This client is only for demonstrating how to implement a LLM client.
* @note Use OpenAILenientClient instead.
*/ */
import * as z from 'zod/v4'
import { InvokeError, InvokeErrorType } from './errors' import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types' import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
import { modelPatch, zodToOpenAITool } from './utils' import { modelPatch, zodToOpenAITool } from './utils'
/** /**
* @deprecated Use OpenAILenientClient instead. * Client for OpenAI compatible APIs
*/ */
export class OpenAIClient implements LLMClient { export class OpenAIClient implements LLMClient {
config: LLMConfig config: Required<LLMConfig>
private fetch: typeof globalThis.fetch
constructor(config: LLMConfig) { constructor(config: Required<LLMConfig>) {
this.config = config this.config = config
this.fetch = config.customFetch
} }
async invoke( async invoke(
messages: Message[], messages: Message[],
tools: Record<string, Tool>, tools: Record<string, Tool>,
abortSignal?: AbortSignal abortSignal?: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult> { ): Promise<InvokeResult> {
// 1. Convert tools to OpenAI format // 1. Convert tools to OpenAI format
const openaiTools = Object.entries(tools).map(([name, tool]) => zodToOpenAITool(name, tool)) 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,
tool_choice: toolChoice,
}
modelPatch(requestBody)
// 2. Call API // 2. Call API
let response: Response let response: Response
try { try {
response = await fetch(`${this.config.baseURL}/chat/completions`, { response = await this.fetch(`${this.config.baseURL}/chat/completions`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`, ...(this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }),
}, },
body: JSON.stringify( body: JSON.stringify(requestBody),
modelPatch({
model: this.config.model,
temperature: this.config.temperature,
max_tokens: this.config.maxTokens,
messages,
tools: openaiTools,
// tool_choice: 'required',
tool_choice: { type: 'function', function: { name: 'AgentOutput' } },
// model specific params
// reasoning_effort: 'minimal',
// verbosity: 'low',
parallel_tool_calls: false,
})
),
signal: abortSignal, signal: abortSignal,
}) })
} catch (error: unknown) { } catch (error: unknown) {
// Network error const isAbortError = (error as any)?.name === 'AbortError'
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error) const errorMessage = isAbortError ? 'Network request aborted' : 'Network request failed'
if (!isAbortError) console.error(error)
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, errorMessage, error)
} }
// 3. Handle HTTP errors // 3. Handle HTTP errors
@@ -93,91 +99,122 @@ export class OpenAIClient implements LLMClient {
) )
} }
// 4. Parse and validate response
const data = await response.json() const data = await response.json()
// 4. Check finish_reason
const choice = data.choices?.[0] const choice = data.choices?.[0]
if (!choice) { if (!choice) {
throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', data) throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', data)
} }
// Check finish_reason
switch (choice.finish_reason) { switch (choice.finish_reason) {
case 'tool_calls': case 'tool_calls':
// ✅ Normal case 'function_call': // gemini
case 'stop': // some models use this even with tool calls
break break
case 'length': case 'length':
// ⚠️ Token limit reached
throw new InvokeError( throw new InvokeError(
InvokeErrorType.CONTEXT_LENGTH, InvokeErrorType.CONTEXT_LENGTH,
'Response truncated: max tokens reached', 'Response truncated: max tokens reached',
undefined,
data data
) )
case 'content_filter': case 'content_filter':
// ❌ Content filtered
throw new InvokeError( throw new InvokeError(
InvokeErrorType.CONTENT_FILTER, InvokeErrorType.CONTENT_FILTER,
'Content filtered by safety system', 'Content filtered by safety system',
undefined,
data data
) )
case 'stop':
// ❌ Did not call tool (we require tool call)
throw new InvokeError(InvokeErrorType.NO_TOOL_CALL, 'Model did not call any tool', data)
default: default:
throw new InvokeError( throw new InvokeError(
InvokeErrorType.UNKNOWN, InvokeErrorType.UNKNOWN,
`Unexpected finish_reason: ${choice.finish_reason}`, `Unexpected finish_reason: ${choice.finish_reason}`,
undefined,
data data
) )
} }
// 5. Parse tool call // Apply normalizeResponse if provided (for fixing format issues automatically)
const toolCall = choice.message?.tool_calls?.[0] const normalizedData = options?.normalizeResponse ? options.normalizeResponse(data) : data
if (!toolCall) { const normalizedChoice = (normalizedData as any).choices?.[0]
throw new InvokeError(InvokeErrorType.NO_TOOL_CALL, 'No tool call found in response', data)
}
const toolName = toolCall.function.name // Get tool name from response
const tool = tools[toolName] const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name
if (!tool) { if (!toolCallName) {
throw new InvokeError(InvokeErrorType.UNKNOWN, `Tool ${toolName} not found`, data)
}
// 6. Parse and validate arguments
let toolArgs: unknown
try {
toolArgs = JSON.parse(toolCall.function.arguments)
} catch (e) {
throw new InvokeError(InvokeErrorType.INVALID_TOOL_ARGS, 'Invalid JSON in tool arguments', e)
}
// Validate against zod schema
const validation = tool.inputSchema.safeParse(toolArgs)
if (!validation.success) {
throw new InvokeError( throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS, InvokeErrorType.NO_TOOL_CALL,
`Tool arguments validation failed: ${validation.error.message}`, 'No tool call found in response',
validation.error undefined,
data
) )
} }
// 7. Execute tool const tool = tools[toolCallName]
if (!tool) {
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`Tool "${toolCallName}" not found in tools`,
undefined,
data
)
}
// Extract and parse tool arguments
const argString = normalizedChoice.message?.tool_calls?.[0]?.function?.arguments
if (!argString) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'No tool call arguments found',
undefined,
data
)
}
let parsedArgs: unknown
try {
parsedArgs = JSON.parse(argString)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Failed to parse tool arguments as JSON',
error,
data
)
}
// Validate with schema
const validation = tool.inputSchema.safeParse(parsedArgs)
if (!validation.success) {
console.error(z.prettifyError(validation.error))
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Tool arguments validation failed',
validation.error,
data
)
}
const toolInput = validation.data
// 5. Execute tool
let toolResult: unknown let toolResult: unknown
try { try {
toolResult = await tool.execute(validation.data) toolResult = await tool.execute(toolInput)
} catch (e) { } catch (e) {
throw new InvokeError( throw new InvokeError(
InvokeErrorType.TOOL_EXECUTION_ERROR, InvokeErrorType.TOOL_EXECUTION_ERROR,
`Tool execution failed: ${(e as Error).message}`, `Tool execution failed: ${(e as Error).message}`,
e e,
data
) )
} }
// 8. Return result (including cache tokens) // Return result
return { return {
toolCall: { toolCall: {
// id: toolCall.id, name: toolCallName,
name: toolName, args: toolInput,
args: validation.data as Record<string, unknown>,
}, },
toolResult, toolResult,
usage: { usage: {
@@ -188,6 +225,7 @@ export class OpenAIClient implements LLMClient {
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens, reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens,
}, },
rawResponse: data, rawResponse: data,
rawRequest: requestBody,
} }
} }
} }
-125
View File
@@ -1,125 +0,0 @@
/**
* OpenAI Client implementation
*/
import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeResult, LLMClient, LLMConfig, MacroToolInput, Message, Tool } from './types'
import { lenientParseMacroToolCall, modelPatch, zodToOpenAITool } from './utils'
export class OpenAIClient implements LLMClient {
config: Required<LLMConfig>
private fetch: typeof globalThis.fetch
constructor(config: Required<LLMConfig>) {
this.config = config
this.fetch = config.customFetch
}
async invoke(
messages: Message[],
tools: { AgentOutput: Tool<MacroToolInput> },
abortSignal?: AbortSignal
): Promise<InvokeResult> {
// 1. Convert tools to OpenAI format
const openaiTools = Object.entries(tools).map(([name, tool]) => zodToOpenAITool(name, tool))
// 2. Call API
let response: Response
try {
response = await this.fetch(`${this.config.baseURL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`,
},
body: JSON.stringify(
modelPatch({
model: this.config.model,
temperature: this.config.temperature,
max_tokens: this.config.maxTokens,
messages,
tools: openaiTools,
// tool_choice: 'required',
tool_choice: { type: 'function', function: { name: 'AgentOutput' } },
parallel_tool_calls: false,
})
),
signal: abortSignal,
})
} catch (error: unknown) {
// Network error
console.error(error)
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error)
}
// 3. Handle HTTP errors
if (!response.ok) {
const errorData = await response.json().catch()
const errorMessage =
(errorData as { error?: { message?: string } }).error?.message || response.statusText
if (response.status === 401 || response.status === 403) {
throw new InvokeError(
InvokeErrorType.AUTH_ERROR,
`Authentication failed: ${errorMessage}`,
errorData
)
}
if (response.status === 429) {
throw new InvokeError(
InvokeErrorType.RATE_LIMIT,
`Rate limit exceeded: ${errorMessage}`,
errorData
)
}
if (response.status >= 500) {
throw new InvokeError(
InvokeErrorType.SERVER_ERROR,
`Server error: ${errorMessage}`,
errorData
)
}
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`HTTP ${response.status}: ${errorMessage}`,
errorData
)
}
// parse response
const data = await response.json()
const tool = tools.AgentOutput
const macroToolInput = lenientParseMacroToolCall(data, tool.inputSchema as any)
// Execute tool
let toolResult: unknown
try {
toolResult = await tool.execute(macroToolInput)
} catch (e) {
throw new InvokeError(
InvokeErrorType.TOOL_EXECUTION_ERROR,
`Tool execution failed: ${(e as Error).message}`,
e
)
}
// Return result (including cache tokens)
return {
toolCall: {
// id: toolCall.id,
name: 'AgentOutput',
args: macroToolInput,
},
toolResult,
usage: {
promptTokens: data.usage?.prompt_tokens ?? 0,
completionTokens: data.usage?.completion_tokens ?? 0,
totalTokens: data.usage?.total_tokens ?? 0,
cachedTokens: data.usage?.prompt_tokens_details?.cached_tokens,
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens,
},
rawResponse: data,
}
}
}

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