Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
19 changes: 9 additions & 10 deletions ui/desktop/src/components/BaseChat2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
43 changes: 23 additions & 20 deletions ui/desktop/src/components/LoadingGoose.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,38 @@ 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, 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…',
};

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, 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} />,
};

// 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 (
<div className="w-full animate-fade-slide-up">
<div
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>
);
Expand Down
16 changes: 13 additions & 3 deletions ui/desktop/src/hooks/useChatStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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', {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -342,6 +351,7 @@ export function useChatStream({
response,
currentMessages,
(messages: Message[]) => setMessagesAndLog(messages, 'streaming'),
setChatState,
onFinish
);

Expand Down
11 changes: 9 additions & 2 deletions ui/desktop/src/hooks/useMessageStream.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
}

Expand Down
2 changes: 2 additions & 0 deletions ui/desktop/src/types/chatState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export enum ChatState {
Thinking = 'thinking',
Streaming = 'streaming',
WaitingForUserInput = 'waitingForUserInput',
Compacting = 'compacting',
LoadingConversation = 'loadingConversation',
}
19 changes: 19 additions & 0 deletions ui/desktop/src/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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;
}
Loading