diff --git a/src/rovo-dev/rovoDevChatProvider.ts b/src/rovo-dev/rovoDevChatProvider.ts index 7aca5d75e..7b3aa6d96 100644 --- a/src/rovo-dev/rovoDevChatProvider.ts +++ b/src/rovo-dev/rovoDevChatProvider.ts @@ -245,6 +245,13 @@ export class RovoDevChatProvider { } this._pendingCancellation = true; + // Optimistically unlock the UI immediately + await webview.postMessage({ + type: RovoDevProviderMessageType.CompleteMessage, + promptId: this._currentPromptId, + isCancellation: true, + }); + try { const cancelResponse = await this._rovoDevApiClient.cancel(); success = cancelResponse.cancelled || cancelResponse.message === 'No chat in progress'; @@ -396,6 +403,7 @@ export class RovoDevChatProvider { await webview.postMessage({ type: RovoDevProviderMessageType.RovoDevResponseMessage, message: group, + promptId: this._currentPromptId, }); group = []; } @@ -460,6 +468,7 @@ export class RovoDevChatProvider { await webview.postMessage({ type: RovoDevProviderMessageType.RovoDevResponseMessage, message: response, + promptId: this._currentPromptId, }); break; @@ -468,6 +477,7 @@ export class RovoDevChatProvider { await webview.postMessage({ type: RovoDevProviderMessageType.RovoDevResponseMessage, message: response, + promptId: this._currentPromptId, }); } break; @@ -763,6 +773,7 @@ export class RovoDevChatProvider { text, enable_deep_plan, context, + promptId: this._currentPromptId, }); } diff --git a/src/rovo-dev/rovoDevWebviewProviderMessages.ts b/src/rovo-dev/rovoDevWebviewProviderMessages.ts index b62a208e3..d3720aeee 100644 --- a/src/rovo-dev/rovoDevWebviewProviderMessages.ts +++ b/src/rovo-dev/rovoDevWebviewProviderMessages.ts @@ -71,12 +71,15 @@ export type RovoDevProviderMessage = RovoDevProviderMessageType.RovoDevDisabled, { reason: RovoDevDisabledReason; detail?: RovoDevEntitlementCheckFailedDetail } > - | ReducerAction + | ReducerAction< + RovoDevProviderMessageType.SignalPromptSent, + RovoDevPrompt & { echoMessage: boolean; promptId?: string } + > | ReducerAction< RovoDevProviderMessageType.RovoDevResponseMessage, - { message: RovoDevResponseMessageType | RovoDevResponseMessageType[] } + { message: RovoDevResponseMessageType | RovoDevResponseMessageType[]; promptId?: string } > - | ReducerAction + | ReducerAction | ReducerAction | ReducerAction | ReducerAction< diff --git a/src/rovo-dev/ui/rovoDevView.tsx b/src/rovo-dev/ui/rovoDevView.tsx index c3b7a5a95..84a4a4ea8 100644 --- a/src/rovo-dev/ui/rovoDevView.tsx +++ b/src/rovo-dev/ui/rovoDevView.tsx @@ -76,6 +76,8 @@ const RovoDevView: React.FC = () => { const [pendingFilesForFiltering, setPendingFilesForFiltering] = useState(null); const [thinkingBlockEnabled, setThinkingBlockEnabled] = useState(true); const [lastCompletedPromptId, setLastCompletedPromptId] = useState(undefined); + const [activePromptId, setActivePromptId] = useState(undefined); + const [queuedPrompt, setQueuedPrompt] = useState(undefined); const [isAtlassianUser, setIsAtlassianUser] = useState(false); const [feedbackType, setFeedbackType] = React.useState<'like' | 'dislike' | undefined>(undefined); @@ -268,6 +270,9 @@ const RovoDevView: React.FC = () => { case RovoDevProviderMessageType.SignalPromptSent: setIsDeepPlanToggled(event.enable_deep_plan || false); setPendingToolCallMessage(DEFAULT_LOADING_MESSAGE); + if (event.promptId) { + setActivePromptId(event.promptId); + } if (event.echoMessage) { handleAppendResponse({ event_kind: '_RovoDevUserPrompt', @@ -278,9 +283,21 @@ const RovoDevView: React.FC = () => { break; case RovoDevProviderMessageType.RovoDevResponseMessage: - setCurrentState((prev) => - prev.state === 'WaitingForPrompt' ? { state: 'GeneratingResponse' } : prev, - ); + const messagePromptId = event.promptId; + + setCurrentState((prev) => { + // If we are waiting for a prompt, only switch to generating if this is the ACTIVE prompt + if (prev.state === 'WaitingForPrompt') { + if (messagePromptId && activePromptId && messagePromptId === activePromptId) { + return { state: 'GeneratingResponse' }; + } + // Legacy fallback: if IDs are missing, assume we should generate + if (!messagePromptId || !activePromptId) { + return { state: 'GeneratingResponse' }; + } + } + return prev; + }); const messages = Array.isArray(event.message) ? event.message : [event.message]; @@ -305,6 +322,12 @@ const RovoDevView: React.FC = () => { // Signal that we need to send render acknowledgement after this render completes setLastCompletedPromptId(event.promptId); } + + if (event.isCancellation) { + setActivePromptId(undefined); // Clear active prompt so late messages don't re-lock UI + // Queued prompt will be sent via useEffect below after state updates + } + setSummaryMessageInHistory(); setPendingToolCallMessage(''); setModalDialogs([]); @@ -503,7 +526,7 @@ const RovoDevView: React.FC = () => { break; } }, - [handleAppendResponse, currentState.state, setSummaryMessageInHistory, clearChatHistory], + [handleAppendResponse, currentState.state, setSummaryMessageInHistory, clearChatHistory, activePromptId], ); const { postMessage, postMessagePromise, setState } = useMessagingApi< @@ -561,6 +584,19 @@ const RovoDevView: React.FC = () => { return false; } + // If there's an active prompt (cancellation in progress), queue this prompt + if (activePromptId && currentState.state === 'CancellingResponse') { + setQueuedPrompt(text); + // Show a message to the user that cancellation is still being processed + handleAppendResponse({ + event_kind: '_RovoDevDialog', + type: 'info', + title: 'Cancellation in progress', + text: 'Your previous prompt is being cancelled. Your new prompt will be sent once cancellation completes.', + }); + return false; + } + const isWaitingForPrompt = currentState.state === 'WaitingForPrompt' || (currentState.state === 'Initializing' && !currentState.isPromptPending); @@ -589,7 +625,7 @@ const RovoDevView: React.FC = () => { return true; }, - [currentState, isDeepPlanToggled, promptContextCollection, postMessage], + [currentState, isDeepPlanToggled, promptContextCollection, postMessage, activePromptId, handleAppendResponse], ); React.useEffect(() => { @@ -617,6 +653,18 @@ const RovoDevView: React.FC = () => { } }, [lastCompletedPromptId, currentState.state, postMessage]); + // Send queued prompt after cancellation completes + React.useEffect(() => { + if (queuedPrompt && !activePromptId && currentState.state === 'WaitingForPrompt') { + const promptToSend = queuedPrompt; + setQueuedPrompt(undefined); + // Send on next tick to ensure state updates have propagated + setTimeout(() => { + sendPrompt(promptToSend); + }, 0); + } + }, [queuedPrompt, activePromptId, currentState.state, sendPrompt]); + const executeCodePlan = useCallback(() => { if (currentState.state !== 'WaitingForPrompt') { return;