From 9ba1e7e46e619a1406a839da99577b37e23ded1a Mon Sep 17 00:00:00 2001 From: Patrick Chin <8509935+thepatrickchin@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:11:16 +0800 Subject: [PATCH 1/5] feat: add option to open OAuth login on same tab Signed-off-by: Patrick Chin <8509935+thepatrickchin@users.noreply.github.com> --- components/Chat/Chat.tsx | 39 +++++++++++++++++++++++++-------------- types/websocket.ts | 1 + 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index f40d07e..9715cc9 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -207,6 +207,7 @@ export const Chat = () => { expandIntermediateSteps, intermediateStepOverride, enableIntermediateSteps, + useOAuthPopup, }, handleUpdateConversation, dispatch: homeDispatch, @@ -493,7 +494,7 @@ export const Chat = () => { }, [intermediateStepOverride]); /** - * Handles OAuth consent flow by opening popup window + * Handles OAuth consent flow by opening a popup window or navigating in the same tab */ const handleOAuthConsent = (message: WebSocketInbound) => { if (!isSystemInteractionMessage(message)) return false; @@ -507,17 +508,22 @@ export const Chat = () => { toast.error('OAuth URL validation failed.'); return false; } - - const popup = window.open( - oauthUrl, - 'oauth-popup', - 'width=600,height=700,scrollbars=yes,resizable=yes,noopener,noreferrer' - ); - const handleOAuthComplete = (event: MessageEvent) => { - if (popup && !popup.closed) popup.close(); - window.removeEventListener('message', handleOAuthComplete); - }; - window.addEventListener('message', handleOAuthComplete); + + const shouldUsePopup = message.content?.use_popup !== undefined ? message.content.use_popup : useOAuthPopup !== false; + if (shouldUsePopup) { + const popup = window.open( + oauthUrl, + 'oauth-popup', + 'width=600,height=700,scrollbars=yes,resizable=yes,noopener,noreferrer' + ); + const handleOAuthComplete = (event: MessageEvent) => { + if (popup && !popup.closed) popup.close(); + window.removeEventListener('message', handleOAuthComplete); + }; + window.addEventListener('message', handleOAuthComplete); + } else { + window.location.href = oauthUrl; + } } return true; } @@ -748,8 +754,13 @@ export const Chat = () => { if (oauthUrl) { // Validate URL before opening to prevent Open Redirect attacks if (isValidConsentPromptURL(oauthUrl)) { - // Open the validated OAuth URL in a new tab - window.open(oauthUrl, '_blank', 'noopener,noreferrer'); + const shouldUsePopup = message?.content?.use_popup !== undefined ? message.content.use_popup : useOAuthPopup !== false; + if (shouldUsePopup) { + // Open the validated OAuth URL in a new tab + window.open(oauthUrl, '_blank', 'noopener,noreferrer'); + } else { + window.location.href = oauthUrl; + } } else { console.error('OAuth URL validation failed, refusing to open potentially malicious URL:', oauthUrl); toast.error('Invalid OAuth URL received. Please contact support.'); diff --git a/types/websocket.ts b/types/websocket.ts index 526e1d9..c343c7c 100644 --- a/types/websocket.ts +++ b/types/websocket.ts @@ -44,6 +44,7 @@ export interface SystemInteractionMessage extends WebSocketMessageBase { text?: string; timeout?: number | null; error?: string | null; + use_popup?: boolean; }; thread_id?: string; } From 2805e6b733d3163333677d0043e010a013f43bc1 Mon Sep 17 00:00:00 2001 From: Patrick Chin <8509935+thepatrickchin@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:11:16 +0800 Subject: [PATCH 2/5] feat: reconnect WS and resubmit message after returning from login Signed-off-by: Patrick Chin <8509935+thepatrickchin@users.noreply.github.com> --- components/Chat/Chat.tsx | 42 ++++++++++++++++++++++++++++++++++++++-- pages/api/home/home.tsx | 5 +++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index 9715cc9..560db51 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -493,6 +493,15 @@ export const Chat = () => { } }, [intermediateStepOverride]); + const persistOAuthPendingMessage = () => { + const conversation = selectedConversationRef.current; + if (!conversation) return; + const lastUserMessage = fetchLastMessage({ messages: conversation.messages, role: 'user' }); + if (!lastUserMessage) return; + sessionStorage.setItem('oauth_pending_message', JSON.stringify(lastUserMessage)); + sessionStorage.setItem('oauth_pending_conversation_id', conversation.id); + }; + /** * Handles OAuth consent flow by opening a popup window or navigating in the same tab */ @@ -509,7 +518,7 @@ export const Chat = () => { return false; } - const shouldUsePopup = message.content?.use_popup !== undefined ? message.content.use_popup : useOAuthPopup !== false; + const shouldUsePopup = message.content?.use_popup !== false; if (shouldUsePopup) { const popup = window.open( oauthUrl, @@ -522,6 +531,7 @@ export const Chat = () => { }; window.addEventListener('message', handleOAuthComplete); } else { + persistOAuthPendingMessage(); window.location.href = oauthUrl; } } @@ -754,11 +764,12 @@ export const Chat = () => { if (oauthUrl) { // Validate URL before opening to prevent Open Redirect attacks if (isValidConsentPromptURL(oauthUrl)) { - const shouldUsePopup = message?.content?.use_popup !== undefined ? message.content.use_popup : useOAuthPopup !== false; + const shouldUsePopup = message?.content?.use_popup !== false; if (shouldUsePopup) { // Open the validated OAuth URL in a new tab window.open(oauthUrl, '_blank', 'noopener,noreferrer'); } else { + persistOAuthPendingMessage(); window.location.href = oauthUrl; } } else { @@ -1445,6 +1456,33 @@ export const Chat = () => { handleSend(editedMessage, deleteCount || 0); }, [handleSend]); + // After returning from the OAuth provider, resubmit the message that triggered auth. + useEffect(() => { + const pendingMessageRaw = sessionStorage.getItem('oauth_pending_message'); + const pendingConversationId = sessionStorage.getItem('oauth_pending_conversation_id'); + if (!pendingMessageRaw || !pendingConversationId) return; + if (!selectedConversation || selectedConversation.id !== pendingConversationId) return; + + sessionStorage.removeItem('oauth_pending_message'); + sessionStorage.removeItem('oauth_pending_conversation_id'); + + const resume = async () => { + let pendingMessage: Message; + try { + pendingMessage = JSON.parse(pendingMessageRaw); + } catch { + return; + } + // Ensure the WebSocket is connected before calling handleSend + if (webSocketModeRef.current && !webSocketConnectedRef.current) { + await connectWebSocket(); + } + // Delete the user message + empty assistant placeholder appended during original send, then resubmit. + handleSend(pendingMessage, 2); + }; + resume(); + }, [selectedConversation?.id]); + // Add a new effect to handle streaming state changes useEffect(() => { if (messageIsStreaming) { diff --git a/pages/api/home/home.tsx b/pages/api/home/home.tsx index 420a6f3..071e1b7 100644 --- a/pages/api/home/home.tsx +++ b/pages/api/home/home.tsx @@ -215,6 +215,11 @@ const Home = (props: any) => { dispatch({ field: 'showChatbar', value: showChatbar === 'true' }); } + const webSocketMode = sessionStorage.getItem('webSocketMode'); + if (webSocketMode !== null) { + dispatch({ field: 'webSocketMode', value: webSocketMode === 'true' }); + } + const enableIntermediateSteps = sessionStorage.getItem('enableIntermediateSteps'); if (enableIntermediateSteps !== null) { dispatch({ field: 'enableIntermediateSteps', value: enableIntermediateSteps === 'true' }); From c5c4984c62c42a15409ef2c6080a22dcdc066203 Mon Sep 17 00:00:00 2001 From: Patrick Chin <8509935+thepatrickchin@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:47:06 +0800 Subject: [PATCH 3/5] fix: display auth cancellation message when user clicks back Signed-off-by: Patrick Chin <8509935+thepatrickchin@users.noreply.github.com> --- components/Chat/Chat.tsx | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index 560db51..d559b5f 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -207,7 +207,6 @@ export const Chat = () => { expandIntermediateSteps, intermediateStepOverride, enableIntermediateSteps, - useOAuthPopup, }, handleUpdateConversation, dispatch: homeDispatch, @@ -1463,9 +1462,39 @@ export const Chat = () => { if (!pendingMessageRaw || !pendingConversationId) return; if (!selectedConversation || selectedConversation.id !== pendingConversationId) return; + // The success page runs at the NAT server origin, so sessionStorage is cross-origin and + // unavailable here. The flag is instead passed back as a URL query parameter. + const urlParams = new URLSearchParams(window.location.search); + const authCompleted = urlParams.get('oauth_auth_completed'); + if (authCompleted) { + urlParams.delete('oauth_auth_completed'); + const cleanUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : ''); + window.history.replaceState({}, '', cleanUrl); + } + sessionStorage.removeItem('oauth_pending_message'); sessionStorage.removeItem('oauth_pending_conversation_id'); + // If the user pressed back without completing OAuth, show a cancellation message. + if (!authCompleted) { + const conversation = selectedConversationRef.current; + if (conversation) { + const messages = conversation.messages; + const lastMessage = messages.at(-1); + const updatedMessages = lastMessage?.role === 'assistant' + ? messages.map((m, idx) => + idx === messages.length - 1 ? updateAssistantMessage(m, 'Authorization cancelled.') : m + ) + : [...messages, createAssistantMessage(undefined, undefined, 'Authorization cancelled.')]; + const updatedConversation = { ...conversation, messages: updatedMessages }; + const updatedConversations = conversationsRef.current.map(c => + c.id === updatedConversation.id ? updatedConversation : c + ); + updateRefsAndDispatch(updatedConversations, updatedConversation, conversation); + } + return; + } + const resume = async () => { let pendingMessage: Message; try { From 31f26b4af5140e583bfa967d86a944f6131fe14c Mon Sep 17 00:00:00 2001 From: Patrick Chin <8509935+thepatrickchin@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:27:12 +0800 Subject: [PATCH 4/5] feat: display cancellation reason Signed-off-by: Patrick Chin <8509935+thepatrickchin@users.noreply.github.com> --- components/Chat/Chat.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index d559b5f..1d34a8e 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -229,6 +229,7 @@ export const Chat = () => { const [interactionMessage, setInteractionMessage] = useState(null); const webSocketRef = useRef(null); const webSocketConnectedRef = useRef(false); + const oauthPopupCancelledRef = useRef(false); const webSocketModeRef = useRef( sessionStorage.getItem('webSocketMode') === 'false' ? false : webSocketMode ); @@ -522,7 +523,7 @@ export const Chat = () => { const popup = window.open( oauthUrl, 'oauth-popup', - 'width=600,height=700,scrollbars=yes,resizable=yes,noopener,noreferrer' + 'noopener,noreferrer' ); const handleOAuthComplete = (event: MessageEvent) => { if (popup && !popup.closed) popup.close(); @@ -765,8 +766,17 @@ export const Chat = () => { if (isValidConsentPromptURL(oauthUrl)) { const shouldUsePopup = message?.content?.use_popup !== false; if (shouldUsePopup) { + if (oauthPopupCancelledRef.current) return; // Open the validated OAuth URL in a new tab - window.open(oauthUrl, '_blank', 'noopener,noreferrer'); + const popup = window.open(oauthUrl, 'oauth-popup', 'noopener,noreferrer'); + const handleOAuthComplete = (event: MessageEvent) => { + if (popup && !popup.closed) popup.close(); + window.removeEventListener('message', handleOAuthComplete); + if (event.data?.type === 'AUTH_CANCELLED') { + oauthPopupCancelledRef.current = true; + } + }; + window.addEventListener('message', handleOAuthComplete); } else { persistOAuthPendingMessage(); window.location.href = oauthUrl; @@ -852,6 +862,7 @@ export const Chat = () => { const handleSend = useCallback( async (message: Message, deleteCount = 0, retry = false) => { message.id = uuidv4(); + oauthPopupCancelledRef.current = false; // Set the active user message ID for WebSocket message tracking activeUserMessageId.current = message.id; From db82f96b3301dcca896d6df1f2432bb3eb7c0a28 Mon Sep 17 00:00:00 2001 From: Patrick Chin <8509935+thepatrickchin@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:07:41 +0800 Subject: [PATCH 5/5] refactor: rename use_popup_auth to use_redirect_auth and negate logic Signed-off-by: Patrick Chin <8509935+thepatrickchin@users.noreply.github.com> --- components/Chat/Chat.tsx | 4 ++-- types/websocket.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index 1d34a8e..3f2e293 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -518,7 +518,7 @@ export const Chat = () => { return false; } - const shouldUsePopup = message.content?.use_popup !== false; + const shouldUsePopup = !message.content?.use_redirect; if (shouldUsePopup) { const popup = window.open( oauthUrl, @@ -764,7 +764,7 @@ export const Chat = () => { if (oauthUrl) { // Validate URL before opening to prevent Open Redirect attacks if (isValidConsentPromptURL(oauthUrl)) { - const shouldUsePopup = message?.content?.use_popup !== false; + const shouldUsePopup = !message?.content?.use_redirect; if (shouldUsePopup) { if (oauthPopupCancelledRef.current) return; // Open the validated OAuth URL in a new tab diff --git a/types/websocket.ts b/types/websocket.ts index c343c7c..7d65266 100644 --- a/types/websocket.ts +++ b/types/websocket.ts @@ -44,7 +44,7 @@ export interface SystemInteractionMessage extends WebSocketMessageBase { text?: string; timeout?: number | null; error?: string | null; - use_popup?: boolean; + use_redirect?: boolean; }; thread_id?: string; }