diff --git a/packages/core/src/PageAgentCore.ts b/packages/core/src/PageAgentCore.ts index bf2a242..69d66eb 100644 --- a/packages/core/src/PageAgentCore.ts +++ b/packages/core/src/PageAgentCore.ts @@ -68,6 +68,8 @@ export class PageAgentCore extends EventTarget { taskId = '' /** History events */ history: HistoricalEvent[] = [] + /** Whether this agent has been disposed */ + disposed = false /** * Callback for when agent needs user input (ask_user tool) @@ -183,7 +185,15 @@ export class PageAgentCore extends EventTarget { 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 { + 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() @@ -300,9 +310,13 @@ export class PageAgentCore extends EventTarget { } } catch (error: unknown) { console.groupEnd() // to prevent nested groups + const isAbortError = (error as any)?.rawError?.name === 'AbortError' + console.error('Task failed', error) - const errorMessage = String(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, @@ -315,10 +329,13 @@ export class PageAgentCore extends EventTarget { 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: 'Step count exceeded maximum limit', + data: errorMessage, history: this.history, } await onAfterTask?.(this, result) @@ -601,6 +618,7 @@ export class PageAgentCore extends EventTarget { dispose() { console.log('Disposing PageAgent...') + this.disposed = true this.pageController.dispose() // this.history = [] this.#abortController.abort() diff --git a/packages/extension/docs/extension_api.md b/packages/extension/docs/extension_api.md index 32a52df..c4671f3 100644 --- a/packages/extension/docs/extension_api.md +++ b/packages/extension/docs/extension_api.md @@ -97,7 +97,7 @@ Parameters: Returns: `Promise` -#### `PAGE_AGENT_EXT.dispose()` +#### `PAGE_AGENT_EXT.stop()` Stop the current task. @@ -124,7 +124,6 @@ export interface ExecuteConfig { onStatusChange?: (status: AgentStatus) => void onActivity?: (activity: AgentActivity) => void onHistoryUpdate?: (history: HistoricalEvent[]) => void - onDispose?: () => void } export type Execute = (task: string, config: ExecuteConfig) => Promise @@ -189,7 +188,7 @@ const result = await window.PAGE_AGENT_EXT!.execute( ### Stop the Current Task ```typescript -window.PAGE_AGENT_EXT!.dispose() +window.PAGE_AGENT_EXT!.stop() ``` ## Window Type Declaration @@ -212,7 +211,6 @@ interface ExecuteConfig { onStatusChange?: (status: AgentStatus) => void onActivity?: (activity: AgentActivity) => void onHistoryUpdate?: (history: HistoricalEvent[]) => void - onDispose?: () => void } declare global { @@ -221,7 +219,7 @@ declare global { PAGE_AGENT_EXT?: { version: string execute: Execute - dispose: () => void + stop: () => void } } } diff --git a/packages/extension/docs/extension_api_zh.md b/packages/extension/docs/extension_api_zh.md index 6814aa2..8ad4411 100644 --- a/packages/extension/docs/extension_api_zh.md +++ b/packages/extension/docs/extension_api_zh.md @@ -97,7 +97,7 @@ token 匹配后,插件会在 `window` 上注入 API。 返回:`Promise` -#### `PAGE_AGENT_EXT.dispose()` +#### `PAGE_AGENT_EXT.stop()` 停止当前任务。 @@ -124,7 +124,6 @@ export interface ExecuteConfig { onStatusChange?: (status: AgentStatus) => void onActivity?: (activity: AgentActivity) => void onHistoryUpdate?: (history: HistoricalEvent[]) => void - onDispose?: () => void } export type Execute = (task: string, config: ExecuteConfig) => Promise @@ -189,7 +188,7 @@ const result = await window.PAGE_AGENT_EXT!.execute( ### 停止当前任务 ```typescript -window.PAGE_AGENT_EXT!.dispose() +window.PAGE_AGENT_EXT!.stop() ``` ## Window 类型声明 @@ -212,7 +211,6 @@ interface ExecuteConfig { onStatusChange?: (status: AgentStatus) => void onActivity?: (activity: AgentActivity) => void onHistoryUpdate?: (history: HistoricalEvent[]) => void - onDispose?: () => void } declare global { @@ -221,7 +219,7 @@ declare global { PAGE_AGENT_EXT?: { version: string execute: Execute - dispose: () => void + stop: () => void } } } diff --git a/packages/extension/src/agent/MultiPageAgent.ts b/packages/extension/src/agent/MultiPageAgent.ts index 590475e..84b2592 100644 --- a/packages/extension/src/agent/MultiPageAgent.ts +++ b/packages/extension/src/agent/MultiPageAgent.ts @@ -89,8 +89,7 @@ export class MultiPageAgent extends PageAgentCore { isAgentRunning: false, }) - // no need to dispose tabsController and pageController - // as they do not keep references + tabsController.dispose() }, }) } diff --git a/packages/extension/src/agent/useAgent.ts b/packages/extension/src/agent/useAgent.ts index 15f091c..c734110 100644 --- a/packages/extension/src/agent/useAgent.ts +++ b/packages/extension/src/agent/useAgent.ts @@ -84,7 +84,7 @@ export function useAgent(): UseAgentResult { }, []) const stop = useCallback(() => { - agentRef.current?.dispose() + agentRef.current?.stop() }, []) const configure = useCallback(async (newConfig: LLMConfig) => { diff --git a/packages/extension/src/entrypoints/content.ts b/packages/extension/src/entrypoints/content.ts index f984735..e06dd1f 100644 --- a/packages/extension/src/entrypoints/content.ts +++ b/packages/extension/src/entrypoints/content.ts @@ -71,7 +71,8 @@ async function exposeAgentToPage() { try { const { task, config } = payload - // create when used + // Dispose old instance before creating new one + multiPageAgent?.dispose() multiPageAgent = new MultiPageAgent(config) @@ -116,17 +117,6 @@ async function exposeAgentToPage() { ) }) - multiPageAgent.addEventListener('dispose', () => { - window.postMessage( - { - channel: 'PAGE_AGENT_EXT_RESPONSE', - id, - action: 'dispose_event', - }, - '*' - ) - }) - // result const result = await multiPageAgent.execute(task) @@ -155,9 +145,8 @@ async function exposeAgentToPage() { break } - case 'dispose': { - // @note stop ongoing processes but can still be re-used later - multiPageAgent?.dispose() + case 'stop': { + multiPageAgent?.stop() break } diff --git a/packages/extension/src/entrypoints/main-world.ts b/packages/extension/src/entrypoints/main-world.ts index 606a756..c17f035 100644 --- a/packages/extension/src/entrypoints/main-world.ts +++ b/packages/extension/src/entrypoints/main-world.ts @@ -16,7 +16,6 @@ export interface ExecuteConfig { onStatusChange?: (status: AgentStatus) => void onActivity?: (activity: AgentActivity) => void onHistoryUpdate?: (history: HistoricalEvent[]) => void - onDispose?: () => void } export default defineUnlistedScript(() => { @@ -60,12 +59,6 @@ export default defineUnlistedScript(() => { return } - if (data.action === 'dispose_event' && config.onDispose) { - config.onDispose() - window.removeEventListener('message', handleMessage) - return - } - if (data.action !== 'execute_result') return // execute_result @@ -104,14 +97,14 @@ export default defineUnlistedScript(() => { return promise } - const dispose = () => { + const stop = () => { const id = getId() window.postMessage( { channel: 'PAGE_AGENT_EXT_REQUEST', id, - action: 'dispose', + action: 'stop', }, '*' ) @@ -121,6 +114,6 @@ export default defineUnlistedScript(() => { ;(window as any).PAGE_AGENT_EXT = { version: __EXT_VERSION__, execute, - dispose, + stop, } }) diff --git a/packages/llms/src/OpenAIClient.ts b/packages/llms/src/OpenAIClient.ts index 2f97759..ca2a317 100644 --- a/packages/llms/src/OpenAIClient.ts +++ b/packages/llms/src/OpenAIClient.ts @@ -54,8 +54,10 @@ export class OpenAIClient implements LLMClient { signal: abortSignal, }) } catch (error: unknown) { - console.error(error) - throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error) + const isAbortError = (error as any)?.name === 'AbortError' + 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 diff --git a/packages/llms/src/errors.ts b/packages/llms/src/errors.ts index 4970d4c..378af75 100644 --- a/packages/llms/src/errors.ts +++ b/packages/llms/src/errors.ts @@ -34,12 +34,15 @@ export class InvokeError extends Error { super(message) this.name = 'InvokeError' this.type = type - this.retryable = this.isRetryable(type) + this.retryable = this.isRetryable(type, rawError) this.rawError = rawError this.rawResponse = rawResponse } - private isRetryable(type: InvokeErrorType): boolean { + private isRetryable(type: InvokeErrorType, rawError?: unknown): boolean { + const isAbortError = (rawError as any)?.name === 'AbortError' + if (isAbortError) return false + const retryableTypes: InvokeErrorType[] = [ InvokeErrorType.NETWORK_ERROR, InvokeErrorType.RATE_LIMIT, diff --git a/packages/ui/src/i18n/locales.ts b/packages/ui/src/i18n/locales.ts index 70a02e8..eacf31e 100644 --- a/packages/ui/src/i18n/locales.ts +++ b/packages/ui/src/i18n/locales.ts @@ -12,6 +12,7 @@ const enUS = { question: 'Question: {{question}}', waitingPlaceholder: 'Waiting for task to start...', stop: 'Stop', + close: 'Close', expand: 'Expand history', collapse: 'Collapse history', step: 'Step {{number}} · {{time}}{{duration}}', @@ -59,6 +60,7 @@ const zhCN = { question: '询问: {{question}}', waitingPlaceholder: '等待任务开始...', stop: '终止', + close: '关闭', expand: '展开历史', collapse: '收起历史', step: '步骤 {{number}} · {{time}}{{duration}}', diff --git a/packages/ui/src/panel/Panel.ts b/packages/ui/src/panel/Panel.ts index 8a37f9a..23e83b6 100644 --- a/packages/ui/src/panel/Panel.ts +++ b/packages/ui/src/panel/Panel.ts @@ -33,7 +33,7 @@ export class Panel { #statusText: HTMLElement #historySection: HTMLElement #expandButton: HTMLElement - #stopButton: HTMLElement + #actionButton: HTMLElement #inputSection: HTMLElement #taskInput: HTMLInputElement @@ -76,7 +76,7 @@ export class Panel { this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)! this.#historySection = this.#wrapper.querySelector(`.${styles.historySection}`)! this.#expandButton = this.#wrapper.querySelector(`.${styles.expandButton}`)! - this.#stopButton = this.#wrapper.querySelector(`.${styles.stopButton}`)! + this.#actionButton = this.#wrapper.querySelector(`.${styles.stopButton}`)! this.#inputSection = this.#wrapper.querySelector(`.${styles.inputSectionWrapper}`)! this.#taskInput = this.#wrapper.querySelector(`.${styles.taskInput}`)! @@ -105,6 +105,15 @@ export class Panel { status === 'running' ? 'thinking' : status === 'idle' ? 'thinking' : status this.#updateStatusIndicator(indicatorType) + // Morph action button: running = stop (■), not running = close (X) + if (status === 'running') { + this.#actionButton.textContent = '■' + this.#actionButton.title = this.#i18n.t('ui.panel.stop') + } else { + this.#actionButton.textContent = 'X' + this.#actionButton.title = this.#i18n.t('ui.panel.close') + } + // Show/hide based on status if (status === 'running') { this.show() @@ -266,10 +275,14 @@ export class Panel { } /** - * Stop Agent + * Action button handler: stop when running, close (dispose) when idle */ - #stopAgent(): void { - this.#agent.dispose() + #handleActionButton(): void { + if (this.#agent.status === 'running') { + this.#agent.stop() + } else { + this.#agent.dispose() + } } /** @@ -383,7 +396,7 @@ export class Panel { - @@ -420,10 +433,10 @@ export class Panel { this.#toggle() }) - // Stop button - this.#stopButton.addEventListener('click', (e) => { + // Action button (stop / close) + this.#actionButton.addEventListener('click', (e) => { e.stopPropagation() - this.#stopAgent() + this.#handleActionButton() }) // Submit on Enter key in input field diff --git a/packages/ui/src/panel/types.ts b/packages/ui/src/panel/types.ts index a8dc14b..66c7a9e 100644 --- a/packages/ui/src/panel/types.ts +++ b/packages/ui/src/panel/types.ts @@ -68,6 +68,9 @@ export interface PanelAgentAdapter extends EventTarget { /** Execute a task */ execute(task: string): Promise - /** Dispose the agent */ + /** Stop the current task (agent remains reusable) */ + stop(): void + + /** Dispose the agent (terminal, cannot be reused) */ dispose(): void } diff --git a/packages/website/src/pages/docs/features/chrome-extension/page.tsx b/packages/website/src/pages/docs/features/chrome-extension/page.tsx index f4db211..0bf8fe4 100644 --- a/packages/website/src/pages/docs/features/chrome-extension/page.tsx +++ b/packages/website/src/pages/docs/features/chrome-extension/page.tsx @@ -206,7 +206,6 @@ interface ExecuteConfig { onStatusChange?: (status: AgentStatus) => void onActivity?: (activity: AgentActivity) => void onHistoryUpdate?: (history: HistoricalEvent[]) => void - onDispose?: () => void } type Execute = (task: string, config: ExecuteConfig) => Promise @@ -217,7 +216,7 @@ declare global { PAGE_AGENT_EXT?: { version: string execute: Execute - dispose: () => void + stop: () => void } } }` @@ -237,7 +236,6 @@ interface ExecuteConfig { onStatusChange?: (status: AgentStatus) => void onActivity?: (activity: AgentActivity) => void onHistoryUpdate?: (history: HistoricalEvent[]) => void - onDispose?: () => void } type Execute = (task: string, config: ExecuteConfig) => Promise @@ -248,7 +246,7 @@ declare global { PAGE_AGENT_EXT?: { version: string execute: Execute - dispose: () => void + stop: () => void } } }` @@ -271,8 +269,7 @@ const result = await window.PAGE_AGENT_EXT.execute( // includeInitialTab: false, // 设为 false 排除初始标签页 onStatusChange: status => console.log('状态变化:', status), onActivity: activity => console.log('活动:', activity), - onHistoryUpdate: history => console.log('历史更新:', history), - onDispose: () => console.log('已停止') + onHistoryUpdate: history => console.log('历史更新:', history) } ) @@ -287,8 +284,7 @@ const result = await window.PAGE_AGENT_EXT.execute( // includeInitialTab: false, // Set to false to exclude initial tab onStatusChange: status => console.log('Status change:', status), onActivity: activity => console.log('Activity:', activity), - onHistoryUpdate: history => console.log('History update:', history), - onDispose: () => console.log('Disposed') + onHistoryUpdate: history => console.log('History update:', history) } ) @@ -297,20 +293,18 @@ console.log(result) // Task execution result` language="javascript" /> -

PAGE_AGENT_EXT.dispose()

+

PAGE_AGENT_EXT.stop()

- {isZh - ? '停止当前正在运行的任务。停止后 Agent 可以重新使用。' - : 'Stop the current running task. The agent can be reused after disposal.'} + {isZh ? '停止当前正在运行的任务。' : 'Stop the current running task.'}