Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
58 changes: 10 additions & 48 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import LauncherView from './components/LauncherView';
import 'react-toastify/dist/ReactToastify.css';
import { useConfig } from './components/ConfigContext';
import { ModelAndProviderProvider } from './components/ModelAndProviderContext';
import { ThemeProvider } from './contexts/ThemeContext';
import PermissionSettingsView from './components/settings/permission/PermissionSetting';

import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView';
Expand Down Expand Up @@ -547,47 +548,6 @@ export function AppInner() {
};
}, []);

useEffect(() => {
if (!window.electron) return;

const handleThemeChanged = (_event: unknown, ...args: unknown[]) => {
const themeData = args[0] as { mode: string; useSystemTheme: boolean; theme: string };

if (themeData.useSystemTheme) {
localStorage.setItem('use_system_theme', 'true');
} else {
localStorage.setItem('use_system_theme', 'false');
localStorage.setItem('theme', themeData.theme);
}

const isDark = themeData.useSystemTheme
? window.matchMedia('(prefers-color-scheme: dark)').matches
: themeData.mode === 'dark';

if (isDark) {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('light');
} else {
document.documentElement.classList.remove('dark');
document.documentElement.classList.add('light');
}

const storageEvent = new Event('storage') as Event & {
key: string | null;
newValue: string | null;
};
storageEvent.key = themeData.useSystemTheme ? 'use_system_theme' : 'theme';
storageEvent.newValue = themeData.useSystemTheme ? 'true' : themeData.theme;
window.dispatchEvent(storageEvent);
};

window.electron.on('theme-changed', handleThemeChanged);

return () => {
window.electron.off('theme-changed', handleThemeChanged);
};
}, []);

// Handle initial message from launcher
useEffect(() => {
const handleSetInitialMessage = (_event: IpcRendererEvent, ...args: unknown[]) => {
Expand Down Expand Up @@ -698,12 +658,14 @@ export function AppInner() {

export default function App() {
return (
<ModelAndProviderProvider>
<HashRouter>
<AppInner />
</HashRouter>
<AnnouncementModal />
<TelemetryOptOutModal controlled={false} />
</ModelAndProviderProvider>
<ThemeProvider>
<ModelAndProviderProvider>
<HashRouter>
<AppInner />
</HashRouter>
<AnnouncementModal />
<TelemetryOptOutModal controlled={false} />
</ModelAndProviderProvider>
</ThemeProvider>
);
}
103 changes: 9 additions & 94 deletions ui/desktop/src/components/GooseSidebar/ThemeSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,105 +1,20 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import { Moon, Sliders, Sun } from 'lucide-react';
import { Button } from '../ui/button';
import { useTheme } from '../../contexts/ThemeContext';

interface ThemeSelectorProps {
className?: string;
hideTitle?: boolean;
horizontal?: boolean;
}

const getIsDarkMode = (mode: 'light' | 'dark' | 'system'): boolean => {
if (mode === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return mode === 'dark';
};

const getThemeMode = (): 'light' | 'dark' | 'system' => {
const savedUseSystemTheme = localStorage.getItem('use_system_theme');
if (savedUseSystemTheme === 'true') {
return 'system';
}

const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
return savedTheme === 'dark' ? 'dark' : 'light';
}

return getIsDarkMode('system') ? 'dark' : 'light';
};

const setThemeModeStorage = (mode: 'light' | 'dark' | 'system') => {
if (mode === 'system') {
localStorage.setItem('use_system_theme', 'true');
} else {
localStorage.setItem('use_system_theme', 'false');
localStorage.setItem('theme', mode);
}

const themeData = {
mode,
useSystemTheme: mode === 'system',
theme: mode === 'system' ? '' : mode,
};

window.electron?.broadcastThemeChange(themeData);
};

const ThemeSelector: React.FC<ThemeSelectorProps> = ({
className = '',
hideTitle = false,
horizontal = false,
}) => {
const [themeMode, setThemeMode] = useState<'light' | 'dark' | 'system'>(getThemeMode);
const [isDarkMode, setDarkMode] = useState(() => getIsDarkMode(getThemeMode()));

useEffect(() => {
const handleStorageChange = (e: { key: string | null; newValue: string | null }) => {
if (e.key === 'use_system_theme' || e.key === 'theme') {
const newThemeMode = getThemeMode();
setThemeMode(newThemeMode);
setDarkMode(getIsDarkMode(newThemeMode));
}
};

window.addEventListener('storage', handleStorageChange);

return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []);

useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

const handleThemeChange = (e: { matches: boolean }) => {
if (themeMode === 'system') {
setDarkMode(e.matches);
}
};

mediaQuery.addEventListener('change', handleThemeChange);

setThemeModeStorage(themeMode);
setDarkMode(getIsDarkMode(themeMode));

return () => mediaQuery.removeEventListener('change', handleThemeChange);
}, [themeMode]);

useEffect(() => {
if (isDarkMode) {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('light');
} else {
document.documentElement.classList.remove('dark');
document.documentElement.classList.add('light');
}
}, [isDarkMode]);

const handleThemeChange = (newTheme: 'light' | 'dark' | 'system') => {
setThemeMode(newTheme);
};
const { userThemePreference, setUserThemePreference } = useTheme();

return (
<div className={`${!horizontal ? 'px-1 py-2 space-y-2' : ''} ${className}`}>
Expand All @@ -109,9 +24,9 @@ const ThemeSelector: React.FC<ThemeSelectorProps> = ({
>
<Button
data-testid="light-mode-button"
onClick={() => handleThemeChange('light')}
onClick={() => setUserThemePreference('light')}
className={`flex items-center justify-center gap-1 p-2 rounded-md border transition-colors text-xs ${
themeMode === 'light'
userThemePreference === 'light'
? 'bg-background-accent text-text-on-accent border-border-accent hover:!bg-background-accent hover:!text-text-on-accent'
: 'border-border-default hover:!bg-background-muted text-text-muted hover:text-text-default'
}`}
Expand All @@ -124,9 +39,9 @@ const ThemeSelector: React.FC<ThemeSelectorProps> = ({

<Button
data-testid="dark-mode-button"
onClick={() => handleThemeChange('dark')}
onClick={() => setUserThemePreference('dark')}
className={`flex items-center justify-center gap-1 p-2 rounded-md border transition-colors text-xs ${
themeMode === 'dark'
userThemePreference === 'dark'
? 'bg-background-accent text-text-on-accent border-border-accent hover:!bg-background-accent hover:!text-text-on-accent'
: 'border-border-default hover:!bg-background-muted text-text-muted hover:text-text-default'
}`}
Expand All @@ -139,9 +54,9 @@ const ThemeSelector: React.FC<ThemeSelectorProps> = ({

<Button
data-testid="system-mode-button"
onClick={() => handleThemeChange('system')}
onClick={() => setUserThemePreference('system')}
className={`flex items-center justify-center gap-1 p-2 rounded-md border transition-colors text-xs ${
themeMode === 'system'
userThemePreference === 'system'
? 'bg-background-accent text-text-on-accent border-border-accent hover:!bg-background-accent hover:!text-text-on-accent'
: 'border-border-default hover:!bg-background-muted text-text-muted hover:text-text-default'
}`}
Expand Down
14 changes: 6 additions & 8 deletions ui/desktop/src/components/MCPUIResourceRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
import { EmbeddedResource } from '../api';
import { useTheme } from '../contexts/ThemeContext';

interface MCPUIResourceRendererProps {
content: EmbeddedResource & { type: 'resource' };
Expand Down Expand Up @@ -91,13 +92,10 @@ export default function MCPUIResourceRenderer({
content,
appendPromptToChat,
}: MCPUIResourceRendererProps) {
const [currentThemeValue, setCurrentThemeValue] = useState<string>('light');
const { resolvedTheme } = useTheme();
const [proxyUrl, setProxyUrl] = useState<string | undefined>(undefined);

useEffect(() => {
const theme = localStorage.getItem('theme') || 'light';
setCurrentThemeValue(theme);

const fetchProxyUrl = async () => {
try {
const gooseApiHost = await window.electron.getGoosedHostPort();
Expand All @@ -124,7 +122,7 @@ export default function MCPUIResourceRenderer({
): Promise<UIActionHandlerResult> => {
const { toolName, params } = actionEvent.payload;
toast.info(<ToastComponent messageType="tool" message={toolName} isImplemented={false} />, {
theme: currentThemeValue,
theme: resolvedTheme,
});
return {
status: 'error' as const,
Expand Down Expand Up @@ -232,7 +230,7 @@ export default function MCPUIResourceRenderer({
const { message } = actionEvent.payload;

toast.info(<ToastComponent messageType="notify" message={message} isImplemented={true} />, {
theme: currentThemeValue,
theme: resolvedTheme,
});
return {
status: 'success' as const,
Expand All @@ -254,7 +252,7 @@ export default function MCPUIResourceRenderer({
isImplemented={false}
/>,
{
theme: currentThemeValue,
theme: resolvedTheme,
}
);
return {
Expand Down Expand Up @@ -334,7 +332,7 @@ export default function MCPUIResourceRenderer({
// MCP-UIs might find stuff like host and theme for conditional rendering
// usage of this is experimental, leaving in place for demos
host: 'goose',
theme: currentThemeValue,
theme: resolvedTheme,
},
proxy: proxyUrl, // refer to https://mcpui.dev/guide/client/using-a-proxy
}}
Expand Down
Loading
Loading