diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json
index 971f05dbb766..6814ace4eb2a 100644
--- a/ui/desktop/openapi.json
+++ b/ui/desktop/openapi.json
@@ -2332,17 +2332,16 @@
}
},
"ResourceContents": {
- "oneOf": [
+ "anyOf": [
{
"type": "object",
"required": [
- "uri",
- "text"
+ "text",
+ "uri"
],
"properties": {
"mime_type": {
- "type": "string",
- "nullable": true
+ "type": "string"
},
"text": {
"type": "string"
@@ -2355,16 +2354,15 @@
{
"type": "object",
"required": [
- "uri",
- "blob"
+ "blob",
+ "uri"
],
"properties": {
"blob": {
"type": "string"
},
"mime_type": {
- "type": "string",
- "nullable": true
+ "type": "string"
},
"uri": {
"type": "string"
diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts
index 53230d066b87..6df01c7809fc 100644
--- a/ui/desktop/src/api/types.gen.ts
+++ b/ui/desktop/src/api/types.gen.ts
@@ -463,12 +463,12 @@ export type RedactedThinkingContent = {
};
export type ResourceContents = {
- mime_type?: string | null;
+ mime_type?: string;
text: string;
uri: string;
} | {
blob: string;
- mime_type?: string | null;
+ mime_type?: string;
uri: string;
};
diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx
index 247d8bee1116..5b35122805db 100644
--- a/ui/desktop/src/components/BaseChat.tsx
+++ b/ui/desktop/src/components/BaseChat.tsx
@@ -145,6 +145,8 @@ function BaseChatContent({
setAncestorMessages,
append,
isLoading,
+ isWaiting,
+ isStreaming,
error,
setMessages,
input: _input,
@@ -482,7 +484,11 @@ function BaseChatContent({
{/* Fixed loading indicator at bottom left of chat container */}
{isLoading && (
-
+
)}
diff --git a/ui/desktop/src/components/FlyingBird.tsx b/ui/desktop/src/components/FlyingBird.tsx
new file mode 100644
index 000000000000..93baa3f5be6c
--- /dev/null
+++ b/ui/desktop/src/components/FlyingBird.tsx
@@ -0,0 +1,41 @@
+import { useState, useEffect } from 'react';
+import { Bird1, Bird2, Bird3, Bird4, Bird5, Bird6 } from './icons';
+
+interface FlyingBirdProps {
+ className?: string;
+ cycleInterval?: number; // milliseconds between bird frame changes
+}
+
+const birdFrames = [
+ Bird1,
+ Bird2,
+ Bird3,
+ Bird4,
+ Bird5,
+ Bird6,
+];
+
+export default function FlyingBird({
+ className = '',
+ cycleInterval = 150
+}: FlyingBirdProps) {
+ const [currentFrameIndex, setCurrentFrameIndex] = useState(0);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setCurrentFrameIndex((prevIndex) =>
+ (prevIndex + 1) % birdFrames.length
+ );
+ }, cycleInterval);
+
+ return () => clearInterval(interval);
+ }, [cycleInterval]);
+
+ const CurrentFrame = birdFrames[currentFrameIndex];
+
+ return (
+
+
+
+ );
+}
diff --git a/ui/desktop/src/components/LoadingGoose.tsx b/ui/desktop/src/components/LoadingGoose.tsx
index a5f6fd80019e..e84a4efc7030 100644
--- a/ui/desktop/src/components/LoadingGoose.tsx
+++ b/ui/desktop/src/components/LoadingGoose.tsx
@@ -1,18 +1,43 @@
import GooseLogo from './GooseLogo';
+import ThinkingIcons from './ThinkingIcons';
+import FlyingBird from './FlyingBird';
interface LoadingGooseProps {
message?: string;
+ isWaiting?: boolean;
+ isStreaming?: boolean;
}
-const LoadingGoose = ({ message = 'goose is working on it…' }: LoadingGooseProps) => {
+const LoadingGoose = ({
+ message,
+ isWaiting = false,
+ isStreaming = false
+}: LoadingGooseProps) => {
+ // Determine the appropriate message based on state
+ const getLoadingMessage = () => {
+ if (message) return message; // Custom message takes priority
+
+ if (isWaiting) return 'goose is thinking…';
+ if (isStreaming) return 'goose is working on it…';
+
+ // Default fallback
+ return 'goose is working on it…';
+ };
+
return (
-
- {message}
+ {isWaiting ? (
+
+ ) : isStreaming ? (
+
+ ) : (
+
+ )}
+ {getLoadingMessage()}
);
diff --git a/ui/desktop/src/components/ThinkingIcons.tsx b/ui/desktop/src/components/ThinkingIcons.tsx
new file mode 100644
index 000000000000..c7affaaa8255
--- /dev/null
+++ b/ui/desktop/src/components/ThinkingIcons.tsx
@@ -0,0 +1,42 @@
+import { useState, useEffect } from 'react';
+import { CodeXml, Cog, Fuel, GalleryHorizontalEnd, Gavel, GlassWater, Grape } from './icons';
+
+interface ThinkingIconsProps {
+ className?: string;
+ cycleInterval?: number; // milliseconds between icon changes
+}
+
+const thinkingIcons = [
+ CodeXml,
+ Cog,
+ Fuel,
+ GalleryHorizontalEnd,
+ Gavel,
+ GlassWater,
+ Grape,
+];
+
+export default function ThinkingIcons({
+ className = '',
+ cycleInterval = 500
+}: ThinkingIconsProps) {
+ const [currentIconIndex, setCurrentIconIndex] = useState(0);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setCurrentIconIndex((prevIndex) =>
+ (prevIndex + 1) % thinkingIcons.length
+ );
+ }, cycleInterval);
+
+ return () => clearInterval(interval);
+ }, [cycleInterval]);
+
+ const CurrentIcon = thinkingIcons[currentIconIndex];
+
+ return (
+
+
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/Bird1.tsx b/ui/desktop/src/components/icons/Bird1.tsx
new file mode 100644
index 000000000000..bcb7b433f456
--- /dev/null
+++ b/ui/desktop/src/components/icons/Bird1.tsx
@@ -0,0 +1,31 @@
+export function Bird1({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/Bird2.tsx b/ui/desktop/src/components/icons/Bird2.tsx
new file mode 100644
index 000000000000..130d18f5c0eb
--- /dev/null
+++ b/ui/desktop/src/components/icons/Bird2.tsx
@@ -0,0 +1,29 @@
+export function Bird2({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/Bird3.tsx b/ui/desktop/src/components/icons/Bird3.tsx
new file mode 100644
index 000000000000..6642e575c1f2
--- /dev/null
+++ b/ui/desktop/src/components/icons/Bird3.tsx
@@ -0,0 +1,31 @@
+export function Bird3({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/Bird4.tsx b/ui/desktop/src/components/icons/Bird4.tsx
new file mode 100644
index 000000000000..cf8b8f9a8d66
--- /dev/null
+++ b/ui/desktop/src/components/icons/Bird4.tsx
@@ -0,0 +1,31 @@
+export function Bird4({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/Bird5.tsx b/ui/desktop/src/components/icons/Bird5.tsx
new file mode 100644
index 000000000000..ecdadabcb10f
--- /dev/null
+++ b/ui/desktop/src/components/icons/Bird5.tsx
@@ -0,0 +1,25 @@
+export function Bird5({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/Bird6.tsx b/ui/desktop/src/components/icons/Bird6.tsx
new file mode 100644
index 000000000000..db1471509c16
--- /dev/null
+++ b/ui/desktop/src/components/icons/Bird6.tsx
@@ -0,0 +1,31 @@
+export function Bird6({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/CodeXml.tsx b/ui/desktop/src/components/icons/CodeXml.tsx
new file mode 100644
index 000000000000..470a90ee6e17
--- /dev/null
+++ b/ui/desktop/src/components/icons/CodeXml.tsx
@@ -0,0 +1,26 @@
+export function CodeXml({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/Cog.tsx b/ui/desktop/src/components/icons/Cog.tsx
new file mode 100644
index 000000000000..9e91ae42c4d6
--- /dev/null
+++ b/ui/desktop/src/components/icons/Cog.tsx
@@ -0,0 +1,26 @@
+export function Cog({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/Fuel.tsx b/ui/desktop/src/components/icons/Fuel.tsx
new file mode 100644
index 000000000000..ff8b380ffbad
--- /dev/null
+++ b/ui/desktop/src/components/icons/Fuel.tsx
@@ -0,0 +1,26 @@
+export function Fuel({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/GalleryHorizontalEnd.tsx b/ui/desktop/src/components/icons/GalleryHorizontalEnd.tsx
new file mode 100644
index 000000000000..4f036b3866eb
--- /dev/null
+++ b/ui/desktop/src/components/icons/GalleryHorizontalEnd.tsx
@@ -0,0 +1,26 @@
+export function GalleryHorizontalEnd({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/Gavel.tsx b/ui/desktop/src/components/icons/Gavel.tsx
new file mode 100644
index 000000000000..799095576513
--- /dev/null
+++ b/ui/desktop/src/components/icons/Gavel.tsx
@@ -0,0 +1,26 @@
+export function Gavel({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/GlassWater.tsx b/ui/desktop/src/components/icons/GlassWater.tsx
new file mode 100644
index 000000000000..5edef50dec77
--- /dev/null
+++ b/ui/desktop/src/components/icons/GlassWater.tsx
@@ -0,0 +1,19 @@
+export function GlassWater({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/Grape.tsx b/ui/desktop/src/components/icons/Grape.tsx
new file mode 100644
index 000000000000..4e3c21c38a93
--- /dev/null
+++ b/ui/desktop/src/components/icons/Grape.tsx
@@ -0,0 +1,26 @@
+export function Grape({ className = '' }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/ui/desktop/src/components/icons/index.tsx b/ui/desktop/src/components/icons/index.tsx
index dab2ee5428b9..2760c2dc8692 100644
--- a/ui/desktop/src/components/icons/index.tsx
+++ b/ui/desktop/src/components/icons/index.tsx
@@ -2,17 +2,30 @@ import ArrowDown from './ArrowDown';
import ArrowUp from './ArrowUp';
import Attach from './Attach';
import Back from './Back';
+import { Bird1 } from './Bird1';
+import { Bird2 } from './Bird2';
+import { Bird3 } from './Bird3';
+import { Bird4 } from './Bird4';
+import { Bird5 } from './Bird5';
+import { Bird6 } from './Bird6';
import ChatSmart from './ChatSmart';
import Check from './Check';
import ChevronDown from './ChevronDown';
import ChevronUp from './ChevronUp';
import { ChevronRight } from './ChevronRight';
import Close from './Close';
+import { CodeXml } from './CodeXml';
+import { Cog } from './Cog';
import CoinIcon from './CoinIcon';
import Copy from './Copy';
import Discord from './Discord';
import Document from './Document';
import Edit from './Edit';
+import { Fuel } from './Fuel';
+import { GalleryHorizontalEnd } from './GalleryHorizontalEnd';
+import { Gavel } from './Gavel';
+import { GlassWater } from './GlassWater';
+import { Grape } from './Grape';
import Idea from './Idea';
import LinkedIn from './LinkedIn';
import More from './More';
@@ -31,21 +44,34 @@ export {
ArrowUp,
Attach,
Back,
+ Bird1,
+ Bird2,
+ Bird3,
+ Bird4,
+ Bird5,
+ Bird6,
ChatSmart,
Check,
ChevronDown,
ChevronRight,
ChevronUp,
Close,
+ CodeXml,
+ Cog,
CoinIcon,
Copy,
Discord,
Document,
Edit,
- Idea,
+ Fuel,
+ GalleryHorizontalEnd,
+ Gavel,
Gear,
- Microphone,
+ GlassWater,
+ Grape,
+ Idea,
LinkedIn,
+ Microphone,
More,
Refresh,
SensitiveHidden,
diff --git a/ui/desktop/src/hooks/useChatEngine.ts b/ui/desktop/src/hooks/useChatEngine.ts
index 4f2613abe78a..6706e523d065 100644
--- a/ui/desktop/src/hooks/useChatEngine.ts
+++ b/ui/desktop/src/hooks/useChatEngine.ts
@@ -68,6 +68,8 @@ export const useChatEngine = ({
append: originalAppend,
stop,
isLoading,
+ isWaiting,
+ isStreaming,
error,
setMessages,
input: _input,
@@ -369,6 +371,8 @@ export const useChatEngine = ({
append,
stop,
isLoading,
+ isWaiting,
+ isStreaming,
error,
setMessages,
diff --git a/ui/desktop/src/hooks/useMessageStream.ts b/ui/desktop/src/hooks/useMessageStream.ts
index e4db0ac4372e..50807cf54c01 100644
--- a/ui/desktop/src/hooks/useMessageStream.ts
+++ b/ui/desktop/src/hooks/useMessageStream.ts
@@ -148,6 +148,12 @@ export interface UseMessageStreamHelpers {
/** Whether the API request is in progress */
isLoading: boolean;
+ /** Whether we're waiting for the first response from LLM */
+ isWaiting: boolean;
+
+ /** Whether we're actively streaming response content */
+ isStreaming: boolean;
+
/** Add a tool result to a tool call */
addToolResult: ({ toolCallId, result }: { toolCallId: string; result: unknown }) => void;
@@ -214,6 +220,17 @@ export function useMessageStream({
null
);
+ // Track waiting vs streaming states
+ const { data: isWaiting = false, mutate: mutateWaiting } = useSWR(
+ [chatKey, 'waiting'],
+ null
+ );
+
+ const { data: isStreaming = false, mutate: mutateStreaming } = useSWR(
+ [chatKey, 'streaming'],
+ null
+ );
+
const { data: error = undefined, mutate: setError } = useSWR(
[chatKey, 'error'],
null
@@ -273,6 +290,10 @@ export function useMessageStream({
switch (parsedEvent.type) {
case 'Message': {
+ // Transition from waiting to streaming on first message
+ mutateWaiting(false);
+ mutateStreaming(true);
+
// Create a new message object with the properties preserved or defaulted
const newMessage = {
...parsedEvent.message,
@@ -432,7 +453,7 @@ export function useMessageStream({
return currentMessages;
},
- [mutate, onFinish, onError, forceUpdate, setError]
+ [mutate, mutateWaiting, mutateStreaming, onFinish, onError, forceUpdate, setError]
);
// Send a request to the server
@@ -440,6 +461,8 @@ export function useMessageStream({
async (requestMessages: Message[]) => {
try {
mutateLoading(true);
+ mutateWaiting(true); // Start in waiting state
+ mutateStreaming(false);
setError(undefined);
// Create abort controller
@@ -511,10 +534,12 @@ export function useMessageStream({
setError(err as Error);
} finally {
mutateLoading(false);
+ mutateWaiting(false);
+ mutateStreaming(false);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
- [api, processMessageStream, mutateLoading, setError, onResponse, onError, maxSteps]
+ [api, processMessageStream, mutateLoading, mutateWaiting, mutateStreaming, setError, onResponse, onError, maxSteps]
);
// Append a new message and send request
@@ -658,6 +683,8 @@ export function useMessageStream({
handleInputChange,
handleSubmit,
isLoading: isLoading || false,
+ isWaiting: isWaiting || false,
+ isStreaming: isStreaming || false,
addToolResult,
updateMessageStreamBody,
notifications,