| 1 | import { useEffect } from "react"; |
| 2 | import { |
| 3 | app, |
| 4 | onEvent, |
| 5 | onReady, |
| 6 | onRemoteForwards, |
| 7 | onRemoteServer, |
| 8 | onRemoteStatus, |
| 9 | onRuntimeRebuilt, |
| 10 | } from "../lib/bridge"; |
| 11 | import { generativeMusic, isGenerativeMusicEnabled } from "../lib/generative-music"; |
| 12 | import { startTerminalEventBridge } from "../lib/terminalEvents"; |
| 13 | import { trackAppSubscription } from "./appLifecycleProbe"; |
| 14 | import { createSubscriptionScope } from "../lib/subscriptionScope"; |
| 15 | |
| 16 | export type RuntimeEventListener = Parameters<typeof onEvent>[0]; |
| 17 | export type RuntimeReadyListener = Parameters<typeof onReady>[0]; |
| 18 | export type RuntimeRebuiltListener = Parameters<typeof onRuntimeRebuilt>[0]; |
| 19 | export type RemoteStatusListener = Parameters<typeof onRemoteStatus>[0]; |
| 20 | export type RemoteForwardsListener = Parameters<typeof onRemoteForwards>[0]; |
| 21 | export type RemoteServerListener = Parameters<typeof onRemoteServer>[0]; |
| 22 | |
| 23 | type AppRuntimeEffectsProps = { |
| 24 | running: boolean; |
| 25 | onEvent: RuntimeEventListener; |
| 26 | onReady: RuntimeReadyListener; |
| 27 | onRebuilt: RuntimeRebuiltListener; |
| 28 | onRemoteStatus: RemoteStatusListener; |
| 29 | onRemoteForwards: RemoteForwardsListener; |
| 30 | onRemoteServer: RemoteServerListener; |
| 31 | onInitialRemoteHosts: (hosts: Awaited<ReturnType<typeof app.RemoteHosts>>) => void; |
| 32 | onInitialRemoteStatuses: (statuses: Awaited<ReturnType<typeof app.RemoteConnectionStatuses>>) => void; |
| 33 | }; |
| 34 | |
| 35 | /** Owns app-wide bridge subscriptions; App regions never subscribe directly. */ |
| 36 | export function AppRuntimeEffects(props: AppRuntimeEffectsProps) { |
| 37 | const { onEvent: eventListener, onReady: readyListener, onRebuilt, onRemoteStatus: statusListener, |
| 38 | onRemoteForwards: forwardsListener, onRemoteServer: serverListener, |
| 39 | onInitialRemoteHosts, onInitialRemoteStatuses, running } = props; |
| 40 | useEffect(startTerminalEventBridge, []); |
| 41 | useEffect(() => { |
| 42 | const scope = createSubscriptionScope(trackAppSubscription); |
| 43 | scope.listen(onEvent, (event) => { |
| 44 | eventListener(event); |
| 45 | if (event.kind === "text" || event.kind === "reasoning" || event.kind === "tool_dispatch") { |
| 46 | generativeMusic.playTokenNote(); |
| 47 | } |
| 48 | }); |
| 49 | scope.listen(onReady, readyListener); |
| 50 | scope.listen(onRuntimeRebuilt, onRebuilt); |
| 51 | scope.listen(onRemoteStatus, statusListener); |
| 52 | scope.listen(onRemoteForwards, forwardsListener); |
| 53 | scope.listen(onRemoteServer, serverListener); |
| 54 | return () => scope.dispose(); |
| 55 | }, [eventListener, readyListener, onRebuilt, forwardsListener, serverListener, statusListener]); |
| 56 | |
| 57 | useEffect(() => { |
| 58 | let disposed = false; |
| 59 | void app.RemoteHosts().then((hosts) => { if (!disposed) onInitialRemoteHosts(hosts); }).catch(() => {}); |
| 60 | void app.RemoteConnectionStatuses().then((statuses) => { if (!disposed) onInitialRemoteStatuses(statuses); }).catch(() => {}); |
| 61 | return () => { disposed = true; }; |
| 62 | }, [onInitialRemoteHosts, onInitialRemoteStatuses]); |
| 63 | |
| 64 | useEffect(() => { |
| 65 | if (running && isGenerativeMusicEnabled()) generativeMusic.start(); |
| 66 | else generativeMusic.stop(); |
| 67 | return () => generativeMusic.stop(); |
| 68 | }, [running]); |
| 69 | |
| 70 | return null; |
| 71 | } |
| 72 |