refactor(core)!: rework agent run lifecycle and status semantics

BREAKING CHANGE: stop() is now async and resolves after the run fully
settles; status decouples from task outcome (new 'stopped' state, LLM
self-reported failure now ends as 'completed'). Lifecycle hooks re-throw
instead of being folded into the result; agent errors go to history.
Adds agent.lastResult.
This commit is contained in:
Simon
2026-06-11 14:33:12 +08:00
parent 73810b3ed8
commit 052a302a08
8 changed files with 268 additions and 135 deletions

View File

@@ -100,10 +100,10 @@ export class Panel {
#handleStatusChange(): void {
const status = this.#agent.status
// Map agent status to UI indicator type
const indicatorType =
status === 'running' ? 'thinking' : status === 'idle' ? 'thinking' : status
this.#updateStatusIndicator(indicatorType)
// Map agent status to UI indicator. A `completed` run whose result reports
// failure shows as error; other statuses map to their own indicator.
const failed = status === 'completed' && this.#agent.lastResult?.success === false
this.#updateStatusIndicator(failed ? 'error' : status)
// Morph action button: running = stop (■), not running = close (X)
if (status === 'running') {
@@ -121,7 +121,7 @@ export class Panel {
}
// Handle completion
if (status === 'completed' || status === 'error') {
if (status === 'completed' || status === 'error' || status === 'stopped') {
if (!this.#isExpanded) {
this.#expand()
}
@@ -376,7 +376,7 @@ export class Panel {
}
const status = this.#agent.status
const isTaskEnded = status === 'completed' || status === 'error'
const isTaskEnded = status === 'completed' || status === 'error' || status === 'stopped'
// Only show input area after task completion if configured to do so
if (isTaskEnded) {
@@ -559,13 +559,23 @@ export class Panel {
}
#updateStatusIndicator(
type: 'thinking' | 'executing' | 'executed' | 'retrying' | 'completed' | 'error'
type:
| 'idle'
| 'running'
| 'thinking'
| 'executing'
| 'executed'
| 'retrying'
| 'completed'
| 'error'
| 'stopped'
): void {
// Clear all status classes
// `running` animates like thinking; `idle`/`stopped` use the neutral base.
const variant = type === 'running' ? 'thinking' : type
this.#indicator.className = styles.indicator
// Add corresponding status class
this.#indicator.classList.add(styles[type])
if (variant !== 'idle' && variant !== 'stopped') {
this.#indicator.classList.add(styles[variant])
}
}
#scrollToBottom(): void {

View File

@@ -22,14 +22,17 @@ export type AgentActivity =
* This enables decoupling and allows any agent implementation to work with Panel.
*
* Events:
* - 'statuschange': Agent status changed (idle/running/completed/error)
* - 'statuschange': Agent status changed
* - 'historychange': Historical events updated (persisted)
* - 'activity': Transient activity for immediate UI feedback (thinking/executing/etc)
* - 'dispose': Agent is being disposed
*/
export interface PanelAgentAdapter extends EventTarget {
/** Current agent status */
readonly status: 'idle' | 'running' | 'completed' | 'error'
readonly status: 'idle' | 'running' | 'completed' | 'error' | 'stopped'
/** Result of the most recent run, or `null` before the first run completes */
readonly lastResult: { success: boolean } | null
/** History of agent events */
readonly history: readonly {
@@ -71,7 +74,7 @@ export interface PanelAgentAdapter extends EventTarget {
execute(task: string): Promise<unknown>
/** Stop the current task (agent remains reusable) */
stop(): void
stop(): Promise<void>
/** Dispose the agent (terminal, cannot be reused) */
dispose(): void