diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx
index 2e9b0655fba9..9947fdbbaeab 100644
--- a/ui/desktop/src/App.tsx
+++ b/ui/desktop/src/App.tsx
@@ -39,6 +39,7 @@ import 'react-toastify/dist/ReactToastify.css';
import { useConfig } from './components/ConfigContext';
import { ModelAndProviderProvider } from './components/ModelAndProviderContext';
import { ThemeProvider } from './contexts/ThemeContext';
+import { GlyphProvider } from './contexts/GlyphContext';
import PermissionSettingsView from './components/settings/permission/PermissionSetting';
import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView';
@@ -702,13 +703,15 @@ export function AppInner() {
export default function App() {
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
}
diff --git a/ui/desktop/src/components/FlyingBird.tsx b/ui/desktop/src/components/FlyingBird.tsx
index d4c8c55ecf5e..18a3d71437fa 100644
--- a/ui/desktop/src/components/FlyingBird.tsx
+++ b/ui/desktop/src/components/FlyingBird.tsx
@@ -1,29 +1,37 @@
import { useState, useEffect } from 'react';
-import { Bird1, Bird2, Bird3, Bird4, Bird5, Bird6 } from './icons';
+import { useGlyphPack } from '../contexts/GlyphContext';
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 { pack } = useGlyphPack();
+ const frames = pack.AnimationFrames;
const [currentFrameIndex, setCurrentFrameIndex] = useState(0);
useEffect(() => {
+ if (!frames) return;
const interval = setInterval(() => {
- setCurrentFrameIndex((prevIndex) => (prevIndex + 1) % birdFrames.length);
+ setCurrentFrameIndex((prevIndex) => (prevIndex + 1) % frames.length);
}, cycleInterval);
-
return () => clearInterval(interval);
- }, [cycleInterval]);
+ }, [cycleInterval, frames]);
- const CurrentFrame = birdFrames[currentFrameIndex];
+ if (frames) {
+ const CurrentFrame = frames[currentFrameIndex];
+ return (
+
+
+
+ );
+ }
+ // Fallback: static glyph for packs without frame animation
return (
);
}
diff --git a/ui/desktop/src/components/GooseLogo.tsx b/ui/desktop/src/components/GooseLogo.tsx
index b82a0384986e..4a3515ef50bb 100644
--- a/ui/desktop/src/components/GooseLogo.tsx
+++ b/ui/desktop/src/components/GooseLogo.tsx
@@ -1,5 +1,6 @@
-import { Goose, Rain } from './icons/Goose';
+import { Rain } from './icons/Goose';
import { cn } from '../utils';
+import { useGlyphPack } from '../contexts/GlyphContext';
interface GooseLogoProps {
className?: string;
@@ -12,6 +13,8 @@ export default function GooseLogo({
size = 'default',
hover = true,
}: GooseLogoProps) {
+ const { pack } = useGlyphPack();
+
const sizes = {
default: {
frame: 'w-16 h-16',
@@ -43,7 +46,7 @@ export default function GooseLogo({
hover && 'opacity-0 group-hover/with-hover:opacity-100'
)}
/>
-
+
);
}
diff --git a/ui/desktop/src/components/WelcomeGooseLogo.tsx b/ui/desktop/src/components/WelcomeGooseLogo.tsx
index b47ab9fa376f..3063bfe34eb0 100644
--- a/ui/desktop/src/components/WelcomeGooseLogo.tsx
+++ b/ui/desktop/src/components/WelcomeGooseLogo.tsx
@@ -1,13 +1,16 @@
-import { Goose, Rain } from './icons/Goose';
+import { Rain } from './icons/Goose';
+import { useGlyphPack } from '../contexts/GlyphContext';
export default function WelcomeGooseLogo({ className = '' }) {
+ const { pack } = useGlyphPack();
+
return (
);
diff --git a/ui/desktop/src/contexts/GlyphContext.tsx b/ui/desktop/src/contexts/GlyphContext.tsx
new file mode 100644
index 000000000000..cf62cc4b1f90
--- /dev/null
+++ b/ui/desktop/src/contexts/GlyphContext.tsx
@@ -0,0 +1,41 @@
+import React, { createContext, useContext, useState, useCallback } from 'react';
+import type { GlyphPack } from '../packs/types';
+import { goosePack } from '../packs/goose';
+import { getPackById } from '../packs/registry';
+
+interface GlyphContextValue {
+ pack: GlyphPack;
+ setPackId: (id: string) => void;
+}
+
+const GlyphContext = createContext({
+ pack: goosePack,
+ setPackId: () => {},
+});
+
+export function useGlyphPack(): GlyphContextValue {
+ return useContext(GlyphContext);
+}
+
+interface GlyphProviderProps {
+ children: React.ReactNode;
+}
+
+export function GlyphProvider({ children }: GlyphProviderProps) {
+ const [packId, setPackIdState] = useState(
+ () => localStorage.getItem('glyphPack') || 'goose'
+ );
+
+ const pack = getPackById(packId) ?? goosePack;
+
+ const setPackId = useCallback((id: string) => {
+ localStorage.setItem('glyphPack', id);
+ setPackIdState(id);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/ui/desktop/src/packs/goose.ts b/ui/desktop/src/packs/goose.ts
new file mode 100644
index 000000000000..5b329d5070a4
--- /dev/null
+++ b/ui/desktop/src/packs/goose.ts
@@ -0,0 +1,17 @@
+import { Goose } from '../components/icons/Goose';
+import { Bird1 } from '../components/icons/Bird1';
+import { Bird2 } from '../components/icons/Bird2';
+import { Bird3 } from '../components/icons/Bird3';
+import { Bird4 } from '../components/icons/Bird4';
+import { Bird5 } from '../components/icons/Bird5';
+import { Bird6 } from '../components/icons/Bird6';
+import type { GlyphPack } from './types';
+
+export const goosePack: GlyphPack = {
+ id: 'goose',
+ name: 'Goose',
+ emoji: '🪿',
+ description: 'The original.',
+ StaticGlyph: Goose,
+ AnimationFrames: [Bird1, Bird2, Bird3, Bird4, Bird5, Bird6],
+};
diff --git a/ui/desktop/src/packs/registry.ts b/ui/desktop/src/packs/registry.ts
new file mode 100644
index 000000000000..5af01baad417
--- /dev/null
+++ b/ui/desktop/src/packs/registry.ts
@@ -0,0 +1,10 @@
+import type { GlyphPack } from './types';
+import { goosePack } from './goose';
+
+export const allPacks: GlyphPack[] = [goosePack];
+
+const packMap = new Map(allPacks.map((p) => [p.id, p]));
+
+export function getPackById(id: string): GlyphPack | undefined {
+ return packMap.get(id);
+}
diff --git a/ui/desktop/src/packs/types.ts b/ui/desktop/src/packs/types.ts
new file mode 100644
index 000000000000..9965be50f952
--- /dev/null
+++ b/ui/desktop/src/packs/types.ts
@@ -0,0 +1,17 @@
+import type { ComponentType } from 'react';
+
+/** Props accepted by all glyph components — matches existing icon conventions */
+export interface GlyphProps {
+ className?: string;
+}
+
+export interface GlyphPack {
+ id: string;
+ name: string;
+ emoji: string;
+ description: string;
+ /** Static icon used in sidebar logo, welcome screen, etc. */
+ StaticGlyph: ComponentType;
+ /** Frame-based animation components (goose uses Bird1–6). If absent, StaticGlyph is used. */
+ AnimationFrames?: ComponentType[];
+}