Skip to content

Commit

Permalink
fix warnings and double output
Browse files Browse the repository at this point in the history
  • Loading branch information
baxen committed Feb 26, 2025
1 parent 11f9edd commit 0517f32
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 164 deletions.
3 changes: 1 addition & 2 deletions crates/goose-server/src/routes/reply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ use bytes::Bytes;
use futures::{stream::StreamExt, Stream};
use goose::message::{Message, MessageContent};

use mcp_core::{content::Content, role::Role};
use mcp_core::role::Role;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::{
convert::Infallible,
pin::Pin,
Expand Down
42 changes: 21 additions & 21 deletions ui/desktop/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,27 @@ export default function ChatView({ setView }: { setView: (view: View) => void })
const [showGame, setShowGame] = useState(false);
const scrollRef = useRef<ScrollAreaHandle>(null);

const {
messages,
append,
stop,
isLoading,
error,
const {
messages,
append,
stop,
isLoading,
error,
setMessages,
input,
setInput,
handleInputChange,
handleSubmit: submitMessage
input: _input,
setInput: _setInput,
handleInputChange: _handleInputChange,
handleSubmit: _submitMessage,
} = useMessageStream({
api: getApiUrl('/reply'),
initialMessages: chat?.messages || [],
onFinish: async (message, reason) => {
onFinish: async (message, _reason) => {
window.electron.stopPowerSaveBlocker();

// Extract text content from the message to pass to askAi
const messageText = getTextContent(message);
const fetchResponses = await askAi(messageText);
setMessageMetadata((prev) => ({ ...prev, [message.id || ''] : fetchResponses }));
setMessageMetadata((prev) => ({ ...prev, [message.id || '']: fetchResponses }));

const timeSinceLastInteraction = Date.now() - lastInteractionTime;
window.electron.logInfo('last interaction:' + lastInteractionTime);
Expand All @@ -72,12 +72,12 @@ export default function ChatView({ setView }: { setView: (view: View) => void })
// Handle tool calls if needed
console.log('Tool call received:', toolCall);
// Implement tool call handling logic here
}
},
});

// Update chat messages when they change
useEffect(() => {
setChat({ ...chat, messages });
setChat((prevChat) => ({ ...prevChat, messages }));
}, [messages]);

useEffect(() => {
Expand Down Expand Up @@ -125,19 +125,19 @@ export default function ChatView({ setView }: { setView: (view: View) => void })

// Filter out standalone tool response messages for rendering
// They will be shown as part of the tool invocation in the assistant message
const filteredMessages = messages.filter(message => {
const filteredMessages = messages.filter((message) => {
// Keep all assistant messages and user messages that aren't just tool responses
if (message.role === 'assistant') return true;

// For user messages, check if they're only tool responses
if (message.role === 'user') {
const hasOnlyToolResponses = message.content.every(c => 'ToolResponse' in c);
const hasTextContent = message.content.some(c => 'Text' in c);
const hasOnlyToolResponses = message.content.every((c) => 'ToolResponse' in c);
const hasTextContent = message.content.some((c) => 'Text' in c);

// Keep the message if it has text content or is not just tool responses
return hasTextContent || !hasOnlyToolResponses;
}

return true;
});

Expand Down Expand Up @@ -206,4 +206,4 @@ export default function ChatView({ setView }: { setView: (view: View) => void })
{showGame && <FlappyGoose onClose={() => setShowGame(false)} />}
</div>
);
}
}
43 changes: 12 additions & 31 deletions ui/desktop/src/components/GooseMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React, { useMemo } from 'react';
import ToolInvocations from './ToolInvocations';
import LinkPreview from './LinkPreview';
import GooseResponseForm from './GooseResponseForm';
import { extractUrls } from '../utils/urlUtils';
import MarkdownContent from './MarkdownContent';
import ToolCallWithResponse from './ToolCallWithResponse';
import { Message, getTextContent, getToolRequests, getToolResponses } from '../types/message';

interface GooseMessageProps {
message: Message;
messages: Message[];
metadata?: any;
metadata?: string[];
append: (value: string) => void;
}

Expand Down Expand Up @@ -51,46 +51,27 @@ export default function GooseMessage({ message, metadata, messages, append }: Go
return responseMap;
}, [messages, messageIndex, toolRequests]);

// Convert tool requests to the format expected by ToolInvocations
const toolInvocations = useMemo(() => {
const invocations = toolRequests
.map((toolRequest) => {
const toolCall = toolRequest.tool_call.Ok;

if (!toolCall) {
return null;
}

const toolResponse = toolResponsesMap.get(toolRequest.id);

return {
toolCallId: toolRequest.id,
toolName: toolCall.name,
args: toolCall.arguments,
state: toolResponse ? 'result' : 'running',
result: toolResponse?.tool_result?.Ok || undefined,
};
})
.filter(Boolean);

return invocations;
}, [toolRequests, toolResponsesMap]);

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">
{/* Always show the top content area if there are tool calls, even if textContent is empty */}
{(textContent || toolInvocations.length > 0) && (
{(textContent || toolRequests.length > 0) && (
<div
className={`goose-message-content bg-bgSubtle rounded-2xl px-4 py-2 ${toolInvocations.length > 0 ? 'rounded-b-none' : ''}`}
className={`goose-message-content bg-bgSubtle rounded-2xl px-4 py-2 ${toolRequests.length > 0 ? 'rounded-b-none' : ''}`}
>
{textContent ? <MarkdownContent content={textContent} /> : null}
</div>
)}

{toolInvocations.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">
<ToolInvocations toolInvocations={toolInvocations} />
{toolRequests.map((toolRequest) => (
<ToolCallWithResponse
key={toolRequest.id}
toolRequest={toolRequest}
toolResponse={toolResponsesMap.get(toolRequest.id)}
/>
))}
</div>
)}
</div>
Expand Down
5 changes: 3 additions & 2 deletions ui/desktop/src/components/GooseResponseForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import MarkdownContent from './MarkdownContent';
import { Button } from './ui/button';
import { cn } from '../utils';
import { Send } from './icons';
import { createUserMessage } from '../types/message';
// Prefixing unused imports with underscore
import { createUserMessage as _createUserMessage } from '../types/message';

interface FormField {
label: string;
Expand All @@ -21,7 +22,7 @@ interface DynamicForm {

interface GooseResponseFormProps {
message: string;
metadata: any;
metadata: string[] | null;
append: (value: string) => void;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,32 @@ import { Card } from './ui/card';
import Box from './ui/Box';
import { ToolCallArguments } from './ToolCallArguments';
import MarkdownContent from './MarkdownContent';
import { snakeToTitleCase } from '../utils';
import { LoadingPlaceholder } from './LoadingPlaceholder';
import { ChevronUp } from 'lucide-react';
import { Content } from '../types/message';
import { Content, ToolRequest, ToolResponse } from '../types/message';
import { snakeToTitleCase } from '../utils';

interface ToolInvocation {
toolCallId: string;
toolName: string;
args: any;
state: 'running' | 'result';
result?: Content[];
interface ToolCallWithResponseProps {
toolRequest: ToolRequest;
toolResponse?: ToolResponse;
}

interface ToolInvocationsProps {
toolInvocations: ToolInvocation[];
}
export default function ToolCallWithResponse({
toolRequest,
toolResponse,
}: ToolCallWithResponseProps) {
const toolCall = toolRequest.tool_call.Ok;

export default function ToolInvocations({ toolInvocations }: ToolInvocationsProps) {
return (
<>
{toolInvocations.map((toolInvocation) => (
<ToolInvocation key={toolInvocation.toolCallId} toolInvocation={toolInvocation} />
))}
</>
);
}
if (!toolCall) {
return null;
}

function ToolInvocation({ toolInvocation }: { toolInvocation: ToolInvocation }) {
return (
<div className="w-full">
<Card className="">
<ToolCall call={toolInvocation} />
{toolInvocation.state === 'result' ? (
<ToolResult result={toolInvocation} />
<ToolCallView toolCall={toolCall} />
{toolResponse ? (
<ToolResultView result={toolResponse.tool_result.Ok} />
) : (
<LoadingPlaceholder />
)}
Expand All @@ -45,56 +37,48 @@ function ToolInvocation({ toolInvocation }: { toolInvocation: ToolInvocation })
);
}

interface ToolCallProps {
call: {
state: 'running' | 'result';
toolCallId: string;
toolName: string;
args: Record<string, any>;
interface ToolCallViewProps {
toolCall: {
name: string;
arguments: Record<string, unknown>;
};
}

function ToolCall({ call }: ToolCallProps) {
function ToolCallView({ toolCall }: ToolCallViewProps) {
return (
<div>
<div className="flex items-center mb-4">
<Box size={16} />
<span className="ml-[8px] text-textStandard">
{snakeToTitleCase(call.toolName.substring(call.toolName.lastIndexOf('__') + 2))}
{snakeToTitleCase(toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2))}
</span>
</div>

{call.args && <ToolCallArguments args={call.args} />}
{toolCall.arguments && <ToolCallArguments args={toolCall.arguments} />}

<div className="self-stretch h-px my-[10px] -mx-4 bg-borderSubtle dark:bg-gray-700" />
</div>
);
}

interface ToolResultProps {
result: {
result?: Content[];
state?: string;
toolCallId?: string;
toolName?: string;
args?: any;
};
interface ToolResultViewProps {
result?: Content[];
}

function ToolResult({ result }: ToolResultProps) {
function ToolResultView({ result }: ToolResultViewProps) {
// State to track expanded items
const [expandedItems, setExpandedItems] = React.useState<number[]>([]);

// If no result info, don't show anything
if (!result || !result.result) return null;
if (!result) return null;

// Normalize to an array
const results = Array.isArray(result.result) ? result.result : [result.result];
// Find results where either audience is not set, or it's set to a list that includes user
const filteredResults = result.filter((item) => {
// Check audience (which may not be in the type)
const audience = item.annotations?.audience;

// Find results where either audience is not set, or it's set to a list that contains user
const filteredResults = results.filter(
(item) => !item.audience || item.audience?.includes('user')
);
return !audience || audience.includes('user');
});

if (filteredResults.length === 0) return null;

Expand All @@ -105,20 +89,14 @@ function ToolResult({ result }: ToolResultProps) {
};

const shouldShowExpanded = (item: Content, index: number) => {
// (priority is defined and > 0.5) OR already in the expandedItems
return (
(item.priority !== undefined && item.priority >= 0.5) ||
expandedItems.includes(index)
);
return (item.priority !== undefined && item.priority >= 0.5) || expandedItems.includes(index);
};

return (
<div className="">
{filteredResults.map((item, index) => {
const isExpanded = shouldShowExpanded(item, index);
// minimize if priority is not set or < 0.5
const shouldMinimize =
item.priority === undefined || item.priority < 0.5;
const shouldMinimize = item.priority === undefined || item.priority < 0.5;
return (
<div key={index} className="relative">
{shouldMinimize && (
Expand Down
Loading

0 comments on commit 0517f32

Please sign in to comment.