feat: make panel optional

This commit is contained in:
Simon
2026-01-17 17:24:17 +08:00
parent 1af2607cca
commit 78d4919289
5 changed files with 72 additions and 21 deletions

View File

@@ -99,7 +99,7 @@ export interface ExecutionResult {
export class PageAgent extends EventTarget {
config: PageAgentConfig
id = uid()
panel: Panel
panel: Panel | null = null
tools: typeof tools
disposed = false
task = ''
@@ -130,11 +130,17 @@ export class PageAgent extends EventTarget {
this.config = config
this.#llm = new LLM(this.config)
this.panel = new Panel({
language: this.config.language,
onExecuteTask: (task) => this.execute(task),
onStop: () => this.dispose(),
})
// Conditionally initialize Panel
if (this.config.enablePanel !== false) {
this.panel = new Panel({
language: this.config.language,
onExecuteTask: (task) => this.execute(task),
onStop: () => this.dispose(),
promptForNextTask: this.config.promptForNextTask,
})
}
this.tools = new Map(tools)
// Initialize PageController with config (mask enabled by default)
@@ -146,11 +152,11 @@ export class PageAgent extends EventTarget {
// Listen to LLM events
this.#llmRetryListener = (e) => {
const { current, max } = (e as CustomEvent).detail
this.panel.update({ type: 'retry', current, max })
this.panel?.update({ type: 'retry', current, max })
}
this.#llmErrorListener = (e) => {
const { error } = (e as CustomEvent).detail
this.panel.update({ type: 'error', message: `step failed: ${error.message}` })
this.panel?.update({ type: 'error', message: `step failed: ${error.message}` })
}
this.#llm.addEventListener('retry', this.#llmRetryListener)
this.#llm.addEventListener('error', this.#llmErrorListener)
@@ -169,6 +175,11 @@ export class PageAgent extends EventTarget {
this.tools.delete('execute_javascript')
}
// Disable ask_user tool if enableAskUser is false or if panel is disabled
if (this.config.enableAskUser === false || this.config.enablePanel === false) {
this.tools.delete('ask_user')
}
this.#beforeUnloadListener = (e) => {
if (!this.disposed) this.dispose('PAGE_UNLOADING')
}
@@ -181,7 +192,7 @@ export class PageAgent extends EventTarget {
*/
pushObservation(content: string): void {
this.history.push({ type: 'observation', content })
this.panel.update({ type: 'observation', content })
this.panel?.update({ type: 'observation', content })
}
async execute(task: string): Promise<ExecutionResult> {
@@ -199,10 +210,10 @@ export class PageAgent extends EventTarget {
// Show mask and panel
this.pageController.showMask()
this.panel.show()
this.panel.reset()
this.panel?.show()
this.panel?.reset()
this.panel.update({ type: 'input', task: this.task })
this.panel?.update({ type: 'input', task: this.task })
if (this.#abortController) {
this.#abortController.abort()
@@ -232,7 +243,7 @@ export class PageAgent extends EventTarget {
// Update status to thinking
console.log(chalk.blue('Thinking...'))
this.panel.update({ type: 'thinking' })
this.panel?.update({ type: 'thinking' })
const result = await this.#llm.invoke(
[
@@ -370,7 +381,7 @@ export class PageAgent extends EventTarget {
if (reflectionText) {
console.log(reflectionText)
this.panel.update({ type: 'thinking', text: reflectionText })
this.panel?.update({ type: 'thinking', text: reflectionText })
}
// Find the corresponding tool
@@ -378,7 +389,7 @@ export class PageAgent extends EventTarget {
assert(tool, `Tool ${toolName} not found. (@note should have been caught before this!!!)`)
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
this.panel.update({ type: 'toolExecuting', toolName, args: toolInput })
this.panel?.update({ type: 'toolExecuting', toolName, args: toolInput })
const startTime = Date.now()
@@ -394,7 +405,7 @@ export class PageAgent extends EventTarget {
}
// Briefly display execution result
this.panel.update({
this.panel?.update({
type: 'toolCompleted',
toolName,
args: toolInput,
@@ -557,13 +568,13 @@ export class PageAgent extends EventTarget {
// Update panel status
if (success) {
this.panel.update({ type: 'output', text })
this.panel?.update({ type: 'output', text })
} else {
this.panel.update({ type: 'error', message: text })
this.panel?.update({ type: 'error', message: text })
}
// Task completed
this.panel.update({ type: 'completed' })
this.panel?.update({ type: 'completed' })
this.pageController.hideMask()
@@ -593,7 +604,7 @@ export class PageAgent extends EventTarget {
console.log('Disposing PageAgent...')
this.disposed = true
this.pageController.dispose()
this.panel.dispose()
this.panel?.dispose()
this.history = []
this.#abortController.abort(reason ?? 'PageAgent disposed')

View File

@@ -11,6 +11,27 @@ export interface AgentConfig {
// theme?: 'light' | 'dark'
language?: SupportedLanguage
/**
* Whether to prompt for next task after task completion
* @default true
*/
promptForNextTask?: boolean
/**
* Enable the UI panel for visual feedback and user interaction
* When disabled, the panel will not be created and all UI operations will be skipped.
* Useful for automated testing or when integrating PageAgent as a library.
* @default true
*/
enablePanel?: boolean
/**
* Enable the ask_user tool for agent to ask questions
* When disabled, the agent cannot ask user questions during execution.
* @default true
*/
enableAskUser?: boolean
/**
* Custom tools to extend PageAgent capabilities
* @experimental

View File

@@ -80,6 +80,9 @@ tools.set(
question: zod.string(),
}),
execute: async function (this: PageAgent, input) {
if (!this.panel) {
throw new Error('ask_user tool requires panel to be enabled')
}
const answer = await this.panel.askUser(input.question)
return `✅ Received user answer: ${answer}`
},

View File

@@ -11,6 +11,11 @@ export interface PanelConfig {
language?: SupportedLanguage
onExecuteTask: (task: string) => void
onStop: () => void
/**
* Whether to prompt for next task after task completion
* @default true
*/
promptForNextTask?: boolean
}
/**
@@ -358,7 +363,14 @@ export class Panel {
}
const lastStep = steps[steps.length - 1]
return lastStep.type === 'completed' || lastStep.type === 'error'
const isTaskEnded = lastStep.type === 'completed' || lastStep.type === 'error'
// Only show input area after task completion if configured to do so
if (isTaskEnded) {
return this.#config.promptForNextTask ?? true
}
return false
}
#createWrapper(): HTMLElement {

View File

@@ -89,6 +89,10 @@ export default function HomePage() {
import.meta.env.DEV && import.meta.env.LLM_API_KEY
? import.meta.env.LLM_API_KEY
: DEMO_API_KEY,
// enableAskUser: false,
// promptForNextTask: false,
// enablePanel: false,
})
}