mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
Unify loading goose messages and usechatstream determines chat state (#5306)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -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 (
|
||||
<div className="h-full flex flex-col min-h-0">
|
||||
<h2>Warning: BaseChat2!</h2>
|
||||
@@ -255,10 +248,16 @@ function BaseChatContent({
|
||||
) : null}
|
||||
</ScrollArea>
|
||||
|
||||
{/* Fixed loading indicator at bottom left of chat container */}
|
||||
{chatState !== ChatState.Idle && !sessionLoadError && (
|
||||
<div className="absolute bottom-1 left-4 z-20 pointer-events-none">
|
||||
<LoadingGoose message={getLoadingMessage()} chatState={chatState} />
|
||||
<LoadingGoose
|
||||
chatState={chatState}
|
||||
message={
|
||||
messages.length > 0
|
||||
? getThinkingMessage(messages[messages.length - 1])
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,18 +8,29 @@ interface LoadingGooseProps {
|
||||
chatState?: ChatState;
|
||||
}
|
||||
|
||||
const STATE_MESSAGES: Record<ChatState, string> = {
|
||||
[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…',
|
||||
};
|
||||
|
||||
const STATE_ICONS: Record<ChatState, React.ReactNode> = {
|
||||
[ChatState.LoadingConversation]: <AnimatedIcons className="flex-shrink-0" cycleInterval={600} />,
|
||||
[ChatState.Thinking]: <AnimatedIcons className="flex-shrink-0" cycleInterval={600} />,
|
||||
[ChatState.Streaming]: <FlyingBird className="flex-shrink-0" cycleInterval={150} />,
|
||||
[ChatState.WaitingForUserInput]: (
|
||||
<AnimatedIcons className="flex-shrink-0" cycleInterval={600} variant="waiting" />
|
||||
),
|
||||
[ChatState.Compacting]: <AnimatedIcons className="flex-shrink-0" cycleInterval={600} />,
|
||||
[ChatState.Idle]: <GooseLogo size="small" hover={false} />,
|
||||
};
|
||||
|
||||
const LoadingGoose = ({ message, chatState = ChatState.Idle }: LoadingGooseProps) => {
|
||||
// Determine the appropriate message based on state
|
||||
const getLoadingMessage = () => {
|
||||
if (message) return message; // Custom message takes priority
|
||||
|
||||
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…';
|
||||
|
||||
// Default fallback
|
||||
return 'goose is working on it…';
|
||||
};
|
||||
const displayMessage = message || STATE_MESSAGES[chatState];
|
||||
const icon = STATE_ICONS[chatState];
|
||||
|
||||
return (
|
||||
<div className="w-full animate-fade-slide-up">
|
||||
@@ -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 ? (
|
||||
<AnimatedIcons className="flex-shrink-0" cycleInterval={600} />
|
||||
) : chatState === ChatState.Streaming ? (
|
||||
<FlyingBird className="flex-shrink-0" cycleInterval={150} />
|
||||
) : chatState === ChatState.WaitingForUserInput ? (
|
||||
<AnimatedIcons className="flex-shrink-0" cycleInterval={600} variant="waiting" />
|
||||
) : (
|
||||
<GooseLogo size="small" hover={false} />
|
||||
)}
|
||||
{getLoadingMessage()}
|
||||
{icon}
|
||||
{displayMessage}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<string, { messages: Message[]; session: Session }>();
|
||||
@@ -101,6 +101,7 @@ async function streamFromResponse(
|
||||
response: Response,
|
||||
initialMessages: Message[],
|
||||
updateMessages: (messages: Message[]) => void,
|
||||
updateChatState: (state: ChatState) => void,
|
||||
onFinish: (error?: string) => void
|
||||
): Promise<void> {
|
||||
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
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,4 +3,6 @@ export enum ChatState {
|
||||
Thinking = 'thinking',
|
||||
Streaming = 'streaming',
|
||||
WaitingForUserInput = 'waitingForUserInput',
|
||||
Compacting = 'compacting',
|
||||
LoadingConversation = 'loadingConversation',
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user