( messages: Message[], setMessages: (action: React.SetStateAction<Message[]>) => void, abortControllerRef: React.RefObject<AbortController | null>, commands: readonly Command[], mainLoopModel: string, )
| 70 | * Inbound messages from claude.ai are injected into the REPL via queuedCommands. |
| 71 | */ |
| 72 | export function useReplBridge( |
| 73 | messages: Message[], |
| 74 | setMessages: (action: React.SetStateAction<Message[]>) => void, |
| 75 | abortControllerRef: React.RefObject<AbortController | null>, |
| 76 | commands: readonly Command[], |
| 77 | mainLoopModel: string, |
| 78 | ): { sendBridgeResult: () => void } { |
| 79 | const handleRef = useRef<ReplBridgeHandle | null>(null); |
| 80 | const teardownPromiseRef = useRef<Promise<void> | undefined>(undefined); |
| 81 | const lastWrittenIndexRef = useRef(0); |
| 82 | const pendingResultAfterFlushRef = useRef(false); |
| 83 | const transcriptResetPendingRef = useRef(false); |
| 84 | // Tracks UUIDs already flushed as initial messages. Persists across |
| 85 | // bridge reconnections so Bridge #2+ only sends new messages — sending |
| 86 | // duplicate UUIDs causes the server to kill the WebSocket. |
| 87 | const flushedUUIDsRef = useRef(new Set<string>()); |
| 88 | const failureTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); |
| 89 | // Persists across effect re-runs (unlike the effect's local state). Reset |
| 90 | // only on successful init. Hits MAX_CONSECUTIVE_INIT_FAILURES → fuse blown |
| 91 | // for the session, regardless of replBridgeEnabled re-toggling. |
| 92 | const consecutiveFailuresRef = useRef(0); |
| 93 | const setAppState = useSetAppState(); |
| 94 | const commandsRef = useRef(commands); |
| 95 | commandsRef.current = commands; |
| 96 | const mainLoopModelRef = useRef(mainLoopModel); |
| 97 | mainLoopModelRef.current = mainLoopModel; |
| 98 | const messagesRef = useRef(messages); |
| 99 | messagesRef.current = messages; |
| 100 | const store = useAppStateStore(); |
| 101 | const { addNotification } = useNotifications(); |
| 102 | const replBridgeEnabledRaw = useAppState(s => s.replBridgeEnabled); |
| 103 | const replBridgeEnabled = feature('BRIDGE_MODE') ? replBridgeEnabledRaw : false; |
| 104 | const replBridgeConnectedRaw = useAppState(s => s.replBridgeConnected); |
| 105 | const replBridgeConnected = feature('BRIDGE_MODE') ? replBridgeConnectedRaw : false; |
| 106 | const replBridgeSessionActiveRaw = useAppState(s => s.replBridgeSessionActive); |
| 107 | const replBridgeSessionActive = feature('BRIDGE_MODE') ? replBridgeSessionActiveRaw : false; |
| 108 | const replBridgeOutboundOnlyRaw = useAppState(s => s.replBridgeOutboundOnly); |
| 109 | const replBridgeOutboundOnly = feature('BRIDGE_MODE') ? replBridgeOutboundOnlyRaw : false; |
| 110 | const replBridgeInitialNameRaw = useAppState(s => s.replBridgeInitialName); |
| 111 | const replBridgeInitialName = feature('BRIDGE_MODE') ? replBridgeInitialNameRaw : undefined; |
| 112 | |
| 113 | // Initialize/teardown bridge when enabled state changes. |
| 114 | // Passes current messages as initialMessages so the remote session |
| 115 | // starts with the existing conversation context (e.g. from /bridge). |
| 116 | useEffect(() => { |
| 117 | // feature() check must use positive pattern for dead code elimination — |
| 118 | // negative pattern (if (!feature(...)) return) does NOT eliminate |
| 119 | // dynamic imports below. |
| 120 | if (feature('BRIDGE_MODE')) { |
| 121 | if (!replBridgeEnabled) return; |
| 122 | |
| 123 | const outboundOnly = replBridgeOutboundOnly; |
| 124 | function notifyBridgeFailed(detail?: string): void { |
| 125 | if (outboundOnly) return; |
| 126 | addNotification({ |
| 127 | key: 'bridge-failed', |
| 128 | jsx: ( |
| 129 | <> |
no test coverage detected