From 0b46fd1d7a67a9b231787d9773051402860ef96d Mon Sep 17 00:00:00 2001 From: Simon <10131203+gaomeng1900@users.noreply.github.com> Date: Mon, 15 Dec 2025 17:02:45 +0800 Subject: [PATCH 1/7] refactor(agent): decouple `ui` and `agent`; disable EventBus for now --- AGENTS.md | 20 -- packages/page-agent/src/PageAgent.ts | 87 ++++--- packages/page-agent/src/llms/index.ts | 25 +- packages/page-agent/src/ui/Panel.ts | 331 ++++++++++++++++---------- packages/page-agent/src/umd.ts | 2 +- packages/page-agent/src/utils/bus.ts | 11 +- 6 files changed, 258 insertions(+), 218 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8cc1d22..d4a82b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,19 +107,6 @@ DOM element references and internal state (selectorMap, elementTextMap) are enca 3. **LLM Processing**: AI model returns action plans (in page-agent) 4. **Indexed Operations**: PageAgent calls PageController methods by element index -### Event Bus Communication - -Use `src/utils/bus.ts` for decoupled PageAgent ↔ UI communication: - -```typescript -// Emit from PageAgent -getEventBus().emit('panel:show') -getEventBus().emit('panel:update', { status: 'thinking' }) - -// Listen in UI components -getEventBus().on('panel:show', () => panel.show()) -``` - ### Hash Routing Requirement Uses wouter with `useHashLocation` for static hosting: @@ -147,7 +134,6 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`. | `src/PageAgent.ts` | ⭐ Main AI agent class orchestrating tools and LLM | | `src/entry.ts` | CDN/UMD entry point with auto-initialization | | `src/tools/` | Tool definitions that call PageController methods | -| `src/utils/bus.ts` | Type-safe event bus for decoupled communication | | `src/ui/` | UI components (Panel, SimulatorMask) with CSS modules | | `src/llms/` | LLM integration and communication layer | | `vite.config.js` | Library build configuration (ES + UMD) | @@ -194,11 +180,6 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`. 2. Expose via async method in `PageController.ts` 3. Export from `packages/page-controller/src/index.ts` -### New UI Component - -1. Create in `packages/page-agent/src/ui/` with colocated CSS modules -2. Use event bus for PageAgent communication - ## Code Standards ### TypeScript @@ -233,5 +214,4 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`. 1. Check `packages/page-agent/dist/lib/page-agent.umd.js` builds correctly 2. Test CDN injection with query params -3. Verify event bus communications are properly typed 4. Use `packages/website/src/test-pages/` for isolated testing diff --git a/packages/page-agent/src/PageAgent.ts b/packages/page-agent/src/PageAgent.ts index d80c969..412d992 100644 --- a/packages/page-agent/src/PageAgent.ts +++ b/packages/page-agent/src/PageAgent.ts @@ -8,15 +8,13 @@ import zod from 'zod' import type { PageAgentConfig } from './config' import { MAX_STEPS } from './config/constants' -import { I18n } from './i18n' import { LLM, type Tool } from './llms' import SYSTEM_PROMPT from './prompts/system_prompt.md?raw' import { tools } from './tools' -import { Panel, getToolCompletedText, getToolExecutingText } from './ui/Panel' +import { Panel } from './ui/Panel' import { SimulatorMask } from './ui/SimulatorMask' import { trimLines, uid, waitUntil } from './utils' import { assert } from './utils/assert' -import { getEventBus } from './utils/bus' export type { PageAgentConfig } export { tool, type PageAgentTool } from './tools' @@ -71,8 +69,6 @@ export interface ExecutionResult { export class PageAgent extends EventTarget { config: PageAgentConfig id = uid() - bus = getEventBus(this.id) - i18n: I18n panel: Panel tools: typeof tools paused = false @@ -96,14 +92,32 @@ export class PageAgent extends EventTarget { super() this.config = config - this.#llm = new LLM(this.config, this.id) - this.i18n = new I18n(this.config.language) - this.panel = new Panel(this) + this.#llm = new LLM(this.config) + this.panel = new Panel({ + language: this.config.language, + onExecuteTask: (task) => this.execute(task), + onStop: () => this.dispose(), + onPauseToggle: () => { + this.paused = !this.paused + return this.paused + }, + getPaused: () => this.paused, + }) this.tools = new Map(tools) // Initialize PageController with config this.pageController = new PageController(this.config) + // Listen to LLM events + this.#llm.addEventListener('retry', (e) => { + const { current, max } = (e as CustomEvent).detail + this.panel.update({ type: 'retry', current, max }) + }) + this.#llm.addEventListener('error', (e) => { + const { error } = (e as CustomEvent).detail + this.panel.update({ type: 'error', message: `step failed: ${error.message}` }) + }) + if (this.config.customTools) { for (const [name, tool] of Object.entries(this.config.customTools)) { if (tool === null) { @@ -141,13 +155,10 @@ export class PageAgent extends EventTarget { // Show mask and panel this.mask.show() - this.bus.emit('panel:show') - this.bus.emit('panel:reset') + this.panel.show() + this.panel.reset() - this.bus.emit('panel:update', { - type: 'input', - displayText: this.task, - }) + this.panel.update({ type: 'input', task: this.task }) if (this.#abortController) { this.#abortController.abort() @@ -171,10 +182,7 @@ export class PageAgent extends EventTarget { // Update status to thinking console.log(chalk.blue('Thinking...')) - this.bus.emit('panel:update', { - type: 'thinking', - displayText: this.i18n.t('ui.panel.thinking'), - }) + this.panel.update({ type: 'thinking' }) const result = await this.#llm.invoke( [ @@ -304,22 +312,14 @@ export class PageAgent extends EventTarget { `) console.log(brain) - this.bus.emit('panel:update', { - type: 'thinking', - displayText: brain, - }) + this.panel.update({ type: 'thinking', text: brain }) // Find the corresponding tool const tool = tools.get(toolName) assert(tool, `Tool ${toolName} not found. (@note should have been caught before this!!!)`) console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput) - this.bus.emit('panel:update', { - type: 'tool_executing', - toolName, - toolArgs: toolInput, - displayText: getToolExecutingText(toolName, toolInput, this.i18n), - }) + this.panel.update({ type: 'toolExecuting', toolName, args: toolInput }) const startTime = Date.now() @@ -341,16 +341,13 @@ export class PageAgent extends EventTarget { } // Briefly display execution result - const displayResult = getToolCompletedText(toolName, toolInput, this.i18n) - if (displayResult) - this.bus.emit('panel:update', { - type: 'tool_executing', - toolName, - toolArgs: toolInput, - toolResult: result, - displayText: displayResult, - duration, - }) + this.panel.update({ + type: 'toolCompleted', + toolName, + args: toolInput, + result, + duration, + }) // Wait a moment to let user see the result await new Promise((resolve) => setTimeout(resolve, 100)) @@ -426,16 +423,14 @@ export class PageAgent extends EventTarget { this.pageController.cleanUpHighlights() // Update panel status - this.bus.emit('panel:update', { - type: success ? 'output' : 'error', - displayText: text, - }) + if (success) { + this.panel.update({ type: 'output', text }) + } else { + this.panel.update({ type: 'error', message: text }) + } // Task completed - this.bus.emit('panel:update', { - type: 'completed', - displayText: this.i18n.t('ui.panel.taskCompleted'), - }) + this.panel.update({ type: 'completed' }) this.mask.hide() diff --git a/packages/page-agent/src/llms/index.ts b/packages/page-agent/src/llms/index.ts index 2cc5379..a94992f 100644 --- a/packages/page-agent/src/llms/index.ts +++ b/packages/page-agent/src/llms/index.ts @@ -33,24 +33,19 @@ */ import type { LLMConfig } from '../config' import { parseLLMConfig } from '../config' -import { EventBus, getEventBus } from '../utils/bus' import { OpenAIClient } from './OpenAILenientClient' import { InvokeError } from './errors' import type { InvokeResult, LLMClient, Message, Tool } from './types' export type { Message, Tool, InvokeResult, LLMClient } -export class LLM { +export class LLM extends EventTarget { config: Required - id: string client: LLMClient - #bus: EventBus - constructor(config: LLMConfig, id: string) { + constructor(config: LLMConfig) { + super() this.config = parseLLMConfig(config) - this.id = id - - this.#bus = getEventBus(id) // Default to OpenAI client this.client = new OpenAIClient({ @@ -81,17 +76,13 @@ export class LLM { // retry settings { maxRetries: this.config.maxRetries, - onRetry: (retries: number) => { - this.#bus.emit('panel:update', { - type: 'retry', - displayText: `retry-ing (${retries} / ${this.config.maxRetries})`, - }) + onRetry: (current: number) => { + this.dispatchEvent( + new CustomEvent('retry', { detail: { current, max: this.config.maxRetries } }) + ) }, onError: (error: Error) => { - this.#bus.emit('panel:update', { - type: 'error', - displayText: `step failed: ${(error as Error).message}`, - }) + this.dispatchEvent(new CustomEvent('error', { detail: { error } })) }, } ) diff --git a/packages/page-agent/src/ui/Panel.ts b/packages/page-agent/src/ui/Panel.ts index 6225701..ab2a748 100644 --- a/packages/page-agent/src/ui/Panel.ts +++ b/packages/page-agent/src/ui/Panel.ts @@ -1,11 +1,35 @@ -import type { PageAgent } from '../PageAgent' -import type { I18n } from '../i18n' +import { I18n, type SupportedLanguage } from '../i18n' import { truncate } from '../utils' -import type { EventBus } from '../utils/bus' import { type Step, UIState } from './UIState' import styles from './Panel.module.css' +/** + * Panel configuration + */ +export interface PanelConfig { + language?: SupportedLanguage + onExecuteTask: (task: string) => void + onStop: () => void + onPauseToggle: () => boolean // returns new paused state + getPaused: () => boolean +} + +/** + * Semantic update types - Panel handles i18n internally + */ +export type PanelUpdate = + | { type: 'thinking'; text?: string } // text is optional, defaults to i18n thinking text + | { type: 'input'; task: string } + | { type: 'question'; question: string } + | { type: 'userAnswer'; input: string } + | { type: 'retry'; current: number; max: number } + | { type: 'error'; message: string } + | { type: 'output'; text: string } + | { type: 'completed' } + | { type: 'toolExecuting'; toolName: string; args: any } + | { type: 'toolCompleted'; toolName: string; args: any; result?: string; duration?: number } + /** * Agent control panel */ @@ -19,11 +43,11 @@ export class Panel { #stopButton: HTMLElement #inputSection: HTMLElement #taskInput: HTMLInputElement - #bus: EventBus #state = new UIState() #isExpanded = false - #pageAgent: PageAgent + #config: PanelConfig + #i18n: I18n #userAnswerResolver: ((input: string) => void) | null = null #isWaitingForUserAnswer: boolean = false #headerUpdateTimer: ReturnType | null = null @@ -34,9 +58,9 @@ export class Panel { return this.#wrapper } - constructor(pageAgent: PageAgent) { - this.#pageAgent = pageAgent - this.#bus = pageAgent.bus + constructor(config: PanelConfig) { + this.#config = config + this.#i18n = new I18n(config.language ?? 'en-US') this.#wrapper = this.#createWrapper() this.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`)! this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)! @@ -49,16 +73,8 @@ export class Panel { this.#setupEventListeners() this.#startHeaderUpdateLoop() - // this.#expand() // debug this.#showInputArea() - - this.#bus.on('panel:show', () => this.#show()) - this.#bus.on('panel:hide', () => this.#hide()) - this.#bus.on('panel:reset', () => this.#reset()) - this.#bus.on('panel:update', (stepData) => this.#update(stepData)) - this.#bus.on('panel:expand', () => this.#expand()) - this.#bus.on('panel:collapse', () => this.#collapse()) } /** @@ -71,18 +87,67 @@ export class Panel { this.#userAnswerResolver = resolve // Update state to `running` - this.#update({ + this.#updateInternal({ type: 'output', - displayText: this.#pageAgent.i18n.t('ui.panel.question', { question }), + displayText: this.#i18n.t('ui.panel.question', { question }), }) // Expand history panel if (!this.#isExpanded) { this.#expand() } - this.#showInputArea(this.#pageAgent.i18n.t('ui.panel.userAnswerPrompt')) + this.#showInputArea(this.#i18n.t('ui.panel.userAnswerPrompt')) }) } + // ========== Public control methods ========== + + show(): void { + this.wrapper.style.display = 'block' + void this.wrapper.offsetHeight + this.wrapper.style.opacity = '1' + this.wrapper.style.transform = 'translateX(-50%) translateY(0)' + } + + hide(): void { + this.wrapper.style.opacity = '0' + this.wrapper.style.transform = 'translateX(-50%) translateY(20px)' + this.wrapper.style.display = 'none' + } + + reset(): void { + this.#state.reset() + this.#statusText.textContent = this.#i18n.t('ui.panel.ready') + this.#updateStatusIndicator('thinking') + this.#updateHistory() + this.#collapse() + // Reset pause state via callback + if (this.#config.getPaused()) { + this.#config.onPauseToggle() + } + this.#updatePauseButton() + // Reset user input state + this.#isWaitingForUserAnswer = false + this.#userAnswerResolver = null + // Show input area + this.#showInputArea() + } + + expand(): void { + this.#expand() + } + + collapse(): void { + this.#collapse() + } + + /** + * Update panel with semantic data - i18n handled internally + */ + update(data: PanelUpdate): void { + const stepData = this.#toStepData(data) + this.#updateInternal(stepData) + } + /** * Dispose panel */ @@ -92,10 +157,102 @@ export class Panel { this.wrapper.remove() } + // ========== Private methods ========== + /** - * Update status + * Convert semantic update to step data with i18n */ - #update(stepData: Omit): void { + #toStepData(data: PanelUpdate): Omit { + switch (data.type) { + case 'thinking': + return { type: 'thinking', displayText: data.text ?? this.#i18n.t('ui.panel.thinking') } + case 'input': + return { type: 'input', displayText: data.task } + case 'question': + return { + type: 'output', + displayText: this.#i18n.t('ui.panel.question', { question: data.question }), + } + case 'userAnswer': + return { + type: 'input', + displayText: this.#i18n.t('ui.panel.userAnswer', { input: data.input }), + } + case 'retry': + return { type: 'retry', displayText: `retry-ing (${data.current} / ${data.max})` } + case 'error': + return { type: 'error', displayText: data.message } + case 'output': + return { type: 'output', displayText: data.text } + case 'completed': + return { type: 'completed', displayText: this.#i18n.t('ui.panel.taskCompleted') } + case 'toolExecuting': + return { + type: 'tool_executing', + toolName: data.toolName, + toolArgs: data.args, + displayText: this.#getToolExecutingText(data.toolName, data.args), + } + case 'toolCompleted': { + const displayText = this.#getToolCompletedText(data.toolName, data.args) + if (!displayText) return { type: 'tool_executing', displayText: '' } // will be filtered + return { + type: 'tool_executing', + toolName: data.toolName, + toolArgs: data.args, + toolResult: data.result, + displayText, + duration: data.duration, + } + } + } + } + + #getToolExecutingText(toolName: string, args: any): string { + switch (toolName) { + case 'click_element_by_index': + return this.#i18n.t('ui.tools.clicking', { index: args.index }) + case 'input_text': + return this.#i18n.t('ui.tools.inputting', { index: args.index }) + case 'select_dropdown_option': + return this.#i18n.t('ui.tools.selecting', { text: args.text }) + case 'scroll': + return this.#i18n.t('ui.tools.scrolling') + case 'wait': + return this.#i18n.t('ui.tools.waiting', { seconds: args.seconds }) + case 'done': + return this.#i18n.t('ui.tools.done') + default: + return this.#i18n.t('ui.tools.executing', { toolName }) + } + } + + #getToolCompletedText(toolName: string, args: any): string | null { + switch (toolName) { + case 'click_element_by_index': + return this.#i18n.t('ui.tools.clicked', { index: args.index }) + case 'input_text': + return this.#i18n.t('ui.tools.inputted', { text: args.text }) + case 'select_dropdown_option': + return this.#i18n.t('ui.tools.selected', { text: args.text }) + case 'scroll': + return this.#i18n.t('ui.tools.scrolled') + case 'wait': + return this.#i18n.t('ui.tools.waited') + case 'done': + return null + default: + return null + } + } + + /** + * Update status (internal) + */ + #updateInternal(stepData: Omit): void { + // Skip empty displayText (filtered toolCompleted for 'done') + if (!stepData.displayText) return + const step = this.#state.addStep(stepData) // Queue header text update (will be processed by periodic check) @@ -120,59 +277,20 @@ export class Panel { } } - /** - * Show panel - */ - #show(): void { - this.wrapper.style.display = 'block' - // Force reflow to trigger animation - void this.wrapper.offsetHeight - this.wrapper.style.opacity = '1' - this.wrapper.style.transform = 'translateX(-50%) translateY(0)' - } - - /** - * Hide panel - */ - #hide(): void { - this.wrapper.style.opacity = '0' - this.wrapper.style.transform = 'translateX(-50%) translateY(20px)' - this.wrapper.style.display = 'none' - } - - /** - * Reset state - */ - #reset(): void { - this.#state.reset() - this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.ready') - this.#updateStatusIndicator('thinking') - this.#updateHistory() - this.#collapse() - // Reset pause state - this.#pageAgent.paused = false - this.#updatePauseButton() - // Reset user input state - this.#isWaitingForUserAnswer = false - this.#userAnswerResolver = null - // Show input area - this.#showInputArea() - } - /** * Toggle pause state */ #togglePause(): void { - this.#pageAgent.paused = !this.#pageAgent.paused + const paused = this.#config.onPauseToggle() this.#updatePauseButton() // Update status display - if (this.#pageAgent.paused) { - this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.paused') - this.#updateStatusIndicator('thinking') // Use existing thinking state + if (paused) { + this.#statusText.textContent = this.#i18n.t('ui.panel.paused') + this.#updateStatusIndicator('thinking') } else { - this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.continueExecution') - this.#updateStatusIndicator('tool_executing') // Restore to execution state + this.#statusText.textContent = this.#i18n.t('ui.panel.continueExecution') + this.#updateStatusIndicator('tool_executing') } } @@ -180,13 +298,14 @@ export class Panel { * Update pause button state */ #updatePauseButton(): void { - if (this.#pageAgent.paused) { + const paused = this.#config.getPaused() + if (paused) { this.#pauseButton.textContent = '▶' - this.#pauseButton.title = this.#pageAgent.i18n.t('ui.panel.continue') + this.#pauseButton.title = this.#i18n.t('ui.panel.continue') this.#pauseButton.classList.add(styles.paused) } else { this.#pauseButton.textContent = '⏸︎' - this.#pauseButton.title = this.#pageAgent.i18n.t('ui.panel.pause') + this.#pauseButton.title = this.#i18n.t('ui.panel.pause') this.#pauseButton.classList.remove(styles.paused) } } @@ -196,12 +315,12 @@ export class Panel { */ #stopAgent(): void { // Update status display - this.#update({ + this.#updateInternal({ type: 'error', - displayText: this.#pageAgent.i18n.t('ui.panel.taskTerminated'), + displayText: this.#i18n.t('ui.panel.taskTerminated'), }) - this.#pageAgent.dispose() + this.#config.onStop() } /** @@ -218,7 +337,7 @@ export class Panel { // Handle user input mode this.#handleUserAnswer(input) } else { - this.#pageAgent.execute(input) + this.#config.onExecuteTask(input) } } @@ -227,9 +346,9 @@ export class Panel { */ #handleUserAnswer(input: string): void { // Add user input to history - this.#update({ + this.#updateInternal({ type: 'input', - displayText: this.#pageAgent.i18n.t('ui.panel.userAnswer', { input }), + displayText: this.#i18n.t('ui.panel.userAnswer', { input }), }) // Reset state @@ -248,7 +367,7 @@ export class Panel { #showInputArea(placeholder?: string): void { // Clear input field this.#taskInput.value = '' - this.#taskInput.placeholder = placeholder || this.#pageAgent.i18n.t('ui.panel.taskInput') + this.#taskInput.placeholder = placeholder || this.#i18n.t('ui.panel.taskInput') this.#inputSection.classList.remove(styles.hidden) // Focus on input field setTimeout(() => { @@ -294,23 +413,23 @@ export class Panel { stepNumber: 0, timestamp: new Date(), type: 'thinking', - displayText: this.#pageAgent.i18n.t('ui.panel.waitingPlaceholder'), + displayText: this.#i18n.t('ui.panel.waitingPlaceholder'), })}
-
${this.#pageAgent.i18n.t('ui.panel.ready')}
+
${this.#i18n.t('ui.panel.ready')}
- - -
@@ -501,8 +620,8 @@ export class Panel { // Check if this is a result from done tool if (step.toolName === 'done') { // Judge success or failure based on result - const failureKeyword = this.#pageAgent.i18n.t('ui.tools.resultFailure') - const errorKeyword = this.#pageAgent.i18n.t('ui.tools.resultError') + const failureKeyword = this.#i18n.t('ui.tools.resultFailure') + const errorKeyword = this.#i18n.t('ui.tools.resultError') const isSuccess = !step.toolResult || (!step.toolResult.includes(failureKeyword) && !step.toolResult.includes(errorKeyword)) @@ -531,7 +650,7 @@ export class Panel { } const durationText = step.duration ? ` · ${step.duration}ms` : '' - const stepLabel = this.#pageAgent.i18n.t('ui.panel.step', { + const stepLabel = this.#i18n.t('ui.panel.step', { number: step.stepNumber.toString(), time, duration: durationText || '', // Explicitly pass empty string to replace template @@ -550,47 +669,3 @@ export class Panel { ` } } - -/** - * Get display text for tool execution - */ -export function getToolExecutingText(toolName: string, args: any, i18n: I18n): string { - switch (toolName) { - case 'click_element_by_index': - return i18n.t('ui.tools.clicking', { index: args.index }) - case 'input_text': - return i18n.t('ui.tools.inputting', { index: args.index }) - case 'select_dropdown_option': - return i18n.t('ui.tools.selecting', { text: args.text }) - case 'scroll': - return i18n.t('ui.tools.scrolling') - case 'wait': - return i18n.t('ui.tools.waiting', { seconds: args.seconds }) - case 'done': - return i18n.t('ui.tools.done') - default: - return i18n.t('ui.tools.executing', { toolName }) - } -} - -/** - * Get display text for tool completion - */ -export function getToolCompletedText(toolName: string, args: any, i18n: I18n): string | null { - switch (toolName) { - case 'click_element_by_index': - return i18n.t('ui.tools.clicked', { index: args.index }) - case 'input_text': - return i18n.t('ui.tools.inputted', { text: args.text }) - case 'select_dropdown_option': - return i18n.t('ui.tools.selected', { text: args.text }) - case 'scroll': - return i18n.t('ui.tools.scrolled') - case 'wait': - return i18n.t('ui.tools.waited') - case 'done': - return null - default: - return null - } -} diff --git a/packages/page-agent/src/umd.ts b/packages/page-agent/src/umd.ts index 9ea62da..3d9bc05 100644 --- a/packages/page-agent/src/umd.ts +++ b/packages/page-agent/src/umd.ts @@ -37,4 +37,4 @@ if (currentScript) { console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config) -window.pageAgent.bus.emit('panel:show') // Show panel +window.pageAgent.panel.show() // Show panel diff --git a/packages/page-agent/src/utils/bus.ts b/packages/page-agent/src/utils/bus.ts index be4a4bd..554e420 100644 --- a/packages/page-agent/src/utils/bus.ts +++ b/packages/page-agent/src/utils/bus.ts @@ -1,7 +1,6 @@ /** * Type-safe event bus for decoupling PageAgent and Panel */ -import type { Step } from '../ui/UIState' /** * Event mapping definitions @@ -12,15 +11,15 @@ export interface PageAgentEventMap { // call panel.show() 'panel:show': { params: undefined } // call panel.hide() - 'panel:hide': { params: undefined } + // 'panel:hide': { params: undefined } // call panel.reset() - 'panel:reset': { params: undefined } + // 'panel:reset': { params: undefined } // call panel.update() - 'panel:update': { params: Omit } + // 'panel:update': { params: Omit } // call panel.expand() - 'panel:expand': { params: undefined } + // 'panel:expand': { params: undefined } // call panel.collapse() - 'panel:collapse': { params: undefined } + // 'panel:collapse': { params: undefined } // PageAgent status events // 'agent:execute': { params: { task: string } } From 2b8b6ef86af8418bfd125c21bace9f51f81e33cb Mon Sep 17 00:00:00 2001 From: Simon <10131203+gaomeng1900@users.noreply.github.com> Date: Mon, 15 Dec 2025 17:20:12 +0800 Subject: [PATCH 2/7] refactor(ui): move ui and i18n to new package --- packages/{page-agent/src/ui => ui/src}/Panel.module.css | 0 packages/{page-agent/src/ui => ui/src}/Panel.ts | 0 packages/{page-agent/src/ui => ui/src}/SimulatorMask.module.css | 0 packages/{page-agent/src/ui => ui/src}/SimulatorMask.ts | 0 packages/{page-agent/src/ui => ui/src}/UIState.ts | 0 packages/{page-agent/src/utils => ui/src}/checkDarkMode.ts | 0 packages/{page-agent/src/ui => ui/src}/cursor.module.css | 0 packages/{page-agent => ui}/src/i18n/index.ts | 0 packages/{page-agent => ui}/src/i18n/locales.ts | 0 packages/{page-agent/src/ui => ui/src}/motion-css/createMotion.ts | 0 .../{page-agent/src/ui => ui/src}/motion-css/motion.module.css | 0 packages/{page-agent/src/ui => ui/src}/motion-css/readme | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename packages/{page-agent/src/ui => ui/src}/Panel.module.css (100%) rename packages/{page-agent/src/ui => ui/src}/Panel.ts (100%) rename packages/{page-agent/src/ui => ui/src}/SimulatorMask.module.css (100%) rename packages/{page-agent/src/ui => ui/src}/SimulatorMask.ts (100%) rename packages/{page-agent/src/ui => ui/src}/UIState.ts (100%) rename packages/{page-agent/src/utils => ui/src}/checkDarkMode.ts (100%) rename packages/{page-agent/src/ui => ui/src}/cursor.module.css (100%) rename packages/{page-agent => ui}/src/i18n/index.ts (100%) rename packages/{page-agent => ui}/src/i18n/locales.ts (100%) rename packages/{page-agent/src/ui => ui/src}/motion-css/createMotion.ts (100%) rename packages/{page-agent/src/ui => ui/src}/motion-css/motion.module.css (100%) rename packages/{page-agent/src/ui => ui/src}/motion-css/readme (100%) diff --git a/packages/page-agent/src/ui/Panel.module.css b/packages/ui/src/Panel.module.css similarity index 100% rename from packages/page-agent/src/ui/Panel.module.css rename to packages/ui/src/Panel.module.css diff --git a/packages/page-agent/src/ui/Panel.ts b/packages/ui/src/Panel.ts similarity index 100% rename from packages/page-agent/src/ui/Panel.ts rename to packages/ui/src/Panel.ts diff --git a/packages/page-agent/src/ui/SimulatorMask.module.css b/packages/ui/src/SimulatorMask.module.css similarity index 100% rename from packages/page-agent/src/ui/SimulatorMask.module.css rename to packages/ui/src/SimulatorMask.module.css diff --git a/packages/page-agent/src/ui/SimulatorMask.ts b/packages/ui/src/SimulatorMask.ts similarity index 100% rename from packages/page-agent/src/ui/SimulatorMask.ts rename to packages/ui/src/SimulatorMask.ts diff --git a/packages/page-agent/src/ui/UIState.ts b/packages/ui/src/UIState.ts similarity index 100% rename from packages/page-agent/src/ui/UIState.ts rename to packages/ui/src/UIState.ts diff --git a/packages/page-agent/src/utils/checkDarkMode.ts b/packages/ui/src/checkDarkMode.ts similarity index 100% rename from packages/page-agent/src/utils/checkDarkMode.ts rename to packages/ui/src/checkDarkMode.ts diff --git a/packages/page-agent/src/ui/cursor.module.css b/packages/ui/src/cursor.module.css similarity index 100% rename from packages/page-agent/src/ui/cursor.module.css rename to packages/ui/src/cursor.module.css diff --git a/packages/page-agent/src/i18n/index.ts b/packages/ui/src/i18n/index.ts similarity index 100% rename from packages/page-agent/src/i18n/index.ts rename to packages/ui/src/i18n/index.ts diff --git a/packages/page-agent/src/i18n/locales.ts b/packages/ui/src/i18n/locales.ts similarity index 100% rename from packages/page-agent/src/i18n/locales.ts rename to packages/ui/src/i18n/locales.ts diff --git a/packages/page-agent/src/ui/motion-css/createMotion.ts b/packages/ui/src/motion-css/createMotion.ts similarity index 100% rename from packages/page-agent/src/ui/motion-css/createMotion.ts rename to packages/ui/src/motion-css/createMotion.ts diff --git a/packages/page-agent/src/ui/motion-css/motion.module.css b/packages/ui/src/motion-css/motion.module.css similarity index 100% rename from packages/page-agent/src/ui/motion-css/motion.module.css rename to packages/ui/src/motion-css/motion.module.css diff --git a/packages/page-agent/src/ui/motion-css/readme b/packages/ui/src/motion-css/readme similarity index 100% rename from packages/page-agent/src/ui/motion-css/readme rename to packages/ui/src/motion-css/readme From d1c8ca81975afac4ff6dd1604775eb818aed1891 Mon Sep 17 00:00:00 2001 From: Simon <10131203+gaomeng1900@users.noreply.github.com> Date: Mon, 15 Dec 2025 17:34:12 +0800 Subject: [PATCH 3/7] refactor(ui): implement dedicated package `@page-agent/ui` --- package-lock.json | 15 ++++++++- package.json | 1 + packages/page-agent/package.json | 8 ++--- packages/page-agent/src/PageAgent.ts | 3 +- packages/page-agent/src/config/index.ts | 2 +- packages/page-agent/vite.config.js | 1 + packages/page-controller/package.json | 4 +-- packages/ui/package.json | 43 +++++++++++++++++++++++++ packages/ui/src/Panel.ts | 4 +-- packages/ui/src/SimulatorMask.ts | 2 +- packages/ui/src/env.d.ts | 6 ++++ packages/ui/src/index.ts | 4 +++ packages/ui/src/utils.ts | 6 ++++ packages/ui/tsconfig.dts.json | 10 ++++++ packages/ui/tsconfig.json | 13 ++++++++ packages/ui/vite.config.js | 41 +++++++++++++++++++++++ packages/website/vite.config.js | 1 + tsconfig.json | 1 + 18 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 packages/ui/package.json create mode 100644 packages/ui/src/env.d.ts create mode 100644 packages/ui/src/index.ts create mode 100644 packages/ui/src/utils.ts create mode 100644 packages/ui/tsconfig.dts.json create mode 100644 packages/ui/tsconfig.json create mode 100644 packages/ui/vite.config.js diff --git a/package-lock.json b/package-lock.json index fa7fb15..5d8510c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "workspaces": [ "packages/page-controller", + "packages/ui", "packages/page-agent", "packages/website" ], @@ -1560,6 +1561,10 @@ "resolved": "packages/page-controller", "link": true }, + "node_modules/@page-agent/ui": { + "resolved": "packages/ui", + "link": true + }, "node_modules/@page-agent/website": { "resolved": "packages/website", "link": true @@ -7003,7 +7008,7 @@ "license": "MIT", "dependencies": { "@page-agent/page-controller": "0.0.7", - "ai-motion": "^0.4.7", + "@page-agent/ui": "0.0.7", "chalk": "^5.6.2", "zod": "^4.1.12" } @@ -7013,6 +7018,14 @@ "version": "0.0.7", "license": "MIT" }, + "packages/ui": { + "name": "@page-agent/ui", + "version": "0.0.7", + "license": "MIT", + "dependencies": { + "ai-motion": "^0.4.7" + } + }, "packages/website": { "name": "@page-agent/website", "version": "0.0.7", diff --git a/package.json b/package.json index 1555fa7..a1bdc42 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "workspaces": [ "packages/page-controller", + "packages/ui", "packages/page-agent", "packages/website" ], diff --git a/packages/page-agent/package.json b/packages/page-agent/package.json index c5c99a8..96167bd 100644 --- a/packages/page-agent/package.json +++ b/packages/page-agent/package.json @@ -14,9 +14,7 @@ } }, "files": [ - "dist/", - "README.md", - "LICENSE" + "dist/" ], "description": "GUI agent for web applications - add intelligent automation to any webpage with a single script", "keywords": [ @@ -47,9 +45,9 @@ "postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\"" }, "dependencies": { - "ai-motion": "^0.4.7", "chalk": "^5.6.2", "zod": "^4.1.12", - "@page-agent/page-controller": "0.0.7" + "@page-agent/page-controller": "0.0.7", + "@page-agent/ui": "0.0.7" } } diff --git a/packages/page-agent/src/PageAgent.ts b/packages/page-agent/src/PageAgent.ts index 412d992..ce88ba9 100644 --- a/packages/page-agent/src/PageAgent.ts +++ b/packages/page-agent/src/PageAgent.ts @@ -3,6 +3,7 @@ * All rights reserved. */ import { PageController } from '@page-agent/page-controller' +import { Panel, SimulatorMask } from '@page-agent/ui' import chalk from 'chalk' import zod from 'zod' @@ -11,8 +12,6 @@ import { MAX_STEPS } from './config/constants' import { LLM, type Tool } from './llms' import SYSTEM_PROMPT from './prompts/system_prompt.md?raw' import { tools } from './tools' -import { Panel } from './ui/Panel' -import { SimulatorMask } from './ui/SimulatorMask' import { trimLines, uid, waitUntil } from './utils' import { assert } from './utils/assert' diff --git a/packages/page-agent/src/config/index.ts b/packages/page-agent/src/config/index.ts index e0a5497..6e67e84 100644 --- a/packages/page-agent/src/config/index.ts +++ b/packages/page-agent/src/config/index.ts @@ -1,7 +1,7 @@ import type { PageControllerConfig } from '@page-agent/page-controller' +import type { SupportedLanguage } from '@page-agent/ui' import type { AgentHistory, ExecutionResult, PageAgent } from '../PageAgent' -import type { SupportedLanguage } from '../i18n' import type { PageAgentTool } from '../tools' import { DEFAULT_API_KEY, diff --git a/packages/page-agent/vite.config.js b/packages/page-agent/vite.config.js index 549e73e..24298e0 100644 --- a/packages/page-agent/vite.config.js +++ b/packages/page-agent/vite.config.js @@ -61,6 +61,7 @@ const umdConfig = { resolve: { alias: { '@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'), + '@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'), }, }, build: { diff --git a/packages/page-controller/package.json b/packages/page-controller/package.json index d5136b8..4f8fb63 100644 --- a/packages/page-controller/package.json +++ b/packages/page-controller/package.json @@ -13,9 +13,7 @@ } }, "files": [ - "dist/", - "README.md", - "LICENSE" + "dist/" ], "description": "Page controller for page-agent - DOM operations and element interactions", "keywords": [ diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..0e05876 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,43 @@ +{ + "name": "@page-agent/ui", + "version": "0.0.7", + "type": "module", + "main": "./dist/lib/page-agent-ui.js", + "module": "./dist/lib/page-agent-ui.js", + "types": "./dist/lib/index.d.ts", + "exports": { + ".": { + "types": "./dist/lib/index.d.ts", + "import": "./dist/lib/page-agent-ui.js", + "default": "./dist/lib/page-agent-ui.js" + } + }, + "files": [ + "dist/" + ], + "description": "UI components for page-agent - Panel, SimulatorMask, and i18n", + "keywords": [ + "page-agent", + "ui", + "panel", + "i18n" + ], + "author": "Simon", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/alibaba/page-agent.git", + "directory": "packages/ui" + }, + "homepage": "https://alibaba.github.io/page-agent/", + "scripts": { + "build": "vite build", + "build:watch": "vite build --watch", + "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{}})\"" + }, + "dependencies": { + "ai-motion": "^0.4.7" + } +} + diff --git a/packages/ui/src/Panel.ts b/packages/ui/src/Panel.ts index ab2a748..8c4c110 100644 --- a/packages/ui/src/Panel.ts +++ b/packages/ui/src/Panel.ts @@ -1,6 +1,6 @@ -import { I18n, type SupportedLanguage } from '../i18n' -import { truncate } from '../utils' import { type Step, UIState } from './UIState' +import { I18n, type SupportedLanguage } from './i18n' +import { truncate } from './utils' import styles from './Panel.module.css' diff --git a/packages/ui/src/SimulatorMask.ts b/packages/ui/src/SimulatorMask.ts index c6a126a..26e0a4d 100644 --- a/packages/ui/src/SimulatorMask.ts +++ b/packages/ui/src/SimulatorMask.ts @@ -1,6 +1,6 @@ import { Motion } from 'ai-motion' -import { isPageDark } from '../utils/checkDarkMode' +import { isPageDark } from './checkDarkMode' import styles from './SimulatorMask.module.css' import cursorStyles from './cursor.module.css' diff --git a/packages/ui/src/env.d.ts b/packages/ui/src/env.d.ts new file mode 100644 index 0000000..10798a0 --- /dev/null +++ b/packages/ui/src/env.d.ts @@ -0,0 +1,6 @@ +/// + +declare module '*.module.css' { + const classes: Record + export default classes +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts new file mode 100644 index 0000000..5a1608f --- /dev/null +++ b/packages/ui/src/index.ts @@ -0,0 +1,4 @@ +export { Panel, type PanelConfig, type PanelUpdate } from './Panel' +export { SimulatorMask } from './SimulatorMask' +export { UIState, type Step, type AgentStatus } from './UIState' +export { I18n, type SupportedLanguage, type TranslationKey } from './i18n' diff --git a/packages/ui/src/utils.ts b/packages/ui/src/utils.ts new file mode 100644 index 0000000..fe81d23 --- /dev/null +++ b/packages/ui/src/utils.ts @@ -0,0 +1,6 @@ +export function truncate(text: string, maxLength: number): string { + if (text.length > maxLength) { + return text.substring(0, maxLength) + '...' + } + return text +} diff --git a/packages/ui/tsconfig.dts.json b/packages/ui/tsconfig.dts.json new file mode 100644 index 0000000..9f9b906 --- /dev/null +++ b/packages/ui/tsconfig.dts.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + // @workaround DTS bug + // dts do not work with monorepo path mapping + // disable path mapping for it + "paths": {} + } +} + diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 0000000..7c597c4 --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", + "noEmit": false, + "allowImportingTsExtensions": false, + "baseUrl": ".", + "outDir": "dist" + }, + "include": ["**/*.ts", "**/*.js"], + "exclude": ["dist", "node_modules"] +} + diff --git a/packages/ui/vite.config.js b/packages/ui/vite.config.js new file mode 100644 index 0000000..7828dae --- /dev/null +++ b/packages/ui/vite.config.js @@ -0,0 +1,41 @@ +// @ts-check +import chalk from 'chalk' +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)) + +console.log(chalk.cyan(`📦 Building @page-agent/ui`)) + +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/index.ts'), + name: 'PageAgentUI', + fileName: 'page-agent-ui', + formats: ['es'], + }, + outDir: resolve(__dirname, 'dist', 'lib'), + rollupOptions: { + external: ['ai-motion'], + }, + minify: false, + sourcemap: true, + cssCodeSplit: true, + }, + define: { + 'process.env.NODE_ENV': '"production"', + }, +}) diff --git a/packages/website/vite.config.js b/packages/website/vite.config.js index 79a7ddc..a925e30 100644 --- a/packages/website/vite.config.js +++ b/packages/website/vite.config.js @@ -20,6 +20,7 @@ export default defineConfig({ // Monorepo packages (always bundle local code instead of npm versions) '@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'), + '@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'), 'page-agent': resolve(__dirname, '../page-agent/src/PageAgent.ts'), }, }, diff --git a/tsconfig.json b/tsconfig.json index ff8b649..96843c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.base.json", "references": [ { "path": "./packages/page-controller" }, + { "path": "./packages/ui" }, { "path": "./packages/page-agent" }, { "path": "./packages/website" } ], From b33e361cee49752f36cde0e5d41b3816bfee985c Mon Sep 17 00:00:00 2001 From: Simon <10131203+gaomeng1900@users.noreply.github.com> Date: Mon, 15 Dec 2025 17:42:22 +0800 Subject: [PATCH 4/7] chore: clean up EventBus --- packages/page-agent/src/utils/bus.ts | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/packages/page-agent/src/utils/bus.ts b/packages/page-agent/src/utils/bus.ts index 554e420..d7e89e4 100644 --- a/packages/page-agent/src/utils/bus.ts +++ b/packages/page-agent/src/utils/bus.ts @@ -1,26 +1,8 @@ -/** - * Type-safe event bus for decoupling PageAgent and Panel - */ - /** * Event mapping definitions * @note Event bus callbacks must be repeatable without errors */ export interface PageAgentEventMap { - // Panel control events - // call panel.show() - 'panel:show': { params: undefined } - // call panel.hide() - // 'panel:hide': { params: undefined } - // call panel.reset() - // 'panel:reset': { params: undefined } - // call panel.update() - // 'panel:update': { params: Omit } - // call panel.expand() - // 'panel:expand': { params: undefined } - // call panel.collapse() - // 'panel:collapse': { params: undefined } - // PageAgent status events // 'agent:execute': { params: { task: string } } // 'agent:done': { params: { text: string; success: boolean } } @@ -30,8 +12,7 @@ export interface PageAgentEventMap { // 'agent:error': { params: { error: string | Error } } // Task status change events - // 'task:start': { params: { task: string } } - // 'task:step': { params: Omit } + 'task:start': { params: { task: string } } // 'task:complete': { params: { text: string; success: boolean } } // 'task:error': { params: { error: string | Error } } From 8885bde0ec856f18f56e617cbe1a9a5c489e3103 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:45:34 +0000 Subject: [PATCH 5/7] Initial plan From 67bf38c8d5fccd11070903bff7a116b79bb69454 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:50:28 +0000 Subject: [PATCH 6/7] fix: clean up event listeners in PageAgent dispose method Co-authored-by: gaomeng1900 <10131203+gaomeng1900@users.noreply.github.com> --- package-lock.json | 29 +++++++++++----------------- packages/page-agent/src/PageAgent.ts | 22 +++++++++++++++++---- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5d8510c..1900585 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2685,7 +2684,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2696,7 +2694,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2757,7 +2754,6 @@ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -3021,7 +3017,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3219,7 +3214,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -3534,7 +3528,6 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -3748,7 +3741,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -3813,7 +3805,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4567,7 +4558,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -4998,6 +4988,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5019,6 +5010,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5040,6 +5032,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5061,6 +5054,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5082,6 +5076,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5103,6 +5098,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5124,6 +5120,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5145,6 +5142,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5166,6 +5164,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5187,6 +5186,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5208,6 +5208,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5822,7 +5823,6 @@ "integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5866,7 +5866,6 @@ "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6003,7 +6002,6 @@ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -6331,7 +6329,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6427,7 +6424,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6637,7 +6633,6 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -6741,7 +6736,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6985,7 +6979,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/packages/page-agent/src/PageAgent.ts b/packages/page-agent/src/PageAgent.ts index ce88ba9..4754c39 100644 --- a/packages/page-agent/src/PageAgent.ts +++ b/packages/page-agent/src/PageAgent.ts @@ -78,6 +78,8 @@ export class PageAgent extends EventTarget { #llm: LLM #totalWaitTime = 0 #abortController = new AbortController() + #llmRetryListener: ((e: Event) => void) | null = null + #llmErrorListener: ((e: Event) => void) | null = null /** PageController for DOM operations */ pageController: PageController @@ -108,14 +110,16 @@ export class PageAgent extends EventTarget { this.pageController = new PageController(this.config) // Listen to LLM events - this.#llm.addEventListener('retry', (e) => { + this.#llmRetryListener = (e) => { const { current, max } = (e as CustomEvent).detail this.panel.update({ type: 'retry', current, max }) - }) - this.#llm.addEventListener('error', (e) => { + } + this.#llmErrorListener = (e) => { const { error } = (e as CustomEvent).detail this.panel.update({ type: 'error', message: `step failed: ${error.message}` }) - }) + } + this.#llm.addEventListener('retry', this.#llmRetryListener) + this.#llm.addEventListener('error', this.#llmErrorListener) if (this.config.customTools) { for (const [name, tool] of Object.entries(this.config.customTools)) { @@ -491,6 +495,16 @@ export class PageAgent extends EventTarget { this.history = [] this.#abortController.abort(reason ?? 'PageAgent disposed') + // Clean up LLM event listeners + if (this.#llmRetryListener) { + this.#llm.removeEventListener('retry', this.#llmRetryListener) + this.#llmRetryListener = null + } + if (this.#llmErrorListener) { + this.#llm.removeEventListener('error', this.#llmErrorListener) + this.#llmErrorListener = null + } + this.config.onDispose?.call(this, reason) } } From 8ba04bf243a5dca8f735d1fbe1540dec5b87b63d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:52:23 +0000 Subject: [PATCH 7/7] fix: also clean up beforeunload event listener Co-authored-by: gaomeng1900 <10131203+gaomeng1900@users.noreply.github.com> --- packages/page-agent/src/PageAgent.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/page-agent/src/PageAgent.ts b/packages/page-agent/src/PageAgent.ts index 4754c39..acdfcd6 100644 --- a/packages/page-agent/src/PageAgent.ts +++ b/packages/page-agent/src/PageAgent.ts @@ -80,6 +80,7 @@ export class PageAgent extends EventTarget { #abortController = new AbortController() #llmRetryListener: ((e: Event) => void) | null = null #llmErrorListener: ((e: Event) => void) | null = null + #beforeUnloadListener: ((e: Event) => void) | null = null /** PageController for DOM operations */ pageController: PageController @@ -135,9 +136,10 @@ export class PageAgent extends EventTarget { this.tools.delete('execute_javascript') } - window.addEventListener('beforeunload', (e) => { + this.#beforeUnloadListener = (e) => { if (!this.disposed) this.dispose('PAGE_UNLOADING') - }) + } + window.addEventListener('beforeunload', this.#beforeUnloadListener) } /** @@ -505,6 +507,12 @@ export class PageAgent extends EventTarget { this.#llmErrorListener = null } + // Clean up window event listeners + if (this.#beforeUnloadListener) { + window.removeEventListener('beforeunload', this.#beforeUnloadListener) + this.#beforeUnloadListener = null + } + this.config.onDispose?.call(this, reason) } }