Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/App/src/components/common/TimeoutDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import {
Dialog,
DialogSurface,
DialogTitle,
DialogContent,
DialogBody,
DialogActions,
Button,
} from '@fluentui/react-components';
import { Clock20Regular } from '@fluentui/react-icons';
import "../../styles/Panel.css";

interface TimeoutDialogProps {
isOpen: boolean;
message: string;
onGoHome: () => void;
}

/**
* Dialog shown when the backend sends a timeout notification
* (e.g., plan approval timed out).
*/
const TimeoutDialog: React.FC<TimeoutDialogProps> = ({ isOpen, message, onGoHome }) => (
<Dialog open={isOpen} modalType="alert">
<DialogSurface>
<DialogBody>
<DialogTitle>
<div className="plan-cancellation-dialog-title">
<Clock20Regular className="plan-cancellation-warning-icon" />
Session Timed Out
</div>
</DialogTitle>
<DialogContent>
{message}
</DialogContent>
<DialogActions>
<Button appearance="primary" onClick={onGoHome}>
Go to Home
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
);

export default TimeoutDialog;
20 changes: 20 additions & 0 deletions src/App/src/hooks/usePlanWebSocket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import { PlanDataService } from '@/store/PlanDataService';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import {
setShowProcessingPlanSpinner,
setShowApprovalButtons,
setReloadLeftList,
selectPlanData,
selectContinueWithWebsocketFlow,
selectPlanApproved,
selectShowProcessingPlanSpinner,
setShowTimeoutDialog,
setTimeoutMessage,
approvalRequestReceived,
planCompletedFinal,
planFailedFinal,
Expand Down Expand Up @@ -337,6 +340,23 @@ export function usePlanWebSocket({
return unsub;
}, [dispatch, scrollToBottom, showToast, formatErrorMessage]);

// ── TIMEOUT_NOTIFICATION ──────────────────────────────────────
useEffect(() => {
const unsub = webSocketService.on(
WebsocketMessageType.TIMEOUT_NOTIFICATION,
(msg: any) => {
const message = msg?.data?.message || msg?.message ||
'Session timed out. Please go back to home and try again.';
dispatch(setTimeoutMessage(message));
dispatch(setShowTimeoutDialog(true));
dispatch(setShowProcessingPlanSpinner(false));
dispatch(setShowApprovalButtons(false));
webSocketService.disconnect();
},
);
return unsub;
}, [dispatch]);

// ── AGENT_MESSAGE ─────────────────────────────────────────────
useEffect(() => {
const unsub = webSocketService.on(
Expand Down
1 change: 1 addition & 0 deletions src/App/src/models/enums.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export enum WebsocketMessageType {
USER_CLARIFICATION_REQUEST = "user_clarification_request",
USER_CLARIFICATION_RESPONSE = "user_clarification_response",
FINAL_RESULT_MESSAGE = "final_result_message",
TIMEOUT_NOTIFICATION = "timeout_notification",
ERROR_MESSAGE = 'error_message',
PING = "ping"
}
Expand Down
15 changes: 15 additions & 0 deletions src/App/src/pages/PlanPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
selectLoadingMessage,
selectReloadLeftList,
selectWaitingForPlan,
selectShowTimeoutDialog,
selectTimeoutMessage,
setReloadLeftList,
setProcessingApproval,
setShowProcessingPlanSpinner,
Expand Down Expand Up @@ -73,6 +75,7 @@ import { useInlineToaster } from '../components/toast/InlineToaster';
import Octo from '../commonComponents/imports/Octopus.png';
import LoadingMessage, { loadingMessages } from '../commonComponents/components/LoadingMessage';
import PlanCancellationDialog from '../components/common/PlanCancellationDialog';
import TimeoutDialog from '../components/common/TimeoutDialog';
import '../styles/PlanPage.css';

// Singleton API service
Expand Down Expand Up @@ -135,6 +138,8 @@ const PlanPage: React.FC = () => {
const showBufferingText = useAppSelector(selectShowBufferingText);
const wsConnected = useAppSelector(selectWsConnected);
const selectedTeam = useAppSelector(selectSelectedTeam);
const showTimeoutDialog = useAppSelector(selectShowTimeoutDialog);
const timeoutMessage = useAppSelector(selectTimeoutMessage);

/* ── Cancellation alert hook ────────────────────────────── */
const [pendingNavigation, setPendingNavigation] = React.useState<(() => void) | null>(null);
Expand Down Expand Up @@ -203,6 +208,10 @@ const PlanPage: React.FC = () => {
setPendingNavigation(null);
}, [dispatch]);

const handleTimeoutGoHome = useCallback(() => {
navigate('/');
}, [navigate]);

/* ── Plan Approval / Rejection ──────────────────────────── */
const handleApprovePlan = useCallback(async () => {
if (!planApprovalRequest) return;
Expand Down Expand Up @@ -432,6 +441,12 @@ const PlanPage: React.FC = () => {
onCancel={handleCancelDialog}
loading={cancellingPlan}
/>

<TimeoutDialog
isOpen={showTimeoutDialog}
message={timeoutMessage}
onGoHome={handleTimeoutGoHome}
/>
</CoralShellColumn>
);
};
Expand Down
4 changes: 4 additions & 0 deletions src/App/src/store/WebSocketService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,10 @@ class WebSocketService {
// Server keepalive heartbeat — ignore.
break;
}
case WebsocketMessageType.TIMEOUT_NOTIFICATION: {
this.emit(WebsocketMessageType.TIMEOUT_NOTIFICATION, message);
break;
}
case WebsocketMessageType.ERROR_MESSAGE: {
this.emit(WebsocketMessageType.ERROR_MESSAGE, message.data); // Emit the data
break;
Expand Down
16 changes: 16 additions & 0 deletions src/App/src/store/slices/planSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export interface PlanState {
cancellingPlan: boolean;
/** Loading message for spinners */
loadingMessage: string;
/** Show timeout dialog when backend sends timeout notification */
showTimeoutDialog: boolean;
/** Timeout message from backend */
timeoutMessage: string;
}

const initialState: PlanState = {
Expand All @@ -75,6 +79,8 @@ const initialState: PlanState = {
showCancellationDialog: false,
cancellingPlan: false,
loadingMessage: '',
showTimeoutDialog: false,
timeoutMessage: '',
};

const planSlice = createSlice({
Expand Down Expand Up @@ -123,6 +129,12 @@ const planSlice = createSlice({
setLoadingMessage(state, action: PayloadAction<string>) {
state.loadingMessage = action.payload;
},
setShowTimeoutDialog(state, action: PayloadAction<boolean>) {
state.showTimeoutDialog = action.payload;
},
setTimeoutMessage(state, action: PayloadAction<string>) {
state.timeoutMessage = action.payload;
},
/** Mark plan completed and update local state in one dispatch */
markPlanCompleted(state) {
if (state.planData?.plan) {
Expand Down Expand Up @@ -232,6 +244,8 @@ export const {
setShowCancellationDialog,
setCancellingPlan,
setLoadingMessage,
setShowTimeoutDialog,
setTimeoutMessage,
markPlanCompleted,
planApprovalAccepted,
planApprovalRejected,
Expand All @@ -255,6 +269,8 @@ export const selectReloadLeftList = (s: RootState) => s.plan.reloadLeftList;
export const selectShowCancellationDialog = (s: RootState) => s.plan.showCancellationDialog;
export const selectCancellingPlan = (s: RootState) => s.plan.cancellingPlan;
export const selectLoadingMessage = (s: RootState) => s.plan.loadingMessage;
export const selectShowTimeoutDialog = (s: RootState) => s.plan.showTimeoutDialog;
export const selectTimeoutMessage = (s: RootState) => s.plan.timeoutMessage;
export const selectPlanStatus = (s: RootState) => s.plan.planData?.plan?.overall_status ?? null;
export const selectPlanApproved = (s: RootState) => s.plan.planApproved;

Expand Down
Loading