diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index cb238473..110add25 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -14,7 +14,7 @@ import { ErrorUI } from './components/ErrorBoundary'; import { ExtensionInstallModal } from './components/ExtensionInstallModal'; import { ToastContainer } from 'react-toastify'; import AnnouncementModal from './components/AnnouncementModal'; -import TelemetryOptOutModal from './components/TelemetryOptOutModal'; +import TelemetryConsentPrompt from './components/TelemetryConsentPrompt'; import OnboardingGuard from './components/onboarding/OnboardingGuard'; import { createSession } from './sessions'; @@ -678,7 +678,7 @@ export default function App() { - + diff --git a/ui/desktop/src/components/TelemetryConsentPrompt.tsx b/ui/desktop/src/components/TelemetryConsentPrompt.tsx new file mode 100644 index 00000000..56128077 --- /dev/null +++ b/ui/desktop/src/components/TelemetryConsentPrompt.tsx @@ -0,0 +1,124 @@ +import { useState, useEffect } from 'react'; +import { Button } from './ui/button'; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog'; +import { TELEMETRY_UI_ENABLED } from '../updates'; +import { useConfig } from './ConfigContext'; +import { + trackTelemetryPreference, + setTelemetryEnabled as setAnalyticsTelemetryEnabled, +} from '../utils/analytics'; +import PrivacyInfoModal from './onboarding/PrivacyInfoModal'; +import { defineMessages, useIntl } from '../i18n'; + +const i18n = defineMessages({ + heading: { + id: 'telemetryConsentPrompt.heading', + defaultMessage: 'Help improve goose', + }, + description: { + id: 'telemetryConsentPrompt.description', + defaultMessage: + 'Would you like to share anonymous usage data to help improve goose? We never collect your conversations, code, or personal data.', + }, + learnMore: { + id: 'telemetryConsentPrompt.learnMore', + defaultMessage: 'Learn more', + }, + optIn: { + id: 'telemetryConsentPrompt.optIn', + defaultMessage: 'Yes, share anonymous usage data', + }, + optOut: { + id: 'telemetryConsentPrompt.optOut', + defaultMessage: 'No thanks', + }, +}); + +const TELEMETRY_CONFIG_KEY = 'GOOSE_TELEMETRY_ENABLED'; + +export default function TelemetryConsentPrompt() { + const intl = useIntl(); + const { read, upsert } = useConfig(); + const [showPrompt, setShowPrompt] = useState(false); + const [showPrivacyInfo, setShowPrivacyInfo] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (!TELEMETRY_UI_ENABLED) return; + + (async () => { + try { + const provider = await read('GOOSE_PROVIDER', false); + if (!provider || provider === '') return; + + const telemetryValue = await read(TELEMETRY_CONFIG_KEY, false); + if (telemetryValue === null) { + setShowPrompt(true); + } + } catch (error) { + console.error('Failed to check telemetry config:', error); + } + })(); + }, [read]); + + const handleChoice = async (enabled: boolean) => { + setIsSubmitting(true); + try { + await upsert(TELEMETRY_CONFIG_KEY, enabled, false); + trackTelemetryPreference(enabled, 'modal'); + setAnalyticsTelemetryEnabled(enabled); + } catch (error) { + console.error('Failed to save telemetry preference:', error); + } finally { + setShowPrompt(false); + setIsSubmitting(false); + } + }; + + if (!showPrompt) return null; + + return ( + <> + { + if (!open) setShowPrompt(false); + }} + > + + + {intl.formatMessage(i18n.heading)} + +

+ {intl.formatMessage(i18n.description)}{' '} + +

+ + + + +
+
+ setShowPrivacyInfo(false)} /> + + ); +} diff --git a/ui/desktop/src/components/TelemetryOptOutModal.tsx b/ui/desktop/src/components/TelemetryOptOutModal.tsx deleted file mode 100644 index 5e2adcd6..00000000 --- a/ui/desktop/src/components/TelemetryOptOutModal.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { useState, useEffect } from 'react'; -import { BaseModal } from './ui/BaseModal'; -import { Button } from './ui/button'; -import { Goose } from './icons/Goose'; -import { TELEMETRY_UI_ENABLED } from '../updates'; -import { toastService } from '../toasts'; -import { useConfig } from './ConfigContext'; -import { trackTelemetryPreference } from '../utils/analytics'; -import { defineMessages, useIntl } from '../i18n'; - -const i18n = defineMessages({ - configError: { - id: 'telemetryOptOutModal.configError', - defaultMessage: 'Configuration Error', - }, - configErrorMessage: { - id: 'telemetryOptOutModal.configErrorMessage', - defaultMessage: 'Failed to check telemetry configuration.', - }, - optIn: { - id: 'telemetryOptOutModal.optIn', - defaultMessage: 'Yes, share anonymous usage data', - }, - optOut: { - id: 'telemetryOptOutModal.optOut', - defaultMessage: 'No thanks', - }, - heading: { - id: 'telemetryOptOutModal.heading', - defaultMessage: 'Help improve goose', - }, - description: { - id: 'telemetryOptOutModal.description', - defaultMessage: - 'Would you like to help improve goose by sharing anonymous usage data? This helps us understand how goose is used and identify areas for improvement.', - }, - whatWeCollect: { - id: 'telemetryOptOutModal.whatWeCollect', - defaultMessage: 'What we collect:', - }, - collectOs: { - id: 'telemetryOptOutModal.collectOs', - defaultMessage: 'Operating system, version, and architecture', - }, - collectVersion: { - id: 'telemetryOptOutModal.collectVersion', - defaultMessage: 'goose version and install method', - }, - collectProvider: { - id: 'telemetryOptOutModal.collectProvider', - defaultMessage: 'Provider and model used', - }, - collectExtensions: { - id: 'telemetryOptOutModal.collectExtensions', - defaultMessage: 'Extensions and tool usage counts (names only)', - }, - collectSession: { - id: 'telemetryOptOutModal.collectSession', - defaultMessage: 'Session metrics (duration, interaction count, token usage)', - }, - collectErrors: { - id: 'telemetryOptOutModal.collectErrors', - defaultMessage: 'Error types (e.g., "rate_limit", "auth" - no details)', - }, - privacyNote: { - id: 'telemetryOptOutModal.privacyNote', - defaultMessage: - 'We never collect your conversations, code, tool arguments, error messages, or any personal data. You can change this setting anytime in Settings → App.', - }, -}); - -const TELEMETRY_CONFIG_KEY = 'GOOSE_TELEMETRY_ENABLED'; - -type TelemetryOptOutModalProps = - | { controlled: false } - | { controlled: true; isOpen: boolean; onClose: () => void }; - -export default function TelemetryOptOutModal(props: TelemetryOptOutModalProps) { - const intl = useIntl(); - const { read, upsert } = useConfig(); - const isControlled = props.controlled; - const controlledIsOpen = isControlled ? props.isOpen : undefined; - const onClose = isControlled ? props.onClose : undefined; - const [showModal, setShowModal] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - // Only check telemetry choice on first launch in uncontrolled mode - useEffect(() => { - if (isControlled) return; - - const checkTelemetryChoice = async () => { - try { - const provider = await read('GOOSE_PROVIDER', false); - - if (!provider || provider === '') { - return; - } - - const telemetryEnabled = await read(TELEMETRY_CONFIG_KEY, false); - - if (telemetryEnabled === null) { - setShowModal(true); - } - } catch (error) { - console.error('Failed to check telemetry config:', error); - toastService.error({ - title: intl.formatMessage(i18n.configError), - msg: intl.formatMessage(i18n.configErrorMessage), - traceback: error instanceof Error ? error.stack || '' : '', - }); - } - }; - - checkTelemetryChoice(); - }, [isControlled, read, intl]); - - const handleChoice = async (enabled: boolean) => { - setIsLoading(true); - try { - await upsert(TELEMETRY_CONFIG_KEY, enabled, false); - trackTelemetryPreference(enabled, 'modal'); - setShowModal(false); - onClose?.(); - } catch (error) { - console.error('Failed to set telemetry preference:', error); - setShowModal(false); - onClose?.(); - } finally { - setIsLoading(false); - } - }; - - if (!TELEMETRY_UI_ENABLED) { - return null; - } - - const isModalOpen = controlledIsOpen !== undefined ? controlledIsOpen : showModal; - - if (!isModalOpen) { - return null; - } - - return ( - - - - - } - > -
-
- -
-

- {intl.formatMessage(i18n.heading)} -

-

- {intl.formatMessage(i18n.description)} -

-
-

{intl.formatMessage(i18n.whatWeCollect)}

-
    -
  • {intl.formatMessage(i18n.collectOs)}
  • -
  • {intl.formatMessage(i18n.collectVersion)}
  • -
  • {intl.formatMessage(i18n.collectProvider)}
  • -
  • {intl.formatMessage(i18n.collectExtensions)}
  • -
  • {intl.formatMessage(i18n.collectSession)}
  • -
  • {intl.formatMessage(i18n.collectErrors)}
  • -
-

- {intl.formatMessage(i18n.privacyNote)} -

-
-
-
- ); -} diff --git a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx index 4d64898d..a3c689a7 100644 --- a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx +++ b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx @@ -21,45 +21,106 @@ import { NavigationProvider, useNavigationContextSafe } from '../../Layout/Navig const i18n = defineMessages({ appearanceTitle: { id: 'settings.appearance.title', defaultMessage: 'Appearance' }, - appearanceDesc: { id: 'settings.appearance.description', defaultMessage: 'Configure how goose appears on your system' }, + appearanceDesc: { + id: 'settings.appearance.description', + defaultMessage: 'Configure how goose appears on your system', + }, notifications: { id: 'settings.notifications.title', defaultMessage: 'Notifications' }, - notificationsDesc: { id: 'settings.notifications.description', defaultMessage: 'Notifications are managed by your OS - {link}' }, + notificationsDesc: { + id: 'settings.notifications.description', + defaultMessage: 'Notifications are managed by your OS - {link}', + }, configGuide: { id: 'settings.notifications.configGuide', defaultMessage: 'Configuration guide' }, openSettings: { id: 'settings.notifications.openSettings', defaultMessage: 'Open Settings' }, menuBarIcon: { id: 'settings.menuBarIcon.title', defaultMessage: 'Menu bar icon' }, - menuBarIconDesc: { id: 'settings.menuBarIcon.description', defaultMessage: 'Show goose in the menu bar' }, + menuBarIconDesc: { + id: 'settings.menuBarIcon.description', + defaultMessage: 'Show goose in the menu bar', + }, dockIcon: { id: 'settings.dockIcon.title', defaultMessage: 'Dock icon' }, dockIconDesc: { id: 'settings.dockIcon.description', defaultMessage: 'Show goose in the dock' }, preventSleep: { id: 'settings.preventSleep.title', defaultMessage: 'Prevent Sleep' }, - preventSleepDesc: { id: 'settings.preventSleep.description', defaultMessage: 'Keep your computer awake while goose is running a task (screen can still lock)' }, + preventSleepDesc: { + id: 'settings.preventSleep.description', + defaultMessage: + 'Keep your computer awake while goose is running a task (screen can still lock)', + }, costTracking: { id: 'settings.costTracking.title', defaultMessage: 'Cost Tracking' }, - costTrackingDesc: { id: 'settings.costTracking.description', defaultMessage: 'Show model pricing and usage costs' }, + costTrackingDesc: { + id: 'settings.costTracking.description', + defaultMessage: 'Show model pricing and usage costs', + }, themeTitle: { id: 'settings.theme.title', defaultMessage: 'Theme' }, - themeDesc: { id: 'settings.theme.description', defaultMessage: 'Customize the look and feel of goose' }, + themeDesc: { + id: 'settings.theme.description', + defaultMessage: 'Customize the look and feel of goose', + }, navigationTitle: { id: 'settings.navigation.title', defaultMessage: 'Navigation' }, - navigationDesc: { id: 'settings.navigation.description', defaultMessage: 'Customize navigation layout and behavior' }, + navigationDesc: { + id: 'settings.navigation.description', + defaultMessage: 'Customize navigation layout and behavior', + }, navMode: { id: 'settings.navigation.mode', defaultMessage: 'Mode' }, navStyle: { id: 'settings.navigation.style', defaultMessage: 'Style' }, navPosition: { id: 'settings.navigation.position', defaultMessage: 'Position' }, navCustomize: { id: 'settings.navigation.customize', defaultMessage: 'Customize Items' }, helpTitle: { id: 'settings.help.title', defaultMessage: 'Help & feedback' }, - helpDesc: { id: 'settings.help.description', defaultMessage: 'Help us improve goose by reporting issues or requesting new features' }, + helpDesc: { + id: 'settings.help.description', + defaultMessage: 'Help us improve goose by reporting issues or requesting new features', + }, reportBug: { id: 'settings.help.reportBug', defaultMessage: 'Report a Bug' }, requestFeature: { id: 'settings.help.requestFeature', defaultMessage: 'Request a Feature' }, versionTitle: { id: 'settings.version.title', defaultMessage: 'Version' }, updatesTitle: { id: 'settings.updates.title', defaultMessage: 'Updates' }, - updatesDesc: { id: 'settings.updates.description', defaultMessage: 'Check for and install updates to keep goose running at its best' }, - notificationsModalTitle: { id: 'settings.notifications.modal.title', defaultMessage: 'How to Enable Notifications' }, - notificationsMacInstructions: { id: 'settings.notifications.modal.macInstructions', defaultMessage: 'To enable notifications on macOS:' }, - notificationsMacStep1: { id: 'settings.notifications.modal.macStep1', defaultMessage: 'Open System Preferences' }, - notificationsMacStep2: { id: 'settings.notifications.modal.macStep2', defaultMessage: 'Click on Notifications' }, - notificationsMacStep3: { id: 'settings.notifications.modal.macStep3', defaultMessage: 'Find and select goose in the application list' }, - notificationsMacStep4: { id: 'settings.notifications.modal.macStep4', defaultMessage: 'Enable notifications and adjust settings as desired' }, - notificationsWinInstructions: { id: 'settings.notifications.modal.winInstructions', defaultMessage: 'To enable notifications on Windows:' }, - notificationsWinStep1: { id: 'settings.notifications.modal.winStep1', defaultMessage: 'Open Settings' }, - notificationsWinStep2: { id: 'settings.notifications.modal.winStep2', defaultMessage: 'Go to System > Notifications' }, - notificationsWinStep3: { id: 'settings.notifications.modal.winStep3', defaultMessage: 'Find and select goose in the application list' }, - notificationsWinStep4: { id: 'settings.notifications.modal.winStep4', defaultMessage: 'Toggle notifications on and adjust settings as desired' }, + updatesDesc: { + id: 'settings.updates.description', + defaultMessage: 'Check for and install updates to keep goose running at its best', + }, + notificationsModalTitle: { + id: 'settings.notifications.modal.title', + defaultMessage: 'How to Enable Notifications', + }, + notificationsMacInstructions: { + id: 'settings.notifications.modal.macInstructions', + defaultMessage: 'To enable notifications on macOS:', + }, + notificationsMacStep1: { + id: 'settings.notifications.modal.macStep1', + defaultMessage: 'Open System Preferences', + }, + notificationsMacStep2: { + id: 'settings.notifications.modal.macStep2', + defaultMessage: 'Click on Notifications', + }, + notificationsMacStep3: { + id: 'settings.notifications.modal.macStep3', + defaultMessage: 'Find and select goose in the application list', + }, + notificationsMacStep4: { + id: 'settings.notifications.modal.macStep4', + defaultMessage: 'Enable notifications and adjust settings as desired', + }, + notificationsWinInstructions: { + id: 'settings.notifications.modal.winInstructions', + defaultMessage: 'To enable notifications on Windows:', + }, + notificationsWinStep1: { + id: 'settings.notifications.modal.winStep1', + defaultMessage: 'Open Settings', + }, + notificationsWinStep2: { + id: 'settings.notifications.modal.winStep2', + defaultMessage: 'Go to System > Notifications', + }, + notificationsWinStep3: { + id: 'settings.notifications.modal.winStep3', + defaultMessage: 'Find and select goose in the application list', + }, + notificationsWinStep4: { + id: 'settings.notifications.modal.winStep4', + defaultMessage: 'Toggle notifications on and adjust settings as desired', + }, close: { id: 'settings.close', defaultMessage: 'Close' }, }); @@ -94,23 +155,31 @@ const NavigationSettingsContent: React.FC = () => { {isExpanded && (
-

{intl.formatMessage(i18n.navMode)}

+

+ {intl.formatMessage(i18n.navMode)} +

{!isOverlayMode && (
-

{intl.formatMessage(i18n.navStyle)}

+

+ {intl.formatMessage(i18n.navStyle)} +

)} {!isOverlayMode && (
-

{intl.formatMessage(i18n.navPosition)}

+

+ {intl.formatMessage(i18n.navPosition)} +

)}
-

{intl.formatMessage(i18n.navCustomize)}

+

+ {intl.formatMessage(i18n.navCustomize)} +

@@ -267,7 +336,9 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
-

{intl.formatMessage(i18n.notifications)}

+

+ {intl.formatMessage(i18n.notifications)} +

{intl.formatMessage(i18n.notificationsDesc, { link: ( @@ -386,14 +457,12 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti {/* Navigation Settings */} - + {intl.formatMessage(i18n.helpTitle)} - - {intl.formatMessage(i18n.helpDesc)} - + {intl.formatMessage(i18n.helpDesc)}

@@ -452,9 +521,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti {intl.formatMessage(i18n.updatesTitle)} - - {intl.formatMessage(i18n.updatesDesc)} - + {intl.formatMessage(i18n.updatesDesc)} diff --git a/ui/desktop/src/components/settings/app/TelemetrySettings.tsx b/ui/desktop/src/components/settings/app/TelemetrySettings.tsx index f39af433..8b3d360e 100644 --- a/ui/desktop/src/components/settings/app/TelemetrySettings.tsx +++ b/ui/desktop/src/components/settings/app/TelemetrySettings.tsx @@ -3,7 +3,7 @@ import { Switch } from '../../ui/switch'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card'; import { useConfig } from '../../ConfigContext'; import { TELEMETRY_UI_ENABLED } from '../../../updates'; -import TelemetryOptOutModal from '../../TelemetryOptOutModal'; +import PrivacyInfoModal from '../../onboarding/PrivacyInfoModal'; import { toastService } from '../../../toasts'; import { setTelemetryEnabled as setAnalyticsTelemetryEnabled, @@ -48,11 +48,7 @@ const i18n = defineMessages({ const TELEMETRY_CONFIG_KEY = 'GOOSE_TELEMETRY_ENABLED'; -interface TelemetrySettingsProps { - isWelcome: boolean; -} - -export default function TelemetrySettings({ isWelcome = false }: TelemetrySettingsProps) { +export default function TelemetrySettings() { const intl = useIntl(); const { read, upsert } = useConfig(); const [telemetryEnabled, setTelemetryEnabled] = useState(true); @@ -84,7 +80,7 @@ export default function TelemetrySettings({ isWelcome = false }: TelemetrySettin await upsert(TELEMETRY_CONFIG_KEY, checked, false); setTelemetryEnabled(checked); setAnalyticsTelemetryEnabled(checked); - trackTelemetryPreference(checked, isWelcome ? 'onboarding' : 'settings'); + trackTelemetryPreference(checked, 'settings'); } catch (error) { console.error('Failed to update telemetry status:', error); toastService.error({ @@ -127,15 +123,13 @@ export default function TelemetrySettings({ isWelcome = false }: TelemetrySettin /> ); - const modal = ; + const modal = ; const toggleRow = (
-

- {toggleLabel} -

-

+

{toggleLabel}

+

{toggleDescription} {learnMoreLink}

@@ -143,19 +137,6 @@ export default function TelemetrySettings({ isWelcome = false }: TelemetrySettin
); - if (isWelcome) { - return ( - <> -
-

{title}

-

{description}

- {toggleRow} -
- {modal} - - ); - } - return ( <> diff --git a/ui/desktop/src/i18n/messages/en.json b/ui/desktop/src/i18n/messages/en.json index 93419505..7d998890 100644 --- a/ui/desktop/src/i18n/messages/en.json +++ b/ui/desktop/src/i18n/messages/en.json @@ -4334,48 +4334,21 @@ "switchModelModal.useOtherProvider": { "defaultMessage": "Use other provider" }, - "telemetryOptOutModal.collectErrors": { - "defaultMessage": "Error types (e.g., \"rate_limit\", \"auth\" - no details)" + "telemetryConsentPrompt.description": { + "defaultMessage": "Would you like to share anonymous usage data to help improve goose? We never collect your conversations, code, or personal data." }, - "telemetryOptOutModal.collectExtensions": { - "defaultMessage": "Extensions and tool usage counts (names only)" - }, - "telemetryOptOutModal.collectOs": { - "defaultMessage": "Operating system, version, and architecture" - }, - "telemetryOptOutModal.collectProvider": { - "defaultMessage": "Provider and model used" - }, - "telemetryOptOutModal.collectSession": { - "defaultMessage": "Session metrics (duration, interaction count, token usage)" - }, - "telemetryOptOutModal.collectVersion": { - "defaultMessage": "goose version and install method" - }, - "telemetryOptOutModal.configError": { - "defaultMessage": "Configuration Error" - }, - "telemetryOptOutModal.configErrorMessage": { - "defaultMessage": "Failed to check telemetry configuration." - }, - "telemetryOptOutModal.description": { - "defaultMessage": "Would you like to help improve goose by sharing anonymous usage data? This helps us understand how goose is used and identify areas for improvement." - }, - "telemetryOptOutModal.heading": { + "telemetryConsentPrompt.heading": { "defaultMessage": "Help improve goose" }, - "telemetryOptOutModal.optIn": { + "telemetryConsentPrompt.learnMore": { + "defaultMessage": "Learn more" + }, + "telemetryConsentPrompt.optIn": { "defaultMessage": "Yes, share anonymous usage data" }, - "telemetryOptOutModal.optOut": { + "telemetryConsentPrompt.optOut": { "defaultMessage": "No thanks" }, - "telemetryOptOutModal.privacyNote": { - "defaultMessage": "We never collect your conversations, code, tool arguments, error messages, or any personal data. You can change this setting anytime in Settings → App." - }, - "telemetryOptOutModal.whatWeCollect": { - "defaultMessage": "What we collect:" - }, "telemetrySettings.configErrorTitle": { "defaultMessage": "Configuration Error" },