diff --git a/desktop/src/components/PipelinesPanel.tsx b/desktop/src/components/PipelinesPanel.tsx index 7c98bff..7419371 100644 --- a/desktop/src/components/PipelinesPanel.tsx +++ b/desktop/src/components/PipelinesPanel.tsx @@ -7,7 +7,7 @@ * Pipelines orchestrate Skills and Hands to accomplish complex tasks. */ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Play, RefreshCw, @@ -437,6 +437,22 @@ export function PipelinesPanel() { const [runResult, setRunResult] = useState<{ result: PipelineRunResponse; pipeline: PipelineInfo } | null>(null); const { toast } = useToast(); + // Subscribe to pipeline-complete push events (for background completion) + useEffect(() => { + let unlisten: (() => void) | undefined; + PipelineClient.onComplete((event) => { + // Only show notification if we're not already tracking this run + // (the polling path handles in-flight runs via handleRunComplete) + if (selectedPipeline?.id === event.pipelineId) return; + if (event.status === 'completed') { + toast(`Pipeline "${event.pipelineId}" 后台执行完成`, 'success'); + } else if (event.status === 'failed') { + toast(`Pipeline "${event.pipelineId}" 后台执行失败: ${event.error ?? ''}`, 'error'); + } + }).then((fn) => { unlisten = fn; }); + return () => { unlisten?.(); }; + }, [selectedPipeline, toast]); + // Fetch all pipelines without filtering const { pipelines, loading, error, refresh } = usePipelines({});