Skip to content
Merged
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
10 changes: 10 additions & 0 deletions ui/desktop/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import { Card } from './ui/card';
import { ScrollArea, ScrollAreaHandle } from './ui/scroll-area';
import UserMessage from './UserMessage';
import { askAi } from '../utils/askAI';

Check warning on line 14 in ui/desktop/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Lint Electron Desktop App

'askAi' is defined but never used. Allowed unused vars must match /^_/u
import Splash from './Splash';
import 'react-toastify/dist/ReactToastify.css';
import { useMessageStream } from '../hooks/useMessageStream';
Expand All @@ -23,12 +23,15 @@
ToolRequestMessageContent,
ToolResponseMessageContent,
ToolConfirmationRequestMessageContent,
getTextContent,

Check warning on line 26 in ui/desktop/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Lint Electron Desktop App

'getTextContent' is defined but never used. Allowed unused vars must match /^_/u
} from '../types/message';

export interface ChatType {
id: number;
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[];
}

Expand Down Expand Up @@ -76,6 +79,7 @@
return {
id: Date.now(),
title: resumedSession.metadata?.description || `ID: ${resumedSession.session_id}`,
messageHistoryIndex: convertedMessages.length,
messages: convertedMessages,
};
} catch (e) {
Expand All @@ -98,6 +102,7 @@
id: Date.now(),
title: 'Chat 1',
messages: [],
messageHistoryIndex: 0,
};
});
const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
Expand Down Expand Up @@ -319,10 +324,15 @@
<UserMessage message={message} />
) : (
<GooseMessage
messageHistoryIndex={chat?.messageHistoryIndex}
message={message}
messages={messages}
metadata={messageMetadata[message.id || '']}
append={(text) => append(createUserMessage(text))}
appendMessage={(newMessage) => {
const updatedMessages = [...messages, newMessage];
setMessages(updatedMessages);
}}
/>
)}
</div>
Expand Down
45 changes: 36 additions & 9 deletions ui/desktop/src/components/GooseMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo, useRef } from 'react';
import React, { useEffect, useMemo, useRef } from 'react';
import LinkPreview from './LinkPreview';
import GooseResponseForm from './GooseResponseForm';
import { extractUrls } from '../utils/urlUtils';
Expand All @@ -10,18 +10,28 @@ import {
getToolRequests,
getToolResponses,
getToolConfirmationContent,
createToolErrorResponseMessage,
} from '../types/message';
import ToolCallConfirmation from './ToolCallConfirmation';
import MessageCopyLink from './MessageCopyLink';

interface GooseMessageProps {
messageHistoryIndex: number;
message: Message;
messages: Message[];
metadata?: string[];
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);

// Extract text content from the message
Expand Down Expand Up @@ -64,6 +74,16 @@ export default function GooseMessage({ message, metadata, messages, append }: Go
return responseMap;
}, [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 (
<div className="goose-message flex w-[90%] justify-start opacity-0 animate-[appear_150ms_ease-in_forwards]">
<div className="flex flex-col w-full">
Expand All @@ -86,24 +106,31 @@ export default function GooseMessage({ message, metadata, messages, append }: Go
</div>
)}

{hasToolConfirmation && (
<ToolCallConfirmation
toolConfirmationId={toolConfirmationContent.id}
toolName={toolConfirmationContent.toolName}
/>
)}

{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">
{toolRequests.map((toolRequest) => (
<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}
toolRequest={toolRequest}
toolResponse={toolResponsesMap.get(toolRequest.id)}
/>
))}
</div>
)}

{hasToolConfirmation && (
<ToolCallConfirmation
isCancelledMessage={messageIndex == messageHistoryIndex - 1}
isClicked={messageIndex < messageHistoryIndex - 1}
toolConfirmationId={toolConfirmationContent.id}
toolName={toolConfirmationContent.toolName}
/>
)}
</div>

{/* TODO(alexhancock): Re-enable link previews once styled well again */}
Expand Down
21 changes: 16 additions & 5 deletions ui/desktop/src/components/ToolCallConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@ import React, { useState } from 'react';
import { ConfirmToolRequest } from '../utils/toolConfirm';
import { snakeToTitleCase } from '../utils';

export default function ToolConfirmation({ toolConfirmationId, toolName }) {
const [clicked, setClicked] = useState(false);
const [status, setStatus] = useState('');
export default function ToolConfirmation({
isCancelledMessage,
isClicked,
toolConfirmationId,
toolName,
}) {
const [clicked, setClicked] = useState(isClicked);
const [status, setStatus] = useState('unknown');

const handleButtonClick = (confirmed) => {
setClicked(true);
setStatus(confirmed ? 'approved' : 'denied');
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">
Goose would like to call the above tool. Allow?
Expand Down Expand Up @@ -45,7 +54,9 @@ export default function ToolConfirmation({ toolConfirmationId, toolName }) {
</svg>
)}
<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>
</div>
</div>
Expand Down
26 changes: 15 additions & 11 deletions ui/desktop/src/components/ToolCallWithResponse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import { Content, ToolRequestMessageContent, ToolResponseMessageContent } from '
import { snakeToTitleCase } from '../utils';

interface ToolCallWithResponseProps {
isCancelledMessage: boolean;
toolRequest: ToolRequestMessageContent;
toolResponse?: ToolResponseMessageContent;
}

export default function ToolCallWithResponse({
isCancelledMessage,
toolRequest,
toolResponse,
}: ToolCallWithResponseProps) {
Expand All @@ -27,17 +29,19 @@ export default function ToolCallWithResponse({
<div className="w-full">
<Card className="">
<ToolCallView toolCall={toolCall} />
{toolResponse ? (
<ToolResultView
result={
toolResponse.toolResult.status === 'success'
? toolResponse.toolResult.value
: undefined
}
/>
) : (
<LoadingPlaceholder />
)}
{!isCancelledMessage ? (
toolResponse ? (
<ToolResultView
result={
toolResponse.toolResult.status === 'success'
? toolResponse.toolResult.value
: undefined
}
/>
) : (
<LoadingPlaceholder />
)
) : undefined}
</Card>
</div>
);
Expand Down
5 changes: 5 additions & 0 deletions ui/desktop/src/components/sessions/SessionHistoryView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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">
{toolRequests.map((toolRequest) => (
<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}
toolRequest={toolRequest}
toolResponse={toolResponsesMap.get(toolRequest.id)}
Expand Down
Loading