feat: broadcast pilot agent traces via WebSocket for live status on reconnect

Agent trace logs are now stored in-memory (pilotRunStore) and broadcast
through the existing /ws/crm-updates WebSocket channel. When a client
reconnects, it receives a pilot.catchup with all accumulated logs so the
user sees agent progress even after page reload. Three new WS event
types: pilot.trace, pilot.finished, pilot.catchup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruslan Bakiev
2026-02-25 08:45:32 +07:00
parent b830f3728c
commit bf7f4ae933
6 changed files with 174 additions and 16 deletions

View File

@@ -373,6 +373,8 @@ const {
pushPilotNote,
refetchChatMessages,
refetchChatConversations,
handleRealtimePilotTrace,
handleRealtimePilotFinished,
destroyPilotWaveSurfer,
togglePilotLiveLogsExpanded,
} = pilotChat;
@@ -678,6 +680,8 @@ const { crmRealtimeState, startCrmRealtime, stopCrmRealtime } = useCrmRealtime({
// Refresh contacts to update sidebar preview (lastMessageText, lastAt, hasUnread)
void refetchContacts();
},
onPilotTrace: (log) => handleRealtimePilotTrace(log),
onPilotFinished: () => void handleRealtimePilotFinished(),
});
// ---------------------------------------------------------------------------

View File

@@ -9,10 +9,14 @@ export type RealtimeNewMessage = {
at: string;
};
export type RealtimePilotTrace = { text: string; at: string };
export function useCrmRealtime(opts: {
isAuthenticated: () => boolean;
onDashboardChanged: () => Promise<void>;
onNewMessage?: (msg: RealtimeNewMessage) => void;
onPilotTrace?: (log: RealtimePilotTrace) => void;
onPilotFinished?: () => void;
}) {
const crmRealtimeState = ref<"idle" | "connecting" | "open" | "error">("idle");
let crmRealtimeSocket: WebSocket | null = null;
@@ -116,6 +120,17 @@ export function useCrmRealtime(opts: {
if (payload.type === "message.new" && opts.onNewMessage) {
opts.onNewMessage(payload as unknown as RealtimeNewMessage);
}
if (payload.type === "pilot.trace" && opts.onPilotTrace) {
opts.onPilotTrace({ text: String(payload.text ?? ""), at: String(payload.at ?? "") });
}
if (payload.type === "pilot.catchup" && opts.onPilotTrace && Array.isArray(payload.logs)) {
for (const log of payload.logs) {
opts.onPilotTrace({ text: String((log as any).text ?? ""), at: String((log as any).at ?? "") });
}
}
if (payload.type === "pilot.finished" && opts.onPilotFinished) {
opts.onPilotFinished();
}
} catch {
// ignore malformed realtime payloads
}

View File

@@ -689,6 +689,24 @@ export function usePilotChat(opts: {
pushPilotNote,
refetchChatMessages,
refetchChatConversations,
// realtime pilot trace handlers (called from useCrmRealtime)
handleRealtimePilotTrace(log: { text: string; at: string }) {
const text = String(log.text ?? "").trim();
if (!text) return;
// Mark as sending so the UI shows live-log panel
if (!pilotSending.value) pilotSending.value = true;
pilotLiveLogs.value = [
...pilotLiveLogs.value,
{ id: `ws-${Date.now()}-${Math.random()}`, text, at: log.at || new Date().toISOString() },
];
},
async handleRealtimePilotFinished() {
pilotSending.value = false;
livePilotUserText.value = "";
livePilotAssistantText.value = "";
pilotLiveLogs.value = [];
await Promise.all([refetchChatMessages(), refetchChatConversations(), opts.refetchAllCrmQueries()]);
},
// cleanup
destroyPilotWaveSurfer,
};