From fb75d8db7cebbf1e3eb3a1dc6ef7b5bf712a3dbe Mon Sep 17 00:00:00 2001 From: teg-atlassian Date: Thu, 29 Jan 2026 15:37:16 -0800 Subject: [PATCH 1/4] fix(rovo-dev): Implement responsive cancel with graceful degradation --- src/rovo-dev/rovoDevChatProvider.ts | 11 +++++++ .../rovoDevWebviewProviderMessages.ts | 9 ++++-- src/rovo-dev/ui/rovoDevView.tsx | 32 +++++++++++++++---- 3 files changed, 43 insertions(+), 9 deletions(-) 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..ef41de5ee 100644 --- a/src/rovo-dev/ui/rovoDevView.tsx +++ b/src/rovo-dev/ui/rovoDevView.tsx @@ -76,6 +76,7 @@ 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 [isAtlassianUser, setIsAtlassianUser] = useState(false); const [feedbackType, setFeedbackType] = React.useState<'like' | 'dislike' | undefined>(undefined); @@ -268,6 +269,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,12 +282,23 @@ const RovoDevView: React.FC = () => { break; case RovoDevProviderMessageType.RovoDevResponseMessage: - setCurrentState((prev) => - prev.state === 'WaitingForPrompt' ? { state: 'GeneratingResponse' } : prev, - ); - - const messages = Array.isArray(event.message) ? event.message : [event.message]; + 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' }; + } + // Otherwise (prompt mismatch or cleared active prompt), stay in WaitingForPrompt + return prev; + } + return prev; + }); const last = messages.at(-1); if (last?.event_kind === 'tool-call') { setPendingToolCallMessage(parseToolCallMessage(last.tool_name)); @@ -305,6 +320,11 @@ 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 + } + setSummaryMessageInHistory(); setPendingToolCallMessage(''); setModalDialogs([]); @@ -503,7 +523,7 @@ const RovoDevView: React.FC = () => { break; } }, - [handleAppendResponse, currentState.state, setSummaryMessageInHistory, clearChatHistory], + [handleAppendResponse, currentState.state, setSummaryMessageInHistory, clearChatHistory, activePromptId], ); const { postMessage, postMessagePromise, setState } = useMessagingApi< From 7aeeae41b5a126e6e39f8f554c1b264537f913e4 Mon Sep 17 00:00:00 2001 From: teg-atlassian Date: Thu, 29 Jan 2026 15:55:26 -0800 Subject: [PATCH 2/4] fix: Resolve compiler error in rovoDevView.tsx --- src/rovo-dev/ui/rovoDevView.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/rovo-dev/ui/rovoDevView.tsx b/src/rovo-dev/ui/rovoDevView.tsx index ef41de5ee..54b0d8c67 100644 --- a/src/rovo-dev/ui/rovoDevView.tsx +++ b/src/rovo-dev/ui/rovoDevView.tsx @@ -299,6 +299,9 @@ const RovoDevView: React.FC = () => { } return prev; }); + + const messages = Array.isArray(event.message) ? event.message : [event.message]; + const last = messages.at(-1); if (last?.event_kind === 'tool-call') { setPendingToolCallMessage(parseToolCallMessage(last.tool_name)); From e568937221bf3e7d5c11581cefdd9e86633a1dc2 Mon Sep 17 00:00:00 2001 From: teg-atlassian Date: Fri, 30 Jan 2026 11:07:11 -0800 Subject: [PATCH 3/4] code review comment addressed --- src/rovo-dev/ui/rovoDevView.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/rovo-dev/ui/rovoDevView.tsx b/src/rovo-dev/ui/rovoDevView.tsx index 54b0d8c67..c13bb9097 100644 --- a/src/rovo-dev/ui/rovoDevView.tsx +++ b/src/rovo-dev/ui/rovoDevView.tsx @@ -294,8 +294,6 @@ const RovoDevView: React.FC = () => { if (!messagePromptId || !activePromptId) { return { state: 'GeneratingResponse' }; } - // Otherwise (prompt mismatch or cleared active prompt), stay in WaitingForPrompt - return prev; } return prev; }); From ad08768d47bf93139d24f3af2c059797a7274c51 Mon Sep 17 00:00:00 2001 From: teg-atlassian Date: Tue, 3 Feb 2026 19:24:34 -0800 Subject: [PATCH 4/4] handling the case where prompt is sent before cancellation is confirmed by rovodev --- src/rovo-dev/ui/rovoDevView.tsx | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/rovo-dev/ui/rovoDevView.tsx b/src/rovo-dev/ui/rovoDevView.tsx index c13bb9097..84a4a4ea8 100644 --- a/src/rovo-dev/ui/rovoDevView.tsx +++ b/src/rovo-dev/ui/rovoDevView.tsx @@ -77,6 +77,7 @@ const RovoDevView: React.FC = () => { 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); @@ -324,6 +325,7 @@ const RovoDevView: React.FC = () => { 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(); @@ -582,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); @@ -610,7 +625,7 @@ const RovoDevView: React.FC = () => { return true; }, - [currentState, isDeepPlanToggled, promptContextCollection, postMessage], + [currentState, isDeepPlanToggled, promptContextCollection, postMessage, activePromptId, handleAppendResponse], ); React.useEffect(() => { @@ -638,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;