fix: continue to use resumed session after confirmation is cancelled (#1548)

This commit is contained in:
Yingjie He
2025-03-06 16:51:31 -08:00
committed by GitHub
parent 6cb6e1d918
commit fb444728f0
5 changed files with 82 additions and 25 deletions

View File

@@ -29,6 +29,9 @@ import {
export interface ChatType { export interface ChatType {
id: number; id: number;
title: string; title: string;
// messages up to this index are presumed to be "history" from a resumed session, this is used to track older tool confirmation requests
// anything before this index should not render any buttons, but anything after should
messageHistoryIndex: number;
messages: Message[]; messages: Message[];
} }
@@ -76,6 +79,7 @@ export default function ChatView({
return { return {
id: Date.now(), id: Date.now(),
title: resumedSession.metadata?.description || `ID: ${resumedSession.session_id}`, title: resumedSession.metadata?.description || `ID: ${resumedSession.session_id}`,
messageHistoryIndex: convertedMessages.length,
messages: convertedMessages, messages: convertedMessages,
}; };
} catch (e) { } catch (e) {
@@ -98,6 +102,7 @@ export default function ChatView({
id: Date.now(), id: Date.now(),
title: 'Chat 1', title: 'Chat 1',
messages: [], messages: [],
messageHistoryIndex: 0,
}; };
}); });
const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({}); const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
@@ -319,10 +324,15 @@ export default function ChatView({
<UserMessage message={message} /> <UserMessage message={message} />
) : ( ) : (
<GooseMessage <GooseMessage
messageHistoryIndex={chat?.messageHistoryIndex}
message={message} message={message}
messages={messages} messages={messages}
metadata={messageMetadata[message.id || '']} metadata={messageMetadata[message.id || '']}
append={(text) => append(createUserMessage(text))} append={(text) => append(createUserMessage(text))}
appendMessage={(newMessage) => {
const updatedMessages = [...messages, newMessage];
setMessages(updatedMessages);
}}
/> />
)} )}
</div> </div>

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useRef } from 'react'; import React, { useEffect, useMemo, useRef } from 'react';
import LinkPreview from './LinkPreview'; import LinkPreview from './LinkPreview';
import GooseResponseForm from './GooseResponseForm'; import GooseResponseForm from './GooseResponseForm';
import { extractUrls } from '../utils/urlUtils'; import { extractUrls } from '../utils/urlUtils';
@@ -10,18 +10,28 @@ import {
getToolRequests, getToolRequests,
getToolResponses, getToolResponses,
getToolConfirmationContent, getToolConfirmationContent,
createToolErrorResponseMessage,
} from '../types/message'; } from '../types/message';
import ToolCallConfirmation from './ToolCallConfirmation'; import ToolCallConfirmation from './ToolCallConfirmation';
import MessageCopyLink from './MessageCopyLink'; import MessageCopyLink from './MessageCopyLink';
interface GooseMessageProps { interface GooseMessageProps {
messageHistoryIndex: number;
message: Message; message: Message;
messages: Message[]; messages: Message[];
metadata?: string[]; metadata?: string[];
append: (value: string) => void; append: (value: string) => void;
appendMessage: (message: Message) => void;
} }
export default function GooseMessage({ message, metadata, messages, append }: GooseMessageProps) { export default function GooseMessage({
messageHistoryIndex,
message,
metadata,
messages,
append,
appendMessage,
}: GooseMessageProps) {
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
// Extract text content from the message // Extract text content from the message
@@ -64,6 +74,16 @@ export default function GooseMessage({ message, metadata, messages, append }: Go
return responseMap; return responseMap;
}, [messages, messageIndex, toolRequests]); }, [messages, messageIndex, toolRequests]);
useEffect(() => {
// If the message is the last message in the resumed session and has tool confirmation, it means the tool confirmation
// is broken or cancelled, to contonue use the session, we need to append a tool response to avoid mismatch tool result error.
if (messageIndex == messageHistoryIndex - 1 && hasToolConfirmation) {
appendMessage(
createToolErrorResponseMessage(toolConfirmationContent.id, 'The tool call is cancelled.')
);
}
}, []);
return ( return (
<div className="goose-message flex w-[90%] justify-start opacity-0 animate-[appear_150ms_ease-in_forwards]"> <div className="goose-message flex w-[90%] justify-start opacity-0 animate-[appear_150ms_ease-in_forwards]">
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
@@ -86,17 +106,15 @@ export default function GooseMessage({ message, metadata, messages, append }: Go
</div> </div>
)} )}
{hasToolConfirmation && (
<ToolCallConfirmation
toolConfirmationId={toolConfirmationContent.id}
toolName={toolConfirmationContent.toolName}
/>
)}
{toolRequests.length > 0 && ( {toolRequests.length > 0 && (
<div className="goose-message-tool bg-bgApp border border-borderSubtle dark:border-gray-700 rounded-b-2xl px-4 pt-4 pb-2 mt-1"> <div className="goose-message-tool bg-bgApp border border-borderSubtle dark:border-gray-700 rounded-b-2xl px-4 pt-4 pb-2 mt-1">
{toolRequests.map((toolRequest) => ( {toolRequests.map((toolRequest) => (
<ToolCallWithResponse <ToolCallWithResponse
// If the message is resumed and not matched tool response, it means the tool is broken or cancelled.
isCancelledMessage={
messageIndex < messageHistoryIndex &&
toolResponsesMap.get(toolRequest.id) == undefined
}
key={toolRequest.id} key={toolRequest.id}
toolRequest={toolRequest} toolRequest={toolRequest}
toolResponse={toolResponsesMap.get(toolRequest.id)} toolResponse={toolResponsesMap.get(toolRequest.id)}
@@ -104,6 +122,15 @@ export default function GooseMessage({ message, metadata, messages, append }: Go
))} ))}
</div> </div>
)} )}
{hasToolConfirmation && (
<ToolCallConfirmation
isCancelledMessage={messageIndex == messageHistoryIndex - 1}
isClicked={messageIndex < messageHistoryIndex - 1}
toolConfirmationId={toolConfirmationContent.id}
toolName={toolConfirmationContent.toolName}
/>
)}
</div> </div>
{/* TODO(alexhancock): Re-enable link previews once styled well again */} {/* TODO(alexhancock): Re-enable link previews once styled well again */}

View File

@@ -2,9 +2,14 @@ import React, { useState } from 'react';
import { ConfirmToolRequest } from '../utils/toolConfirm'; import { ConfirmToolRequest } from '../utils/toolConfirm';
import { snakeToTitleCase } from '../utils'; import { snakeToTitleCase } from '../utils';
export default function ToolConfirmation({ toolConfirmationId, toolName }) { export default function ToolConfirmation({
const [clicked, setClicked] = useState(false); isCancelledMessage,
const [status, setStatus] = useState(''); isClicked,
toolConfirmationId,
toolName,
}) {
const [clicked, setClicked] = useState(isClicked);
const [status, setStatus] = useState('unknown');
const handleButtonClick = (confirmed) => { const handleButtonClick = (confirmed) => {
setClicked(true); setClicked(true);
@@ -12,7 +17,11 @@ export default function ToolConfirmation({ toolConfirmationId, toolName }) {
ConfirmToolRequest(toolConfirmationId, confirmed); ConfirmToolRequest(toolConfirmationId, confirmed);
}; };
return ( return isCancelledMessage ? (
<div className="goose-message-content bg-bgSubtle rounded-2xl px-4 py-2 text-textStandard">
Tool call confirmation is cancelled.
</div>
) : (
<> <>
<div className="goose-message-content bg-bgSubtle rounded-2xl px-4 py-2 rounded-b-none text-textStandard"> <div className="goose-message-content bg-bgSubtle rounded-2xl px-4 py-2 rounded-b-none text-textStandard">
Goose would like to call the above tool. Allow? Goose would like to call the above tool. Allow?
@@ -45,7 +54,9 @@ export default function ToolConfirmation({ toolConfirmationId, toolName }) {
</svg> </svg>
)} )}
<span className="ml-2 text-textStandard"> <span className="ml-2 text-textStandard">
{snakeToTitleCase(toolName.substring(toolName.lastIndexOf('__') + 2))} is {status} {isClicked
? 'Tool confirmation is not available'
: `${snakeToTitleCase(toolName.substring(toolName.lastIndexOf('__') + 2))} is ${status}`}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -9,11 +9,13 @@ import { Content, ToolRequestMessageContent, ToolResponseMessageContent } from '
import { snakeToTitleCase } from '../utils'; import { snakeToTitleCase } from '../utils';
interface ToolCallWithResponseProps { interface ToolCallWithResponseProps {
isCancelledMessage: boolean;
toolRequest: ToolRequestMessageContent; toolRequest: ToolRequestMessageContent;
toolResponse?: ToolResponseMessageContent; toolResponse?: ToolResponseMessageContent;
} }
export default function ToolCallWithResponse({ export default function ToolCallWithResponse({
isCancelledMessage,
toolRequest, toolRequest,
toolResponse, toolResponse,
}: ToolCallWithResponseProps) { }: ToolCallWithResponseProps) {
@@ -27,7 +29,8 @@ export default function ToolCallWithResponse({
<div className="w-full"> <div className="w-full">
<Card className=""> <Card className="">
<ToolCallView toolCall={toolCall} /> <ToolCallView toolCall={toolCall} />
{toolResponse ? ( {!isCancelledMessage ? (
toolResponse ? (
<ToolResultView <ToolResultView
result={ result={
toolResponse.toolResult.status === 'success' toolResponse.toolResult.status === 'success'
@@ -37,7 +40,8 @@ export default function ToolCallWithResponse({
/> />
) : ( ) : (
<LoadingPlaceholder /> <LoadingPlaceholder />
)} )
) : undefined}
</Card> </Card>
</div> </div>
); );

View File

@@ -166,6 +166,11 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
<div className="goose-message-tool bg-bgApp border border-borderSubtle dark:border-gray-700 rounded-b-2xl px-4 pt-4 pb-2 mt-1"> <div className="goose-message-tool bg-bgApp border border-borderSubtle dark:border-gray-700 rounded-b-2xl px-4 pt-4 pb-2 mt-1">
{toolRequests.map((toolRequest) => ( {toolRequests.map((toolRequest) => (
<ToolCallWithResponse <ToolCallWithResponse
// In the session history page, if no tool response found for given request, it means the tool call
// is broken or cancelled.
isCancelledMessage={
toolResponsesMap.get(toolRequest.id) == undefined
}
key={toolRequest.id} key={toolRequest.id}
toolRequest={toolRequest} toolRequest={toolRequest}
toolResponse={toolResponsesMap.get(toolRequest.id)} toolResponse={toolResponsesMap.get(toolRequest.id)}