diff --git a/ui/desktop/src/components/BaseChat2.tsx b/ui/desktop/src/components/BaseChat2.tsx index d578e18c115c..e40974409539 100644 --- a/ui/desktop/src/components/BaseChat2.tsx +++ b/ui/desktop/src/components/BaseChat2.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { SearchView } from './conversation/SearchView'; import LoadingGoose from './LoadingGoose'; -import { getThinkingMessage } from '../types/message'; import PopularChatTopics from './PopularChatTopics'; import ProgressiveMessageList from './ProgressiveMessageList'; import { MainPanelLayout } from './Layout/MainPanelLayout'; @@ -23,6 +22,7 @@ import { scanRecipe } from '../recipe'; import { useCostTracking } from '../hooks/useCostTracking'; import RecipeActivities from './recipes/RecipeActivities'; import { useToolCount } from './alerts/useToolCount'; +import { getThinkingMessage } from '../types/message'; interface BaseChatProps { setChat: (chat: ChatType) => void; @@ -179,13 +179,6 @@ function BaseChatContent({ const initialPrompt = messages.length == 0 && recipe?.prompt ? recipe.prompt : ''; - // Map chatState to LoadingGoose message - const getLoadingMessage = (): string | undefined => { - if (messages.length === 0 && chatState === ChatState.Thinking) { - return 'loading conversation...'; - } - return getThinkingMessage(messages[messages.length - 1]); - }; return (

Warning: BaseChat2!

@@ -255,10 +248,16 @@ function BaseChatContent({ ) : null} - {/* Fixed loading indicator at bottom left of chat container */} {chatState !== ChatState.Idle && !sessionLoadError && (
- + 0 + ? getThinkingMessage(messages[messages.length - 1]) + : undefined + } + />
)}
diff --git a/ui/desktop/src/components/LoadingGoose.tsx b/ui/desktop/src/components/LoadingGoose.tsx index 70e6156979d3..56cddb27aa61 100644 --- a/ui/desktop/src/components/LoadingGoose.tsx +++ b/ui/desktop/src/components/LoadingGoose.tsx @@ -8,18 +8,29 @@ interface LoadingGooseProps { chatState?: ChatState; } -const LoadingGoose = ({ message, chatState = ChatState.Idle }: LoadingGooseProps) => { - // Determine the appropriate message based on state - const getLoadingMessage = () => { - if (message) return message; // Custom message takes priority +const STATE_MESSAGES: Record = { + [ChatState.LoadingConversation]: 'loading conversation...', + [ChatState.Thinking]: 'goose is thinking…', + [ChatState.Streaming]: 'goose is working on it…', + [ChatState.WaitingForUserInput]: 'goose is waiting…', + [ChatState.Compacting]: 'goose is compacting the conversation...', + [ChatState.Idle]: 'goose is working on it…', +}; - if (chatState === ChatState.Thinking) return 'goose is thinking…'; - if (chatState === ChatState.Streaming) return 'goose is working on it…'; - if (chatState === ChatState.WaitingForUserInput) return 'goose is waiting…'; +const STATE_ICONS: Record = { + [ChatState.LoadingConversation]: , + [ChatState.Thinking]: , + [ChatState.Streaming]: , + [ChatState.WaitingForUserInput]: ( + + ), + [ChatState.Compacting]: , + [ChatState.Idle]: , +}; - // Default fallback - return 'goose is working on it…'; - }; +const LoadingGoose = ({ message, chatState = ChatState.Idle }: LoadingGooseProps) => { + const displayMessage = message || STATE_MESSAGES[chatState]; + const icon = STATE_ICONS[chatState]; return (
@@ -27,16 +38,8 @@ const LoadingGoose = ({ message, chatState = ChatState.Idle }: LoadingGooseProps data-testid="loading-indicator" className="flex items-center gap-2 text-xs text-textStandard py-2" > - {chatState === ChatState.Thinking ? ( - - ) : chatState === ChatState.Streaming ? ( - - ) : chatState === ChatState.WaitingForUserInput ? ( - - ) : ( - - )} - {getLoadingMessage()} + {icon} + {displayMessage}
); diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index 9fa88c750497..b9b5bd355367 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { ChatState } from '../types/chatState'; import { Conversation, Message, resumeAgent, Session } from '../api'; import { getApiUrl } from '../config'; -import { createUserMessage } from '../types/message'; +import { createUserMessage, getCompactingMessage, getThinkingMessage } from '../types/message'; const TextDecoder = globalThis.TextDecoder; const resultsCache = new Map(); @@ -101,6 +101,7 @@ async function streamFromResponse( response: Response, initialMessages: Message[], updateMessages: (messages: Message[]) => void, + updateChatState: (state: ChatState) => void, onFinish: (error?: string) => void ): Promise { let chunkCount = 0; @@ -145,6 +146,14 @@ async function streamFromResponse( const msg = event.message; currentMessages = pushMessage(currentMessages, msg); + if (getCompactingMessage(msg)) { + log.state(ChatState.Compacting, { reason: 'compacting notification' }); + updateChatState(ChatState.Compacting); + } else if (getThinkingMessage(msg)) { + log.state(ChatState.Thinking, { reason: 'thinking notification' }); + updateChatState(ChatState.Thinking); + } + // Only log every 10th message event to avoid spam if (messageEventCount % 10 === 0) { log.stream('message-chunk', { @@ -259,11 +268,11 @@ export function useChatStream({ setMessagesAndLog([], 'session-reset'); setSession(undefined); setSessionLoadError(undefined); - setChatState(ChatState.Thinking); + setChatState(ChatState.LoadingConversation); let cancelled = false; - log.state(ChatState.Thinking, { reason: 'session load start' }); + log.state(ChatState.LoadingConversation, { reason: 'session load start' }); (async () => { try { @@ -342,6 +351,7 @@ export function useChatStream({ response, currentMessages, (messages: Message[]) => setMessagesAndLog(messages, 'streaming'), + setChatState, onFinish ); diff --git a/ui/desktop/src/hooks/useMessageStream.ts b/ui/desktop/src/hooks/useMessageStream.ts index 44ffa2527018..d05aaadbe067 100644 --- a/ui/desktop/src/hooks/useMessageStream.ts +++ b/ui/desktop/src/hooks/useMessageStream.ts @@ -1,6 +1,11 @@ import { useCallback, useEffect, useId, useReducer, useRef, useState } from 'react'; import useSWR from 'swr'; -import { createUserMessage, getThinkingMessage, hasCompletedToolCalls } from '../types/message'; +import { + createUserMessage, + getThinkingMessage, + getCompactingMessage, + hasCompletedToolCalls, +} from '../types/message'; import { Conversation, Message, Role } from '../api'; import { getSession, Session } from '../api'; @@ -307,7 +312,9 @@ export function useMessageStream({ mutateChatState(ChatState.WaitingForUserInput); } - if (getThinkingMessage(newMessage)) { + if (getCompactingMessage(newMessage)) { + mutateChatState(ChatState.Compacting); + } else if (getThinkingMessage(newMessage)) { mutateChatState(ChatState.Thinking); } diff --git a/ui/desktop/src/types/chatState.ts b/ui/desktop/src/types/chatState.ts index 4adc5f0ece57..067aee4f7b0b 100644 --- a/ui/desktop/src/types/chatState.ts +++ b/ui/desktop/src/types/chatState.ts @@ -3,4 +3,6 @@ export enum ChatState { Thinking = 'thinking', Streaming = 'streaming', WaitingForUserInput = 'waitingForUserInput', + Compacting = 'compacting', + LoadingConversation = 'loadingConversation', } diff --git a/ui/desktop/src/types/message.ts b/ui/desktop/src/types/message.ts index 9498ffec32b1..1c433f88d14b 100644 --- a/ui/desktop/src/types/message.ts +++ b/ui/desktop/src/types/message.ts @@ -3,6 +3,9 @@ import { Message, ToolConfirmationRequest, ToolRequest, ToolResponse } from '../ export type ToolRequestMessageContent = ToolRequest & { type: 'toolRequest' }; export type ToolResponseMessageContent = ToolResponse & { type: 'toolResponse' }; +// Compaction response message - must match backend constant +const COMPACTION_THINKING_TEXT = 'goose is compacting the conversation...'; + export function createUserMessage(text: string): Message { return { id: generateMessageId(), @@ -84,3 +87,19 @@ export function getThinkingMessage(message: Message | undefined): string | undef return undefined; } + +export function getCompactingMessage(message: Message | undefined): string | undefined { + if (!message || message.role !== 'assistant') { + return undefined; + } + + for (const content of message.content) { + if (content.type === 'systemNotification' && content.notificationType === 'thinkingMessage') { + if (content.msg === COMPACTION_THINKING_TEXT) { + return content.msg; + } + } + } + + return undefined; +}