feat(ui): team leader processing indicator and announce run streaming

- 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
This commit is contained in:
viettranx committed 2026-03-20 22:26:22 +07:00
1 parent c6bab9a059
commit 9ad3043fe9
8 files changed
+36 -7

No files matched your search

+10
View File
@@ -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,
+3
View File
@@ -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"
+3
View File
@@ -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",
@@ -63,7 +63,7 @@ export function ActiveRunZone({
</div>
)}
{isRunning && (
{(isRunning || activity?.phase === "leader_processing") && (
<ActivityIndicator activity={activity} isRunning={isRunning} />
)}
</div>
@@ -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..." };
}
@@ -19,6 +19,7 @@ const phaseLabels: Record<RunActivity["phase"], string> = {
streaming: "Responding…",
compacting: "Compacting…",
retrying: "Retrying…",
leader_processing: "Processing team results…",
};
export function ChatTopBar({ agentId, isRunning, isBusy, activity, teamTasks }: ChatTopBarProps) {
@@ -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,
+1 -1
View File
@@ -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;