Client settings (#7381)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2026-02-21 14:26:03 +00:00
committed by GitHub
parent c543586b6f
commit 45d59be201
16 changed files with 335 additions and 233 deletions
@@ -63,9 +63,7 @@ export default function AnnouncementModal() {
});
// Get list of seen announcement IDs
const seenAnnouncementIds = JSON.parse(
localStorage.getItem('seenAnnouncementIds') || '[]'
) as string[];
const seenAnnouncementIds = await window.electron.getSetting('seenAnnouncementIds');
// Find ALL unseen announcements (in order)
const unseenAnnouncementsList = applicableAnnouncements.filter(
@@ -101,13 +99,11 @@ export default function AnnouncementModal() {
loadAnnouncements();
}, []);
const handleCloseAnnouncement = () => {
const handleCloseAnnouncement = async () => {
if (unseenAnnouncements.length === 0) return;
// Get existing seen announcement IDs
const seenAnnouncementIds = JSON.parse(
localStorage.getItem('seenAnnouncementIds') || '[]'
) as string[];
const seenAnnouncementIds = await window.electron.getSetting('seenAnnouncementIds');
// Add all unseen announcement IDs to the seen list
const newSeenIds = [...seenAnnouncementIds];
@@ -117,7 +113,7 @@ export default function AnnouncementModal() {
}
});
localStorage.setItem('seenAnnouncementIds', JSON.stringify(newSeenIds));
await window.electron.setSetting('seenAnnouncementIds', newSeenIds);
setShowAnnouncementModal(false);
};
@@ -428,20 +428,20 @@ function ToolCallView({
notifications,
isStreamingMessage = false,
}: ToolCallViewProps) {
const [responseStyle, setResponseStyle] = useState(() => localStorage.getItem('response_style'));
const [responseStyle, setResponseStyle] = useState<string>('concise');
useEffect(() => {
const handleStorageChange = () => {
setResponseStyle(localStorage.getItem('response_style'));
// Load initial value from settings
window.electron.getSetting('responseStyle').then(setResponseStyle);
const handleStyleChange = () => {
window.electron.getSetting('responseStyle').then(setResponseStyle);
};
window.addEventListener('storage', handleStorageChange);
window.addEventListener(AppEvents.RESPONSE_STYLE_CHANGED, handleStorageChange);
window.addEventListener(AppEvents.RESPONSE_STYLE_CHANGED, handleStyleChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener(AppEvents.RESPONSE_STYLE_CHANGED, handleStorageChange);
window.removeEventListener(AppEvents.RESPONSE_STYLE_CHANGED, handleStyleChange);
};
}, []);
@@ -26,14 +26,19 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }:
// Check if pricing is enabled
useEffect(() => {
const checkPricingSetting = () => {
const stored = localStorage.getItem('show_pricing');
setShowPricing(stored !== 'false');
const loadPricingSetting = async () => {
const enabled = await window.electron.getSetting('showPricing');
setShowPricing(enabled);
};
checkPricingSetting();
window.addEventListener('storage', checkPricingSetting);
return () => window.removeEventListener('storage', checkPricingSetting);
loadPricingSetting();
const handlePricingChange = () => {
loadPricingSetting();
};
window.addEventListener('showPricingChanged', handlePricingChange);
return () => window.removeEventListener('showPricingChanged', handlePricingChange);
}, []);
useEffect(() => {
@@ -151,29 +151,18 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
const setView = useNavigation();
useEffect(() => {
const savedSessionConfig = localStorage.getItem('session_sharing_config');
if (savedSessionConfig) {
try {
const config = JSON.parse(savedSessionConfig);
if (config.enabled && config.baseUrl) {
setCanShare(true);
}
} catch (error) {
console.error('Error parsing session sharing config:', error);
window.electron.getSetting('sessionSharing').then((config) => {
if (config.enabled && config.baseUrl) {
setCanShare(true);
}
}
});
}, []);
const handleShare = async () => {
setIsSharing(true);
try {
const savedSessionConfig = localStorage.getItem('session_sharing_config');
if (!savedSessionConfig) {
throw new Error('Session sharing is not configured. Please configure it in settings.');
}
const config = JSON.parse(savedSessionConfig);
const config = await window.electron.getSetting('sessionSharing');
if (!config.enabled || !config.baseUrl) {
throw new Error('Session sharing is not enabled or base URL is not configured.');
}
@@ -58,8 +58,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
// Load show pricing setting
useEffect(() => {
const stored = localStorage.getItem('show_pricing');
setShowPricing(stored !== 'false');
window.electron.getSetting('showPricing').then(setShowPricing);
}, []);
// Handle scrolling to update section
@@ -140,12 +139,12 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
}
};
const handleShowPricingToggle = (checked: boolean) => {
const handleShowPricingToggle = async (checked: boolean) => {
setShowPricing(checked);
localStorage.setItem('show_pricing', String(checked));
await window.electron.setSetting('showPricing', checked);
trackSettingToggled('cost_tracking', checked);
// Trigger storage event for other components
window.dispatchEvent(new CustomEvent('storage'));
// Trigger event for other components
window.dispatchEvent(new CustomEvent('showPricingChanged'));
};
return (
@@ -3,33 +3,18 @@ import { Switch } from '../../ui/switch';
import { Input } from '../../ui/input';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { AlertCircle } from 'lucide-react';
import { ExternalGoosedConfig } from '../../../utils/settings';
import { ExternalGoosedConfig, defaultSettings } from '../../../utils/settings';
import { WEB_PROTOCOLS } from '../../../utils/urlSecurity';
const DEFAULT_CONFIG: ExternalGoosedConfig = {
enabled: false,
url: '',
secret: '',
};
function parseConfig(config: ExternalGoosedConfig | undefined): ExternalGoosedConfig {
if (!config) return DEFAULT_CONFIG;
return {
enabled: config.enabled ?? DEFAULT_CONFIG.enabled,
url: config.url ?? DEFAULT_CONFIG.url,
secret: config.secret ?? DEFAULT_CONFIG.secret,
};
}
export default function ExternalBackendSection() {
const [config, setConfig] = useState<ExternalGoosedConfig>(DEFAULT_CONFIG);
const [config, setConfig] = useState<ExternalGoosedConfig>(defaultSettings.externalGoosed);
const [isSaving, setIsSaving] = useState(false);
const [urlError, setUrlError] = useState<string | null>(null);
useEffect(() => {
const loadSettings = async () => {
const settings = await window.electron.getSettings();
setConfig(parseConfig(settings.externalGoosed));
const externalGoosed = await window.electron.getSetting('externalGoosed');
setConfig(externalGoosed);
};
loadSettings();
}, []);
@@ -56,11 +41,7 @@ export default function ExternalBackendSection() {
const saveConfig = async (newConfig: ExternalGoosedConfig): Promise<void> => {
setIsSaving(true);
try {
const currentSettings = await window.electron.getSettings();
await window.electron.saveSettings({
...currentSettings,
externalGoosed: newConfig,
});
await window.electron.setSetting('externalGoosed', newConfig);
} catch (error) {
console.error('Failed to save external backend settings:', error);
} finally {
@@ -122,8 +122,8 @@ export default function KeyboardShortcutsSection() {
const [showRestartNotice, setShowRestartNotice] = useState(false);
const loadShortcuts = useCallback(async () => {
const settings = await window.electron.getSettings();
setShortcuts(settings.keyboardShortcuts || defaultKeyboardShortcuts);
const keyboardShortcuts = await window.electron.getSetting('keyboardShortcuts');
setShortcuts(keyboardShortcuts || defaultKeyboardShortcuts);
}, []);
useEffect(() => {
@@ -163,15 +163,11 @@ export default function KeyboardShortcutsSection() {
newShortcuts[key] = null;
}
const settings = await window.electron.getSettings();
settings.keyboardShortcuts = newShortcuts;
const success = await window.electron.saveSettings(settings);
if (success) {
setShortcuts(newShortcuts);
trackSettingToggled(`shortcut_${key}`, enabled);
if (needsRestart.has(key)) {
setShowRestartNotice(true);
}
await window.electron.setSetting('keyboardShortcuts', newShortcuts);
setShortcuts(newShortcuts);
trackSettingToggled(`shortcut_${key}`, enabled);
if (needsRestart.has(key)) {
setShowRestartNotice(true);
}
};
@@ -209,15 +205,11 @@ export default function KeyboardShortcutsSection() {
newShortcuts[editingKey] = shortcut || null;
const settings = await window.electron.getSettings();
settings.keyboardShortcuts = newShortcuts;
const success = await window.electron.saveSettings(settings);
if (success) {
setShortcuts(newShortcuts);
setEditingKey(null);
if (needsRestart.has(editingKey)) {
setShowRestartNotice(true);
}
await window.electron.setSetting('keyboardShortcuts', newShortcuts);
setShortcuts(newShortcuts);
setEditingKey(null);
if (needsRestart.has(editingKey)) {
setShowRestartNotice(true);
}
};
@@ -236,14 +228,10 @@ export default function KeyboardShortcutsSection() {
});
if (confirmed.response === 0) {
const settings = await window.electron.getSettings();
settings.keyboardShortcuts = { ...defaultKeyboardShortcuts };
const success = await window.electron.saveSettings(settings);
if (success) {
setShortcuts({ ...defaultKeyboardShortcuts });
setShowRestartNotice(true);
trackSettingToggled('shortcuts_reset', true);
}
await window.electron.setSetting('keyboardShortcuts', { ...defaultKeyboardShortcuts });
setShortcuts({ ...defaultKeyboardShortcuts });
setShowRestartNotice(true);
trackSettingToggled('shortcuts_reset', true);
}
};
@@ -6,23 +6,24 @@ export const ResponseStylesSection = () => {
const [currentStyle, setCurrentStyle] = useState('concise');
useEffect(() => {
const savedStyle = localStorage.getItem('response_style');
if (savedStyle) {
async function loadResponseStyle() {
try {
const savedStyle = await window.electron.getSetting('responseStyle');
setCurrentStyle(savedStyle);
} catch (error) {
console.error('Error parsing response style:', error);
console.error('Error loading response style:', error);
}
} else {
// Set default to concise for new users
localStorage.setItem('response_style', 'concise');
setCurrentStyle('concise');
}
loadResponseStyle();
}, []);
const handleStyleChange = async (newStyle: string) => {
setCurrentStyle(newStyle);
localStorage.setItem('response_style', newStyle);
try {
await window.electron.setSetting('responseStyle', newStyle);
} catch (error) {
console.error('Error saving response style:', error);
}
// Dispatch custom event to notify other components of the change
window.dispatchEvent(new CustomEvent(AppEvents.RESPONSE_STYLE_CHANGED));
@@ -27,25 +27,19 @@ export default function SessionSharingSection() {
sessionSharingConfig.enabled &&
isValidUrl(String(sessionSharingConfig.baseUrl));
// Only load saved config from localStorage if the env variable is not provided.
// Only load saved config from settings if the env variable is not provided.
useEffect(() => {
if (envBaseUrlShare) {
// If env variable is set, save the forced configuration to localStorage
// If env variable is set, save the forced configuration to settings
const forcedConfig = {
enabled: true,
baseUrl: typeof envBaseUrlShare === 'string' ? envBaseUrlShare : '',
};
localStorage.setItem('session_sharing_config', JSON.stringify(forcedConfig));
window.electron.setSetting('sessionSharing', forcedConfig);
} else {
const savedSessionConfig = localStorage.getItem('session_sharing_config');
if (savedSessionConfig) {
try {
const config = JSON.parse(savedSessionConfig);
setSessionSharingConfig(config);
} catch (error) {
console.error('Error parsing session sharing config:', error);
}
}
window.electron.getSetting('sessionSharing').then((config) => {
setSessionSharingConfig(config);
});
}
}, [envBaseUrlShare]);
@@ -61,20 +55,18 @@ export default function SessionSharingSection() {
}
// Toggle sharing (only allowed when env is not set).
const toggleSharing = () => {
const toggleSharing = async () => {
if (envBaseUrlShare) {
return; // Do nothing if the environment variable forces sharing.
}
setSessionSharingConfig((prev) => {
const updated = { ...prev, enabled: !prev.enabled };
localStorage.setItem('session_sharing_config', JSON.stringify(updated));
trackSettingToggled('session_sharing', updated.enabled);
return updated;
});
const updated = { ...sessionSharingConfig, enabled: !sessionSharingConfig.enabled };
setSessionSharingConfig(updated);
await window.electron.setSetting('sessionSharing', updated);
trackSettingToggled('session_sharing', updated.enabled);
};
// Handle changes to the base URL field
const handleBaseUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleBaseUrlChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const newBaseUrl = e.target.value;
setSessionSharingConfig((prev) => ({
...prev,
@@ -87,7 +79,7 @@ export default function SessionSharingSection() {
if (isValidUrl(newBaseUrl)) {
setUrlError('');
const updated = { ...sessionSharingConfig, baseUrl: newBaseUrl };
localStorage.setItem('session_sharing_config', JSON.stringify(updated));
await window.electron.setSetting('sessionSharing', updated);
} else {
setUrlError('Invalid URL format. Please enter a valid URL (e.g. https://example.com/api).');
}