Merge pull request #118 from alibaba/feat/make-panel-optional

feat: make all ui optional
This commit is contained in:
Simon
2026-01-17 17:43:40 +08:00
committed by GitHub
7 changed files with 127 additions and 22 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,
})
}

View File

@@ -61,6 +61,27 @@ export default function Configuration() {
code={`interface AgentConfig {
language?: 'en-US' | 'zh-CN'
/**
* 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 or override built-in tools */
customTools?: Record<string, PageAgentTool | null>
@@ -140,6 +161,39 @@ interface PageControllerConfig extends DomConfig {
code={`type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig`}
/>
</section>
{/* Programmatic Usage Example */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">
{isZh ? '程序化使用配置' : 'Programmatic Usage'}
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '对于程序化集成场景,可以禁用 UI。'
: 'For programmatic integration, you can disable UI.'}
</p>
<CodeEditor
language="typescript"
code={`const agent = new PageAgent({
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'your-model-name',
// Disable all UI features for pure programmatic usage
enablePanel: false, // Don't create Panel UI
enableMask: false, // Don't show visual overlay (mask and pointer)
// enableAskUser is automatically disabled when enablePanel is false
// Or keep Panel but disable post-task prompts
// enablePanel: true,
// promptForNextTask: false,
})
// Pure programmatic execution
const result = await agent.execute('search for TypeScript documentation')
console.log(result.success, result.data, result.history)`}
/>
</section>
</div>
)
}

View File

@@ -36,7 +36,7 @@ const pageAgentTool = {
},
execute: async (params) => {
const result = await pageAgent.execute(params.instruction)
return { success: result.success, message: result.message }
return { success: result.success, message: result.data }
}
}