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;
+}