From 9ad3043fe92a7fd085aeeda11d1ef08f993f1f70 Mon Sep 17 00:00:00 2001 From: viettranx Date: Fri, 20 Mar 2026 22:26:22 +0700 Subject: [PATCH] feat(ui): team leader processing indicator and announce run streaming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add team.leader.processing backend event (emitted from onDrain before announce run starts) so all WS clients show processing status - Capture announce run.started events to stream leader's summarization in real-time (thinking, tool calls, text) - Add leader_processing phase to ActivityIndicator and ChatTopBar - isBusy stays true during leader processing → stop button visible --- cmd/gateway.go | 10 ++++++++++ pkg/protocol/events.go | 3 +++ ui/web/src/api/protocol.ts | 3 +++ ui/web/src/components/chat/active-run-zone.tsx | 2 +- .../src/components/chat/activity-indicator.tsx | 6 ++++-- ui/web/src/components/chat/chat-top-bar.tsx | 1 + ui/web/src/pages/chat/hooks/use-chat-messages.ts | 16 +++++++++++++--- ui/web/src/types/chat.ts | 2 +- 8 files changed, 36 insertions(+), 7 deletions(-) diff --git a/cmd/gateway.go b/cmd/gateway.go index 7ecd98c6..406094cc 100644 --- a/cmd/gateway.go +++ b/cmd/gateway.go @@ -143,6 +143,16 @@ func runGateway() { for _, item := range items { batchMedia = append(batchMedia, item.Media...) } + // Notify clients that leader is processing team results + // (bridges UI gap between last task.completed and announce run.started). + msgBus.Broadcast(bus.Event{ + Name: protocol.EventTeamLeaderProcessing, + Payload: map[string]any{ + "agentId": meta.ParentAgent, + "tasks": len(items), + }, + }) + msgBus.PublishInbound(bus.InboundMessage{ Channel: "system", SenderID: senderID, diff --git a/pkg/protocol/events.go b/pkg/protocol/events.go index 08a6fe1c..14ac156f 100644 --- a/pkg/protocol/events.go +++ b/pkg/protocol/events.go @@ -52,6 +52,9 @@ const ( EventTeamTaskDeleted = "team.task.deleted" EventTeamTaskStale = "team.task.stale" + // Emitted when leader starts processing completed team task results (before announce run). + EventTeamLeaderProcessing = "team.leader.processing" + // Team CRUD events (admin operations). EventTeamCreated = "team.created" EventTeamUpdated = "team.updated" diff --git a/ui/web/src/api/protocol.ts b/ui/web/src/api/protocol.ts index e59e94f9..e194dc03 100644 --- a/ui/web/src/api/protocol.ts +++ b/ui/web/src/api/protocol.ts @@ -206,6 +206,9 @@ export const Events = { TEAM_TASK_DISPATCHED: "team.task.dispatched", TEAM_TASK_DELETED: "team.task.deleted", + // Team leader processing (bridges gap between last task.completed and announce run.started) + TEAM_LEADER_PROCESSING: "team.leader.processing", + // Team messages TEAM_MESSAGE_SENT: "team.message.sent", diff --git a/ui/web/src/components/chat/active-run-zone.tsx b/ui/web/src/components/chat/active-run-zone.tsx index 9012c77c..ace4534f 100644 --- a/ui/web/src/components/chat/active-run-zone.tsx +++ b/ui/web/src/components/chat/active-run-zone.tsx @@ -63,7 +63,7 @@ export function ActiveRunZone({ )} - {isRunning && ( + {(isRunning || activity?.phase === "leader_processing") && ( )} diff --git a/ui/web/src/components/chat/activity-indicator.tsx b/ui/web/src/components/chat/activity-indicator.tsx index a3fbcce4..abff3717 100644 --- a/ui/web/src/components/chat/activity-indicator.tsx +++ b/ui/web/src/components/chat/activity-indicator.tsx @@ -1,4 +1,4 @@ -import { Brain, Wrench, Pencil, Archive, RefreshCw } from "lucide-react"; +import { Brain, Wrench, Pencil, Archive, RefreshCw, Users } from "lucide-react"; import type { RunActivity } from "@/types/chat"; interface ActivityIndicatorProps { @@ -7,7 +7,7 @@ interface ActivityIndicatorProps { } export function ActivityIndicator({ activity, isRunning }: ActivityIndicatorProps) { - if (!isRunning) return null; + if (!isRunning && activity?.phase !== "leader_processing") return null; if (!activity) { return ( @@ -51,6 +51,8 @@ function getPhaseConfig(activity: RunActivity) { color: "text-orange-500", label: `Retrying (${activity.retryAttempt ?? 0}/${activity.retryMax ?? 0})...`, }; + case "leader_processing": + return { icon: Users, color: "text-emerald-500", label: "Processing team results..." }; default: return { icon: Brain, color: "text-muted-foreground", label: "Working..." }; } diff --git a/ui/web/src/components/chat/chat-top-bar.tsx b/ui/web/src/components/chat/chat-top-bar.tsx index 77ebbf03..cd587a70 100644 --- a/ui/web/src/components/chat/chat-top-bar.tsx +++ b/ui/web/src/components/chat/chat-top-bar.tsx @@ -19,6 +19,7 @@ const phaseLabels: Record = { streaming: "Responding…", compacting: "Compacting…", retrying: "Retrying…", + leader_processing: "Processing team results…", }; export function ChatTopBar({ agentId, isRunning, isBusy, activity, teamTasks }: ChatTopBarProps) { diff --git a/ui/web/src/pages/chat/hooks/use-chat-messages.ts b/ui/web/src/pages/chat/hooks/use-chat-messages.ts index 2e7418c5..aa6fd6e5 100644 --- a/ui/web/src/pages/chat/hooks/use-chat-messages.ts +++ b/ui/web/src/pages/chat/hooks/use-chat-messages.ts @@ -433,14 +433,24 @@ export function useChatMessages(sessionKey: string, agentId: string) { useWsEvent(Events.TEAM_TASK_PROGRESS, onTaskProgress); useWsEvent(Events.TEAM_TASK_ASSIGNED, onTaskAssigned); + // Leader processing: backend emits when announce queue drains (before announce run starts). + const handleLeaderProcessing = useCallback((payload: unknown) => { + const event = payload as { agentId?: string; tasks?: number }; + if (event?.agentId === agentIdRef.current) { + activityRef.current = { phase: "leader_processing" }; + setActivity(activityRef.current); + } + }, []); + useWsEvent(Events.TEAM_LEADER_PROCESSING, handleLeaderProcessing); + // Add a local message optimistically (shown immediately, replaced on next loadHistory) const addLocalMessage = useCallback((msg: ChatMessage) => { setMessages((prev) => [...prev, msg]); }, []); - // isBusy: true when main agent is running OR team tasks are active. - // Used for stop button visibility, top bar status, empty state check. - const isBusy = isRunning || teamTasks.length > 0; + // isBusy: true when main agent is running, team tasks are active, + // or leader is processing team results before announce run starts. + const isBusy = isRunning || teamTasks.length > 0 || activity?.phase === "leader_processing"; return { messages, diff --git a/ui/web/src/types/chat.ts b/ui/web/src/types/chat.ts index c2038fc0..c3ec44e9 100644 --- a/ui/web/src/types/chat.ts +++ b/ui/web/src/types/chat.ts @@ -4,7 +4,7 @@ import type { Message } from "./session"; /** Activity phase tracking during agent run */ export interface RunActivity { - phase: "thinking" | "tool_exec" | "streaming" | "compacting" | "retrying"; + phase: "thinking" | "tool_exec" | "streaming" | "compacting" | "retrying" | "leader_processing"; tool?: string; tools?: string[]; iteration?: number;