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] 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 } }