Skip to content

Commit a4a3599

Browse files
authored
Merge pull request microsoft#430 from microsoft/macae-UI-changes
Fix: User Auth header fix
2 parents 72b5a55 + 5a85556 commit a4a3599

File tree

1 file changed

+123
-25
lines changed

1 file changed

+123
-25
lines changed

src/frontend/src/services/WebSocketService.tsx

Lines changed: 123 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
/**
2-
* WebSocket Service for real-time plan execution streaming
3-
*/
1+
import { headerBuilder } from '../api/config';
42

53
export interface StreamMessage {
6-
type: 'plan_update' | 'step_update' | 'agent_message' | 'error' | 'connection_status';
4+
type: 'plan_update' | 'step_update' | 'agent_message' | 'error' | 'connection_status' | 'plan_approval_request' | 'final_result';
75
plan_id?: string;
86
session_id?: string;
97
data?: any;
@@ -12,40 +10,92 @@ export interface StreamMessage {
1210

1311
export interface StreamingPlanUpdate {
1412
plan_id: string;
15-
session_id: string;
13+
session_id?: string;
1614
step_id?: string;
1715
agent_name?: string;
1816
content?: string;
19-
status?: 'in_progress' | 'completed' | 'error';
20-
message_type?: 'thinking' | 'action' | 'result' | 'clarification_needed';
17+
status?: 'in_progress' | 'completed' | 'error' | 'creating_plan' | 'pending_approval';
18+
message_type?: 'thinking' | 'action' | 'result' | 'clarification_needed' | 'plan_approval_request';
19+
timestamp?: number;
20+
}
21+
22+
export interface PlanApprovalRequestData {
23+
plan_id: string;
24+
session_id: string;
25+
plan: {
26+
steps: Array<{
27+
id: string;
28+
description: string;
29+
agent: string;
30+
estimated_duration?: string;
31+
}>;
32+
total_steps: number;
33+
estimated_completion?: string;
34+
};
35+
status: 'PENDING_APPROVAL';
36+
}
37+
38+
export interface PlanApprovalResponseData {
39+
plan_id: string;
40+
session_id: string;
41+
approved: boolean;
42+
feedback?: string;
2143
}
2244

2345
class WebSocketService {
2446
private ws: WebSocket | null = null;
2547
private reconnectAttempts = 0;
2648
private maxReconnectAttempts = 5;
27-
private reconnectDelay = 1000;
49+
private reconnectDelay = 12000;
2850
private listeners: Map<string, Set<(message: StreamMessage) => void>> = new Map();
2951
private planSubscriptions: Set<string> = new Set();
52+
private reconnectTimer: NodeJS.Timeout | null = null;
53+
private isConnecting = false;
3054

3155
/**
3256
* Connect to WebSocket server
3357
*/
3458
connect(): Promise<void> {
3559
return new Promise((resolve, reject) => {
60+
if (this.isConnecting) {
61+
console.log('Connection attempt already in progress');
62+
resolve();
63+
return;
64+
}
65+
66+
if (this.reconnectTimer) {
67+
clearTimeout(this.reconnectTimer);
68+
this.reconnectTimer = null;
69+
}
70+
3671
try {
37-
// Get WebSocket URL from environment or default to localhost
72+
this.isConnecting = true;
73+
3874
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
3975
const wsHost = process.env.REACT_APP_WS_HOST || '127.0.0.1:8000';
40-
const wsUrl = `${wsProtocol}//${wsHost}/ws/streaming`;
76+
const processId = crypto.randomUUID();
77+
78+
const authHeaders = headerBuilder();
79+
const userId = authHeaders['x-ms-client-principal-id'];
80+
81+
if (!userId) {
82+
console.error('No user ID available for WebSocket connection');
83+
this.isConnecting = false;
84+
reject(new Error('Authentication required for WebSocket connection'));
85+
return;
86+
}
87+
88+
// Use query parameter for WebSocket authentication (as backend expects)
89+
const wsUrl = `${wsProtocol}//${wsHost}/api/v3/socket/${processId}?user_id=${encodeURIComponent(userId)}`;
4190

4291
console.log('Connecting to WebSocket:', wsUrl);
4392

4493
this.ws = new WebSocket(wsUrl);
45-
94+
4695
this.ws.onopen = () => {
47-
console.log('WebSocket connected');
96+
console.log('WebSocket connected successfully');
4897
this.reconnectAttempts = 0;
98+
this.isConnecting = false;
4999
this.emit('connection_status', { connected: true });
50100
resolve();
51101
};
@@ -55,23 +105,30 @@ class WebSocketService {
55105
const message: StreamMessage = JSON.parse(event.data);
56106
this.handleMessage(message);
57107
} catch (error) {
58-
console.error('Error parsing WebSocket message:', error);
108+
console.error('Error parsing WebSocket message:', error, 'Raw data:', event.data);
109+
this.emit('error', { error: 'Failed to parse WebSocket message' });
59110
}
60111
};
61112

62-
this.ws.onclose = () => {
63-
console.log('WebSocket disconnected');
113+
this.ws.onclose = (event) => {
114+
console.log('WebSocket disconnected', event.code, event.reason);
115+
this.isConnecting = false;
64116
this.emit('connection_status', { connected: false });
65-
this.attemptReconnect();
117+
118+
if (event.code !== 1000) {
119+
this.attemptReconnect();
120+
}
66121
};
67122

68123
this.ws.onerror = (error) => {
69124
console.error('WebSocket error:', error);
125+
this.isConnecting = false;
70126
this.emit('error', { error: 'WebSocket connection failed' });
71127
reject(error);
72128
};
73129

74130
} catch (error) {
131+
this.isConnecting = false;
75132
reject(error);
76133
}
77134
});
@@ -81,11 +138,21 @@ class WebSocketService {
81138
* Disconnect from WebSocket server
82139
*/
83140
disconnect(): void {
141+
console.log('Manually disconnecting WebSocket');
142+
143+
if (this.reconnectTimer) {
144+
clearTimeout(this.reconnectTimer);
145+
this.reconnectTimer = null;
146+
}
147+
148+
this.reconnectAttempts = this.maxReconnectAttempts;
149+
84150
if (this.ws) {
85-
this.ws.close();
151+
this.ws.close(1000, 'Manual disconnect');
86152
this.ws = null;
87153
}
88154
this.planSubscriptions.clear();
155+
this.isConnecting = false;
89156
}
90157

91158
/**
@@ -130,7 +197,6 @@ class WebSocketService {
130197

131198
this.listeners.get(eventType)!.add(callback);
132199

133-
// Return unsubscribe function
134200
return () => {
135201
const eventListeners = this.listeners.get(eventType);
136202
if (eventListeners) {
@@ -183,12 +249,14 @@ class WebSocketService {
183249
private handleMessage(message: StreamMessage): void {
184250
console.log('WebSocket message received:', message);
185251

186-
// Emit to specific event listeners
187252
if (message.type) {
188253
this.emit(message.type, message.data);
189254
}
190255

191-
// Emit to general message listeners
256+
if (message.type === 'plan_approval_request') {
257+
console.log('Plan approval request received via WebSocket:', message.data);
258+
}
259+
192260
this.emit('message', message);
193261
}
194262

@@ -197,20 +265,28 @@ class WebSocketService {
197265
*/
198266
private attemptReconnect(): void {
199267
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
200-
console.log('Max reconnection attempts reached');
268+
console.log('Max reconnection attempts reached - stopping reconnect attempts');
201269
this.emit('error', { error: 'Max reconnection attempts reached' });
202270
return;
203271
}
204272

273+
if (this.isConnecting || this.reconnectTimer) {
274+
console.log('Reconnection attempt already in progress');
275+
return;
276+
}
277+
205278
this.reconnectAttempts++;
206279
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
207280

208-
console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
281+
console.log(`Scheduling reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay/1000}s`);
209282

210-
setTimeout(() => {
283+
this.reconnectTimer = setTimeout(() => {
284+
this.reconnectTimer = null;
285+
console.log(`Attempting reconnection (attempt ${this.reconnectAttempts})`);
286+
211287
this.connect()
212288
.then(() => {
213-
// Re-subscribe to all plans
289+
console.log('Reconnection successful - re-subscribing to plans');
214290
this.planSubscriptions.forEach(planId => {
215291
this.subscribeToPlan(planId);
216292
});
@@ -238,8 +314,30 @@ class WebSocketService {
238314
console.warn('WebSocket is not connected. Cannot send message:', message);
239315
}
240316
}
317+
318+
/**
319+
* Send plan approval response
320+
*/
321+
sendPlanApprovalResponse(response: PlanApprovalResponseData): void {
322+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
323+
console.error('WebSocket not connected - cannot send plan approval response');
324+
this.emit('error', { error: 'Cannot send plan approval response - WebSocket not connected' });
325+
return;
326+
}
327+
328+
try {
329+
const message = {
330+
type: 'plan_approval_response',
331+
data: response
332+
};
333+
this.ws.send(JSON.stringify(message));
334+
console.log('Plan approval response sent:', response);
335+
} catch (error) {
336+
console.error('Failed to send plan approval response:', error);
337+
this.emit('error', { error: 'Failed to send plan approval response' });
338+
}
339+
}
241340
}
242341

243-
// Export singleton instance
244342
export const webSocketService = new WebSocketService();
245343
export default webSocketService;

0 commit comments

Comments
 (0)