Skip to content
Open
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
51 changes: 51 additions & 0 deletions src/App/src/components/common/ProcessingStatusIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Spinner } from '@fluentui/react-components';
import { formatElapsedTime } from '@/utils';

interface ProcessingStatusIndicatorProps {
message: string;
elapsedSeconds?: number;
}

const ProcessingStatusIndicator = ({
message,
elapsedSeconds,
}: ProcessingStatusIndicatorProps) => {
const showElapsedSuffix = typeof elapsedSeconds === 'number' && elapsedSeconds > 0;
const elapsedSuffix = showElapsedSuffix ? ` (${formatElapsedTime(elapsedSeconds)})` : '';

return (
<div
style={{
maxWidth: '800px',
margin: '0 auto 32px auto',
padding: '0 24px',
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
backgroundColor: 'var(--colorNeutralBackground2)',
borderRadius: '8px',
border: '1px solid var(--colorNeutralStroke1)',
padding: '16px',
}}
>
<Spinner size="small" />
<span
style={{
fontSize: '14px',
color: 'var(--colorNeutralForeground1)',
fontWeight: '500',
}}
>
{message}
{elapsedSuffix}
</span>
</div>
</div>
);
};

export default ProcessingStatusIndicator;
8 changes: 5 additions & 3 deletions src/App/src/components/content/PlanChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ interface SimplifiedPlanChatProps extends PlanChatProps {
showBufferingText: boolean;
agentMessages: AgentMessageData[];
showProcessingPlanSpinner: boolean;
processingElapsedSeconds: number;
processingStatusMessage: string;
showApprovalButtons: boolean;
handleApprovePlan: () => Promise<void>;
handleRejectPlan: () => Promise<void>;
Expand All @@ -45,13 +47,13 @@ const PlanChat: React.FC<SimplifiedPlanChatProps> = ({
showBufferingText,
agentMessages,
showProcessingPlanSpinner,
processingElapsedSeconds,
processingStatusMessage,
showApprovalButtons,
handleApprovePlan,
handleRejectPlan,
processingApproval
}) => {
// States

if (!planData)
return (
<ContentNotFound subtitle="The requested page could not be found." />
Expand Down Expand Up @@ -86,7 +88,7 @@ const PlanChat: React.FC<SimplifiedPlanChatProps> = ({
{renderPlanResponse(planApprovalRequest, handleApprovePlan, handleRejectPlan, processingApproval, showApprovalButtons)}
{renderAgentMessages(agentMessages, undefined, undefined, finalResultRef)}

{showProcessingPlanSpinner && renderPlanExecutionMessage()}
{showProcessingPlanSpinner && renderPlanExecutionMessage(processingElapsedSeconds, processingStatusMessage)}
{/* Streaming plan updates — hidden while an approval prompt is pending so
the approval action is presented at the appropriate step instead of
after the thinking process visibly completes. */}
Expand Down
34 changes: 9 additions & 25 deletions src/App/src/components/content/streaming/StreamingPlanState.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Spinner } from "@fluentui/react-components";
import ProcessingStatusIndicator from "../../common/ProcessingStatusIndicator.tsx";

// Simple thinking message to show while creating plan
const renderThinkingState = (waitingForPlan: boolean) => {
Expand Down Expand Up @@ -54,32 +55,15 @@ const renderThinkingState = (waitingForPlan: boolean) => {
};

// Simple message to show while executing the plan
const renderPlanExecutionMessage = () => {
const renderPlanExecutionMessage = (
processingElapsedSeconds?: number,
processingStatusMessage = 'Processing your plan and coordinating with AI agents...',
) => {
return (
<div style={{
maxWidth: '800px',
margin: '0 auto 32px auto',
padding: '0 24px'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
backgroundColor: 'var(--colorNeutralBackground2)',
borderRadius: '8px',
border: '1px solid var(--colorNeutralStroke1)',
padding: '16px'
}}>
<Spinner size="small" />
<span style={{
fontSize: '14px',
color: 'var(--colorNeutralForeground1)',
fontWeight: '500'
}}>
Processing your plan and coordinating with AI agents...
</span>
</div>
</div>
<ProcessingStatusIndicator
message={processingStatusMessage}
elapsedSeconds={processingElapsedSeconds}
/>
);
};

Expand Down
28 changes: 25 additions & 3 deletions src/App/src/hooks/usePlanWebSocket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
selectPlanData,
selectContinueWithWebsocketFlow,
selectPlanApproved,
selectShowProcessingPlanSpinner,
approvalRequestReceived,
planCompletedFinal,
planFailedFinal,
Expand Down Expand Up @@ -44,11 +45,11 @@ import {
ProcessedPlanData,
} from '@/models';
import { APIService } from '@/api/apiService';
import { ToastIntent } from '@/components/toast/InlineToaster';
import { formatElapsedTime } from '@/utils';

const apiService = new APIService();

import { ToastIntent } from '@/components/toast/InlineToaster';

interface UsePlanWebSocketProps {
planId: string | undefined;
scrollToBottom: () => void;
Expand Down Expand Up @@ -99,8 +100,20 @@ export function usePlanWebSocket({
const dispatch = useAppDispatch();
const planData = useAppSelector(selectPlanData);
const planApproved = useAppSelector(selectPlanApproved);
const showProcessingPlanSpinner = useAppSelector(selectShowProcessingPlanSpinner);
const continueWithWebsocketFlow = useAppSelector(selectContinueWithWebsocketFlow);
const streamingMessageBuffer = useAppSelector(selectStreamingMessageBuffer);
const processingStartedAtRef = React.useRef<number | null>(null);

useEffect(() => {
if (showProcessingPlanSpinner) {
if (processingStartedAtRef.current === null) {
processingStartedAtRef.current = Date.now();
}
} else {
processingStartedAtRef.current = null;
}
}, [showProcessingPlanSpinner]);
Comment thread
Tejasri-Microsoft marked this conversation as resolved.

// ── PLAN_APPROVAL_REQUEST ─────────────────────────────────────
useEffect(() => {
Expand Down Expand Up @@ -161,6 +174,7 @@ export function usePlanWebSocket({
dispatch(addAgentMessage(agentMessageData));
dispatch(setShowBufferingText(false));
dispatch(setShowProcessingPlanSpinner(false));
processingStartedAtRef.current = null;
dispatch(setSubmittingChatDisableInput(false));
scrollToBottom();
persistAgentMessage(agentMessageData, planData, dispatch);
Expand All @@ -181,6 +195,12 @@ export function usePlanWebSocket({
WebsocketMessageType.FINAL_RESULT_MESSAGE,
(finalMessage: any) => {
if (!finalMessage) return;
const completionElapsedSeconds = processingStartedAtRef.current
? Math.max(Math.round((Date.now() - processingStartedAtRef.current) / 1000), 0)
: null;
const completionTimeLine = completionElapsedSeconds !== null
? `\n\n**Total completion time: ${formatElapsedTime(completionElapsedSeconds)}**`
: '';
const messageStatus = finalMessage?.data?.status;

if (messageStatus === PlanStatus.COMPLETED) {
Expand All @@ -190,14 +210,15 @@ export function usePlanWebSocket({
timestamp: Date.now(),
steps: [],
next_steps: [],
content: finalMessage.data?.content || '',
content: (finalMessage.data?.content || '') + completionTimeLine,
raw_data: finalMessage,
};
dispatch(setShowBufferingText(true));
dispatch(addAgentMessage(agentMessageData));
dispatch(setSelectedTeam(planData?.team || null));
/* P0: single compound action replaces setShowProcessingPlanSpinner(false) + markPlanCompleted() */
dispatch(planCompletedFinal());
processingStartedAtRef.current = null;
scrollToFinalResult();
webSocketService.disconnect();
persistAgentMessage(agentMessageData, planData, dispatch, true, streamingMessageBuffer);
Expand Down Expand Up @@ -256,6 +277,7 @@ export function usePlanWebSocket({
};
dispatch(addAgentMessage(errorAgent));
dispatch(planFailedFinal());
processingStartedAtRef.current = null;
Comment thread
Tejasri-Microsoft marked this conversation as resolved.
dispatch(setShowBufferingText(false));
dispatch(setSubmittingChatDisableInput(true));
scrollToBottom();
Expand Down
43 changes: 43 additions & 0 deletions src/App/src/pages/PlanPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,30 @@ import '../styles/PlanPage.css';
// Singleton API service
const apiService = new APIService();

const getPlanProcessingStatusMessage = (elapsedSeconds: number): string => {
if (elapsedSeconds < 8) {
return 'Processing your plan and coordinating with AI agents...';
}

if (elapsedSeconds < 20) {
return 'Assigning tasks to specialized agents...';
}

if (elapsedSeconds < 35) {
return 'Agents are analyzing and researching...';
}

if (elapsedSeconds < 50) {
return 'Compiling results from agents...';
}

if (elapsedSeconds < 90) {
return 'Finalizing responses...';
}

return 'Still processing, please wait...';
};

/* ================================================================
* PlanPage — refactored to use Redux + extracted hooks
* ================================================================ */
Expand Down Expand Up @@ -114,6 +138,8 @@ const PlanPage: React.FC = () => {

/* ── Cancellation alert hook ────────────────────────────── */
const [pendingNavigation, setPendingNavigation] = React.useState<(() => void) | null>(null);
const [processingElapsedSeconds, setProcessingElapsedSeconds] = React.useState<number>(0);
const processingStatusMessage = getPlanProcessingStatusMessage(processingElapsedSeconds);

const { isPlanActive } = usePlanCancellationAlert({
planData,
Expand Down Expand Up @@ -288,6 +314,21 @@ const PlanPage: React.FC = () => {
return () => clearInterval(interval);
}, [loading, dispatch]);

/* ── Plan execution elapsed timer ───────────────────────── */
useEffect(() => {
if (!showProcessingPlanSpinner) {
setProcessingElapsedSeconds(0);
return;
}

setProcessingElapsedSeconds(0);
const interval = setInterval(() => {
setProcessingElapsedSeconds((currentSeconds: number) => currentSeconds + 1);
}, 1000);

return () => clearInterval(interval);
}, [showProcessingPlanSpinner]);

/* ── Initial plan load ──────────────────────────────────── */
useEffect(() => {
if (!planId) {
Expand Down Expand Up @@ -367,6 +408,8 @@ const PlanPage: React.FC = () => {
showBufferingText={showBufferingText}
agentMessages={agentMessages}
showProcessingPlanSpinner={showProcessingPlanSpinner}
processingElapsedSeconds={processingElapsedSeconds}
processingStatusMessage={processingStatusMessage}
showApprovalButtons={showApprovalButtons}
processingApproval={processingApproval}
handleApprovePlan={handleApprovePlan}
Expand Down
2 changes: 1 addition & 1 deletion src/App/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* - agentIconUtils → agent-to-icon mapping
*/

export { formatDate } from './utils';
export { formatDate, formatElapsedTime } from './utils';
export { getErrorMessage, getErrorStyle } from './errorUtils';
export { formatErrorMessage, extractPlainAnswer, truncate } from './messageUtils';
export {
Expand Down
25 changes: 24 additions & 1 deletion src/App/src/utils/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,27 @@ export const formatDate = (
});

return formatted;
}
}

/**
* Formats an elapsed-time duration in seconds for display in processing
* indicators and completion messages.
*
* Examples:
* - 5 → "5s"
* - 59 → "59s"
* - 60 → "1min 0sec"
* - 75 → "1min 15sec"
*
* @param elapsedSeconds Non-negative integer seconds elapsed.
* @returns Human-readable elapsed-time string.
*/
export const formatElapsedTime = (elapsedSeconds: number): string => {
if (elapsedSeconds < 60) {
return `${elapsedSeconds}s`;
}

const minutes = Math.floor(elapsedSeconds / 60);
const seconds = elapsedSeconds % 60;
return `${minutes}min ${seconds}sec`;
};