@@ -21,6 +21,42 @@ const RECONNECT_DELAY_BASE = 1000; // 1 second
2121const RECONNECT_DELAY_MAX = 30000 ; // 30 seconds
2222const RECONNECT_DELAY_MULTIPLIER = 1.5 ;
2323
24+ // Access store state/actions via getState() - avoids calling hooks in callbacks/effects
25+ // This is the zustand-recommended pattern for non-rendering store access
26+ function getStoreState ( ) {
27+ const notification = useNotificationStore . getState ( ) ;
28+ const execution = useExecutionStore . getState ( ) ;
29+ const flow = useFlowStore . getState ( ) ;
30+ const cliStream = useCliStreamStore . getState ( ) ;
31+ const coordinator = useCoordinatorStore . getState ( ) ;
32+ return {
33+ // Notification store
34+ setWsStatus : notification . setWsStatus ,
35+ setWsLastMessage : notification . setWsLastMessage ,
36+ incrementReconnectAttempts : notification . incrementReconnectAttempts ,
37+ resetReconnectAttempts : notification . resetReconnectAttempts ,
38+ addA2UINotification : notification . addA2UINotification ,
39+ // Execution store
40+ setExecutionStatus : execution . setExecutionStatus ,
41+ setNodeStarted : execution . setNodeStarted ,
42+ setNodeCompleted : execution . setNodeCompleted ,
43+ setNodeFailed : execution . setNodeFailed ,
44+ addLog : execution . addLog ,
45+ completeExecution : execution . completeExecution ,
46+ currentExecution : execution . currentExecution ,
47+ // Flow store
48+ updateNode : flow . updateNode ,
49+ // CLI stream store
50+ addOutput : cliStream . addOutput ,
51+ // Coordinator store
52+ updateNodeStatus : coordinator . updateNodeStatus ,
53+ addCoordinatorLog : coordinator . addLog ,
54+ setActiveQuestion : coordinator . setActiveQuestion ,
55+ markExecutionComplete : coordinator . markExecutionComplete ,
56+ coordinatorExecutionId : coordinator . currentExecutionId ,
57+ } ;
58+ }
59+
2460interface UseWebSocketOptions {
2561 enabled ?: boolean ;
2662 onMessage ?: ( message : OrchestratorWebSocketMessage ) => void ;
@@ -40,74 +76,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
4076 const reconnectDelayRef = useRef ( RECONNECT_DELAY_BASE ) ;
4177 const mountedRef = useRef ( true ) ;
4278
43- // Store refs to prevent handler recreation - use useRef to keep stable references
44- const storeRefs = useRef ( {
45- // Notification store
46- setWsStatus : useNotificationStore ( ( state ) => state . setWsStatus ) ,
47- setWsLastMessage : useNotificationStore ( ( state ) => state . setWsLastMessage ) ,
48- incrementReconnectAttempts : useNotificationStore ( ( state ) => state . incrementReconnectAttempts ) ,
49- resetReconnectAttempts : useNotificationStore ( ( state ) => state . resetReconnectAttempts ) ,
50- addA2UINotification : useNotificationStore ( ( state ) => state . addA2UINotification ) ,
51-
52- // Execution store
53- setExecutionStatus : useExecutionStore ( ( state ) => state . setExecutionStatus ) ,
54- setNodeStarted : useExecutionStore ( ( state ) => state . setNodeStarted ) ,
55- setNodeCompleted : useExecutionStore ( ( state ) => state . setNodeCompleted ) ,
56- setNodeFailed : useExecutionStore ( ( state ) => state . setNodeFailed ) ,
57- addLog : useExecutionStore ( ( state ) => state . addLog ) ,
58- completeExecution : useExecutionStore ( ( state ) => state . completeExecution ) ,
59- currentExecution : useExecutionStore ( ( state ) => state . currentExecution ) ,
60-
61- // Flow store
62- updateNode : useFlowStore ( ( state ) => state . updateNode ) ,
63-
64- // CLI stream store
65- addOutput : useCliStreamStore ( ( state ) => state . addOutput ) ,
66-
67- // Coordinator store
68- updateNodeStatus : useCoordinatorStore ( ( state ) => state . updateNodeStatus ) ,
69- addCoordinatorLog : useCoordinatorStore ( ( state ) => state . addLog ) ,
70- setActiveQuestion : useCoordinatorStore ( ( state ) => state . setActiveQuestion ) ,
71- markExecutionComplete : useCoordinatorStore ( ( state ) => state . markExecutionComplete ) ,
72- coordinatorExecutionId : useCoordinatorStore ( ( state ) => state . currentExecutionId ) ,
73- } ) ;
74-
75- // Update refs periodically to ensure they have fresh store references
76- useEffect ( ( ) => {
77- storeRefs . current = {
78- // Notification store
79- setWsStatus : useNotificationStore ( ( state ) => state . setWsStatus ) ,
80- setWsLastMessage : useNotificationStore ( ( state ) => state . setWsLastMessage ) ,
81- incrementReconnectAttempts : useNotificationStore ( ( state ) => state . incrementReconnectAttempts ) ,
82- resetReconnectAttempts : useNotificationStore ( ( state ) => state . resetReconnectAttempts ) ,
83- addA2UINotification : useNotificationStore ( ( state ) => state . addA2UINotification ) ,
84-
85- // Execution store
86- setExecutionStatus : useExecutionStore ( ( state ) => state . setExecutionStatus ) ,
87- setNodeStarted : useExecutionStore ( ( state ) => state . setNodeStarted ) ,
88- setNodeCompleted : useExecutionStore ( ( state ) => state . setNodeCompleted ) ,
89- setNodeFailed : useExecutionStore ( ( state ) => state . setNodeFailed ) ,
90- addLog : useExecutionStore ( ( state ) => state . addLog ) ,
91- completeExecution : useExecutionStore ( ( state ) => state . completeExecution ) ,
92- currentExecution : useExecutionStore ( ( state ) => state . currentExecution ) ,
93-
94- // Flow store
95- updateNode : useFlowStore ( ( state ) => state . updateNode ) ,
96-
97- // CLI stream store
98- addOutput : useCliStreamStore ( ( state ) => state . addOutput ) ,
99-
100- // Coordinator store
101- updateNodeStatus : useCoordinatorStore ( ( state ) => state . updateNodeStatus ) ,
102- addCoordinatorLog : useCoordinatorStore ( ( state ) => state . addLog ) ,
103- setActiveQuestion : useCoordinatorStore ( ( state ) => state . setActiveQuestion ) ,
104- markExecutionComplete : useCoordinatorStore ( ( state ) => state . markExecutionComplete ) ,
105- coordinatorExecutionId : useCoordinatorStore ( ( state ) => state . currentExecutionId ) ,
106- } ;
107- } ) ; // Run on every render to keep refs fresh
108-
10979 // Handle incoming WebSocket messages
110- // Note: Using refs via storeRefs to prevent handler recreation on every store change
11180 const handleMessage = useCallback (
11281 ( event : MessageEvent ) => {
11382 // Guard against state updates after unmount
@@ -117,9 +86,10 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
11786
11887 try {
11988 const data = JSON . parse ( event . data ) ;
89+ const stores = getStoreState ( ) ;
12090
12191 // Store last message for debugging
122- storeRefs . current . setWsLastMessage ( data ) ;
92+ stores . setWsLastMessage ( data ) ;
12393
12494 // Handle CLI messages
12595 if ( data . type ?. startsWith ( 'CLI_' ) ) {
@@ -128,7 +98,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
12898 const { executionId, tool, mode, timestamp } = data . payload ;
12999
130100 // Add system message for CLI start
131- storeRefs . current . addOutput ( executionId , {
101+ stores . addOutput ( executionId , {
132102 type : 'system' ,
133103 content : `[${ new Date ( timestamp ) . toLocaleTimeString ( ) } ] CLI execution started: ${ tool } (${ mode || 'default' } mode)` ,
134104 timestamp : Date . now ( ) ,
@@ -157,7 +127,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
157127 lines . forEach ( ( line : string ) => {
158128 // Add non-empty lines, or single line if that's all we have
159129 if ( line . trim ( ) || lines . length === 1 ) {
160- storeRefs . current . addOutput ( executionId , {
130+ stores . addOutput ( executionId , {
161131 type : unitType as any ,
162132 content : line ,
163133 timestamp : Date . now ( ) ,
@@ -173,7 +143,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
173143 const statusText = success ? 'completed successfully' : 'failed' ;
174144 const durationText = duration ? ` (${ duration } ms)` : '' ;
175145
176- storeRefs . current . addOutput ( executionId , {
146+ stores . addOutput ( executionId , {
177147 type : 'system' ,
178148 content : `[${ new Date ( ) . toLocaleTimeString ( ) } ] CLI execution ${ statusText } ${ durationText } ` ,
179149 timestamp : Date . now ( ) ,
@@ -188,7 +158,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
188158 if ( data . type === 'a2ui-surface' ) {
189159 const parsed = SurfaceUpdateSchema . safeParse ( data . payload ) ;
190160 if ( parsed . success ) {
191- storeRefs . current . addA2UINotification ( parsed . data , 'Interactive UI' ) ;
161+ stores . addA2UINotification ( parsed . data , 'Interactive UI' ) ;
192162 } else {
193163 console . warn ( '[WebSocket] Invalid A2UI surface:' , parsed . error . issues ) ;
194164 }
@@ -197,7 +167,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
197167
198168 // Handle Coordinator messages
199169 if ( data . type ?. startsWith ( 'COORDINATOR_' ) ) {
200- const { coordinatorExecutionId } = storeRefs . current ;
170+ const { coordinatorExecutionId } = stores ;
201171 // Only process messages for current coordinator execution
202172 if ( coordinatorExecutionId && data . executionId !== coordinatorExecutionId ) {
203173 return ;
@@ -208,26 +178,26 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
208178 case 'COORDINATOR_STATE_UPDATE' :
209179 // Check for completion
210180 if ( data . status === 'completed' ) {
211- storeRefs . current . markExecutionComplete ( true ) ;
181+ stores . markExecutionComplete ( true ) ;
212182 } else if ( data . status === 'failed' ) {
213- storeRefs . current . markExecutionComplete ( false ) ;
183+ stores . markExecutionComplete ( false ) ;
214184 }
215185 break ;
216186
217187 case 'COORDINATOR_COMMAND_STARTED' :
218- storeRefs . current . updateNodeStatus ( data . nodeId , 'running' ) ;
188+ stores . updateNodeStatus ( data . nodeId , 'running' ) ;
219189 break ;
220190
221191 case 'COORDINATOR_COMMAND_COMPLETED' :
222- storeRefs . current . updateNodeStatus ( data . nodeId , 'completed' , data . result ) ;
192+ stores . updateNodeStatus ( data . nodeId , 'completed' , data . result ) ;
223193 break ;
224194
225195 case 'COORDINATOR_COMMAND_FAILED' :
226- storeRefs . current . updateNodeStatus ( data . nodeId , 'failed' , undefined , data . error ) ;
196+ stores . updateNodeStatus ( data . nodeId , 'failed' , undefined , data . error ) ;
227197 break ;
228198
229199 case 'COORDINATOR_LOG_ENTRY' :
230- storeRefs . current . addCoordinatorLog (
200+ stores . addCoordinatorLog (
231201 data . log . message ,
232202 data . log . level ,
233203 data . log . nodeId ,
@@ -236,7 +206,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
236206 break ;
237207
238208 case 'COORDINATOR_QUESTION_ASKED' :
239- storeRefs . current . setActiveQuestion ( data . question ) ;
209+ stores . setActiveQuestion ( data . question ) ;
240210 break ;
241211
242212 case 'COORDINATOR_ANSWER_RECEIVED' :
@@ -262,47 +232,47 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
262232 const message = parsed . data as OrchestratorWebSocketMessage ;
263233
264234 // Only process messages for current execution
265- const { currentExecution } = storeRefs . current ;
235+ const { currentExecution } = stores ;
266236 if ( currentExecution && message . execId !== currentExecution . execId ) {
267237 return ;
268238 }
269239
270240 // Dispatch to execution store based on message type
271241 switch ( message . type ) {
272242 case 'ORCHESTRATOR_STATE_UPDATE' :
273- storeRefs . current . setExecutionStatus ( message . status , message . currentNodeId ) ;
243+ stores . setExecutionStatus ( message . status , message . currentNodeId ) ;
274244 // Check for completion
275245 if ( message . status === 'completed' || message . status === 'failed' ) {
276- storeRefs . current . completeExecution ( message . status ) ;
246+ stores . completeExecution ( message . status ) ;
277247 }
278248 break ;
279249
280250 case 'ORCHESTRATOR_NODE_STARTED' :
281- storeRefs . current . setNodeStarted ( message . nodeId ) ;
251+ stores . setNodeStarted ( message . nodeId ) ;
282252 // Update canvas node status
283- storeRefs . current . updateNode ( message . nodeId , { executionStatus : 'running' } ) ;
253+ stores . updateNode ( message . nodeId , { executionStatus : 'running' } ) ;
284254 break ;
285255
286256 case 'ORCHESTRATOR_NODE_COMPLETED' :
287- storeRefs . current . setNodeCompleted ( message . nodeId , message . result ) ;
257+ stores . setNodeCompleted ( message . nodeId , message . result ) ;
288258 // Update canvas node status
289- storeRefs . current . updateNode ( message . nodeId , {
259+ stores . updateNode ( message . nodeId , {
290260 executionStatus : 'completed' ,
291261 executionResult : message . result ,
292262 } ) ;
293263 break ;
294264
295265 case 'ORCHESTRATOR_NODE_FAILED' :
296- storeRefs . current . setNodeFailed ( message . nodeId , message . error ) ;
266+ stores . setNodeFailed ( message . nodeId , message . error ) ;
297267 // Update canvas node status
298- storeRefs . current . updateNode ( message . nodeId , {
268+ stores . updateNode ( message . nodeId , {
299269 executionStatus : 'failed' ,
300270 executionError : message . error ,
301271 } ) ;
302272 break ;
303273
304274 case 'ORCHESTRATOR_LOG' :
305- storeRefs . current . addLog ( message . log as ExecutionLog ) ;
275+ stores . addLog ( message . log as ExecutionLog ) ;
306276 break ;
307277 }
308278
@@ -312,7 +282,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
312282 console . error ( '[WebSocket] Failed to parse message:' , error ) ;
313283 }
314284 } ,
315- [ onMessage ] // Only dependency is onMessage, all other functions accessed via refs
285+ [ onMessage ] // Only dependency is onMessage, store access via getState()
316286 ) ;
317287
318288 // Connect to WebSocket
@@ -329,8 +299,9 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
329299 const delay = reconnectDelayRef . current ;
330300 console . log ( `[WebSocket] Reconnecting in ${ delay } ms...` ) ;
331301
332- storeRefs . current . setWsStatus ( 'reconnecting' ) ;
333- storeRefs . current . incrementReconnectAttempts ( ) ;
302+ const stores = getStoreState ( ) ;
303+ stores . setWsStatus ( 'reconnecting' ) ;
304+ stores . incrementReconnectAttempts ( ) ;
334305
335306 reconnectTimeoutRef . current = setTimeout ( ( ) => {
336307 connectRef . current ?.( ) ;
@@ -341,7 +312,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
341312 reconnectDelayRef . current * RECONNECT_DELAY_MULTIPLIER ,
342313 RECONNECT_DELAY_MAX
343314 ) ;
344- } , [ ] ) ; // No dependencies - uses connectRef
315+ } , [ ] ) ; // No dependencies - uses connectRef and getStoreState()
345316
346317 const connect = useCallback ( ( ) => {
347318 if ( ! enabled ) return ;
@@ -351,34 +322,35 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
351322 const wsUrl = `${ protocol } //${ window . location . host } /ws` ;
352323
353324 try {
354- storeRefs . current . setWsStatus ( 'connecting' ) ;
325+ getStoreState ( ) . setWsStatus ( 'connecting' ) ;
355326
356327 const ws = new WebSocket ( wsUrl ) ;
357328 wsRef . current = ws ;
358329
359330 ws . onopen = ( ) => {
360331 console . log ( '[WebSocket] Connected' ) ;
361- storeRefs . current . setWsStatus ( 'connected' ) ;
362- storeRefs . current . resetReconnectAttempts ( ) ;
332+ const s = getStoreState ( ) ;
333+ s . setWsStatus ( 'connected' ) ;
334+ s . resetReconnectAttempts ( ) ;
363335 reconnectDelayRef . current = RECONNECT_DELAY_BASE ;
364336 } ;
365337
366338 ws . onmessage = handleMessage ;
367339
368340 ws . onclose = ( ) => {
369341 console . log ( '[WebSocket] Disconnected' ) ;
370- storeRefs . current . setWsStatus ( 'disconnected' ) ;
342+ getStoreState ( ) . setWsStatus ( 'disconnected' ) ;
371343 wsRef . current = null ;
372344 scheduleReconnect ( ) ;
373345 } ;
374346
375347 ws . onerror = ( error ) => {
376348 console . error ( '[WebSocket] Error:' , error ) ;
377- storeRefs . current . setWsStatus ( 'error' ) ;
349+ getStoreState ( ) . setWsStatus ( 'error' ) ;
378350 } ;
379351 } catch ( error ) {
380352 console . error ( '[WebSocket] Failed to connect:' , error ) ;
381- storeRefs . current . setWsStatus ( 'error' ) ;
353+ getStoreState ( ) . setWsStatus ( 'error' ) ;
382354 scheduleReconnect ( ) ;
383355 }
384356 } , [ enabled , handleMessage , scheduleReconnect ] ) ;
0 commit comments