Make cancel not explode.

- We were console.erroring, throwing and early aborting. Instead we now treat cancels like a normal user message and show an indicator in the UI

Fixes https://b.corp.google.com/issues/416515841
This commit is contained in:
Taylor Mullen 2025-05-09 22:47:18 -07:00 committed by N. Taylor Mullen
parent 28f9a2adfa
commit 090198a7d6
3 changed files with 18 additions and 11 deletions

View File

@ -17,7 +17,7 @@ export const InfoMessage: React.FC<InfoMessageProps> = ({ text }) => {
const prefixWidth = prefix.length; const prefixWidth = prefix.length;
return ( return (
<Box flexDirection="row"> <Box flexDirection="row" marginTop={1}>
<Box width={prefixWidth}> <Box width={prefixWidth}>
<Text color={Colors.AccentYellow}>{prefix}</Text> <Text color={Colors.AccentYellow}>{prefix}</Text>
</Box> </Box>

View File

@ -177,8 +177,6 @@ export const useGeminiStream = (
let geminiMessageBuffer = ''; let geminiMessageBuffer = '';
for await (const event of stream) { for await (const event of stream) {
if (signal.aborted) break;
if (event.type === ServerGeminiEventType.Content) { if (event.type === ServerGeminiEventType.Content) {
if ( if (
pendingHistoryItemRef.current?.type !== 'gemini' && pendingHistoryItemRef.current?.type !== 'gemini' &&
@ -293,6 +291,18 @@ export const useGeminiStream = (
); );
setStreamingState(StreamingState.WaitingForConfirmation); setStreamingState(StreamingState.WaitingForConfirmation);
return; // Wait for user confirmation return; // Wait for user confirmation
} else if (event.type === ServerGeminiEventType.UserCancelled) {
// Flush out existing pending history item.
if (pendingHistoryItemRef.current) {
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
setPendingHistoryItem(null);
}
addItem(
{ type: 'info', text: 'User cancelled the request.' },
userMessageTimestamp,
);
setStreamingState(StreamingState.Idle);
return; // Stop processing the stream
} }
} // End stream loop } // End stream loop

View File

@ -47,6 +47,7 @@ export enum GeminiEventType {
ToolCallRequest = 'tool_call_request', ToolCallRequest = 'tool_call_request',
ToolCallResponse = 'tool_call_response', ToolCallResponse = 'tool_call_response',
ToolCallConfirmation = 'tool_call_confirmation', ToolCallConfirmation = 'tool_call_confirmation',
UserCancelled = 'user_cancelled',
} }
export interface ToolCallRequestInfo { export interface ToolCallRequestInfo {
@ -74,7 +75,8 @@ export type ServerGeminiStreamEvent =
| { | {
type: GeminiEventType.ToolCallConfirmation; type: GeminiEventType.ToolCallConfirmation;
value: ServerToolCallConfirmationDetails; value: ServerToolCallConfirmationDetails;
}; }
| { type: GeminiEventType.UserCancelled };
// A turn manages the agentic loop turn within the server context. // A turn manages the agentic loop turn within the server context.
export class Turn { export class Turn {
@ -108,7 +110,8 @@ export class Turn {
for await (const resp of responseStream) { for await (const resp of responseStream) {
this.debugResponses.push(resp); this.debugResponses.push(resp);
if (signal?.aborted) { if (signal?.aborted) {
throw this.abortError(); yield { type: GeminiEventType.UserCancelled };
return;
} }
const text = getResponseText(resp); const text = getResponseText(resp);
@ -240,12 +243,6 @@ export class Turn {
}; };
} }
private abortError(): Error {
const error = new Error('Request cancelled by user during stream.');
error.name = 'AbortError';
return error; // Return instead of throw, let caller handle
}
getConfirmationDetails(): ToolCallConfirmationDetails[] { getConfirmationDetails(): ToolCallConfirmationDetails[] {
return this.confirmationDetails; return this.confirmationDetails;
} }