| 1 | "use client"; |
| 2 | |
| 3 | import { useEffect, useRef } from "react"; |
| 4 | |
| 5 | export type CanopyEventType = |
| 6 | | "run:created" |
| 7 | | "run:started" |
| 8 | | "run:completed" |
| 9 | | "run:cancelled" |
| 10 | | "step:started" |
| 11 | | "step:completed" |
| 12 | | "step:skipped" |
| 13 | | "log:append"; |
| 14 | |
| 15 | export interface CanopyEvent { |
| 16 | type: CanopyEventType; |
| 17 | runId: number; |
| 18 | repoId: number; |
| 19 | stepId?: number; |
| 20 | stepIndex?: number; |
| 21 | status?: string; |
| 22 | run?: Record<string, unknown>; |
| 23 | step?: Record<string, unknown>; |
| 24 | log?: { stream: string; content: string; created_at: string }; |
| 25 | ts: string; |
| 26 | } |
| 27 | |
| 28 | interface UseCanopyEventsOptions { |
| 29 | scope: "run" | "repo" | "global"; |
| 30 | runId?: number; |
| 31 | owner?: string; |
| 32 | repo?: string; |
| 33 | onEvent: (event: CanopyEvent) => void; |
| 34 | enabled?: boolean; |
| 35 | } |
| 36 | |
| 37 | export function useCanopyEvents({ |
| 38 | scope, |
| 39 | runId, |
| 40 | owner, |
| 41 | repo, |
| 42 | onEvent, |
| 43 | enabled = true, |
| 44 | }: UseCanopyEventsOptions) { |
| 45 | const handlerRef = useRef(onEvent); |
| 46 | handlerRef.current = onEvent; |
| 47 | |
| 48 | useEffect(() => { |
| 49 | if (!enabled) return; |
| 50 | |
| 51 | const params = new URLSearchParams({ scope }); |
| 52 | if (runId != null) params.set("runId", String(runId)); |
| 53 | if (owner) params.set("owner", owner); |
| 54 | if (repo) params.set("repo", repo); |
| 55 | |
| 56 | const es = new EventSource(`/api/canopy/events?${params}`); |
| 57 | |
| 58 | const eventTypes: CanopyEventType[] = [ |
| 59 | "run:created", |
| 60 | "run:started", |
| 61 | "run:completed", |
| 62 | "run:cancelled", |
| 63 | "step:started", |
| 64 | "step:completed", |
| 65 | "step:skipped", |
| 66 | "log:append", |
| 67 | ]; |
| 68 | |
| 69 | for (const type of eventTypes) { |
| 70 | es.addEventListener(type, (e: MessageEvent) => { |
| 71 | try { |
| 72 | const data = JSON.parse(e.data) as CanopyEvent; |
| 73 | handlerRef.current(data); |
| 74 | } catch {} |
| 75 | }); |
| 76 | } |
| 77 | |
| 78 | return () => es.close(); |
| 79 | }, [scope, runId, owner, repo, enabled]); |
| 80 | } |
| 81 | |