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).');
}
+49 -31
View File
@@ -22,29 +22,6 @@ function resolveTheme(preference: ThemePreference): ResolvedTheme {
return preference;
}
function loadThemePreference(): ThemePreference {
const useSystemTheme = localStorage.getItem('use_system_theme');
if (useSystemTheme === 'true') {
return 'system';
}
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
return 'dark';
}
return 'light';
}
function saveThemePreference(preference: ThemePreference): void {
if (preference === 'system') {
localStorage.setItem('use_system_theme', 'true');
} else {
localStorage.setItem('use_system_theme', 'false');
localStorage.setItem('theme', preference);
}
}
function applyThemeToDocument(theme: ResolvedTheme): void {
const toRemove = theme === 'dark' ? 'light' : 'dark';
document.documentElement.classList.add(theme);
@@ -56,19 +33,53 @@ interface ThemeProviderProps {
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const [userThemePreference, setUserThemePreferenceState] =
useState<ThemePreference>(loadThemePreference);
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() =>
resolveTheme(loadThemePreference())
);
// Start with light theme to avoid flash, will update once settings load
const [userThemePreference, setUserThemePreferenceState] = useState<ThemePreference>('light');
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>('light');
const setUserThemePreference = useCallback((preference: ThemePreference) => {
useEffect(() => {
async function loadThemeFromSettings() {
try {
const [useSystemTheme, savedTheme] = await Promise.all([
window.electron.getSetting('useSystemTheme'),
window.electron.getSetting('theme'),
]);
let preference: ThemePreference;
if (useSystemTheme) {
preference = 'system';
} else {
preference = savedTheme;
}
setUserThemePreferenceState(preference);
setResolvedTheme(resolveTheme(preference));
} catch (error) {
console.warn('[ThemeContext] Failed to load theme settings:', error);
}
}
loadThemeFromSettings();
}, []);
const setUserThemePreference = useCallback(async (preference: ThemePreference) => {
setUserThemePreferenceState(preference);
saveThemePreference(preference);
const resolved = resolveTheme(preference);
setResolvedTheme(resolved);
// Save to settings
try {
if (preference === 'system') {
await window.electron.setSetting('useSystemTheme', true);
} else {
await window.electron.setSetting('useSystemTheme', false);
await window.electron.setSetting('theme', preference);
}
} catch (error) {
console.warn('[ThemeContext] Failed to save theme settings:', error);
}
// Broadcast to other windows via Electron
window.electron?.broadcastThemeChange({
mode: resolved,
@@ -104,8 +115,15 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
: 'light';
setUserThemePreferenceState(newPreference);
saveThemePreference(newPreference);
setResolvedTheme(resolveTheme(newPreference));
// Save to settings (don't await, fire and forget)
if (newPreference === 'system') {
window.electron.setSetting('useSystemTheme', true);
} else {
window.electron.setSetting('useSystemTheme', false);
window.electron.setSetting('theme', newPreference);
}
};
window.electron.on('theme-changed', handleThemeChanged);
+53 -25
View File
@@ -30,8 +30,8 @@ import log from './utils/logger';
import { ensureWinShims } from './utils/winShims';
import { addRecentDir, loadRecentDirs } from './utils/recentDirs';
import { formatAppName, errorMessage, formatErrorForLogging } from './utils/conversionUtils';
import type { Settings } from './utils/settings';
import { defaultKeyboardShortcuts, getKeyboardShortcuts } from './utils/settings';
import type { Settings, SettingKey } from './utils/settings';
import { defaultSettings, getKeyboardShortcuts } from './utils/settings';
import * as crypto from 'crypto';
import * as yaml from 'yaml';
import windowStateKeeper from 'electron-window-state';
@@ -57,18 +57,27 @@ function shouldSetupUpdater(): boolean {
// Settings management
const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json');
const defaultSettings: Settings = {
showMenuBarIcon: true,
showDockIcon: true,
enableWakelock: false,
spellcheckEnabled: true,
keyboardShortcuts: defaultKeyboardShortcuts,
};
function getSettings(): Settings {
if (fsSync.existsSync(SETTINGS_FILE)) {
const data = fsSync.readFileSync(SETTINGS_FILE, 'utf8');
return JSON.parse(data);
const stored = JSON.parse(data) as Partial<Settings>;
// Deep merge to ensure nested objects get their defaults too
return {
...defaultSettings,
...stored,
externalGoosed: {
...defaultSettings.externalGoosed,
...(stored.externalGoosed ?? {}),
},
keyboardShortcuts: {
...defaultSettings.keyboardShortcuts,
...(stored.keyboardShortcuts ?? {}),
},
sessionSharing: {
...defaultSettings.sessionSharing,
...(stored.sessionSharing ?? {}),
},
};
}
return defaultSettings;
}
@@ -1192,25 +1201,44 @@ ipcMain.handle('add-recent-dir', (_event, dir: string) => {
}
});
// Handle scheduling engine settings
ipcMain.handle('get-settings', () => {
return getSettings(); // Always returns Settings (uses defaults as fallback)
ipcMain.handle('get-setting', (_event, key: SettingKey) => {
const settings = getSettings();
return settings[key];
});
ipcMain.handle('save-settings', (_event, settings) => {
const oldSettings = getSettings();
// Valid setting keys for runtime validation
const validSettingKeys: Set<string> = new Set([
'showMenuBarIcon',
'showDockIcon',
'enableWakelock',
'spellcheckEnabled',
'externalGoosed',
'globalShortcut',
'keyboardShortcuts',
'theme',
'useSystemTheme',
'responseStyle',
'showPricing',
'sessionSharing',
'seenAnnouncementIds',
]);
const oldShortcuts = getKeyboardShortcuts(oldSettings);
const newShortcuts = getKeyboardShortcuts(settings);
const shortcutsChanged = JSON.stringify(oldShortcuts) !== JSON.stringify(newShortcuts);
fsSync.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
if (shortcutsChanged) {
registerGlobalShortcuts();
ipcMain.handle('set-setting', (_event, key: SettingKey, value: unknown) => {
// Validate key at runtime to prevent prototype pollution
if (!validSettingKeys.has(key)) {
console.error(`Invalid setting key rejected: ${key}`);
return;
}
return true;
const settings = getSettings();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(settings as any)[key] = value;
fsSync.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
// Re-register shortcuts if keyboard shortcuts changed
if (key === 'keyboardShortcuts') {
registerGlobalShortcuts();
}
});
ipcMain.handle('get-secret-key', () => {
+68 -5
View File
@@ -1,7 +1,45 @@
import Electron, { contextBridge, ipcRenderer, webUtils } from 'electron';
import { Recipe } from './recipe';
import { GooseApp } from './api';
import type { Settings } from './utils/settings';
import type { Settings, SettingKey } from './utils/settings';
import { defaultSettings } from './utils/settings';
// Mapping from settings keys to their old localStorage keys for lazy migration
const localStorageKeyMap: Partial<Record<SettingKey, string>> = {
theme: 'theme',
useSystemTheme: 'use_system_theme',
responseStyle: 'response_style',
showPricing: 'show_pricing',
sessionSharing: 'session_sharing_config',
seenAnnouncementIds: 'seenAnnouncementIds',
};
// Parse localStorage value based on the setting key
function parseLocalStorageValue<K extends SettingKey>(
key: K,
rawValue: string
): Settings[K] | null {
try {
switch (key) {
case 'theme':
return (rawValue === 'dark' || rawValue === 'light' ? rawValue : null) as Settings[K];
case 'useSystemTheme':
return (rawValue === 'true') as unknown as Settings[K];
case 'responseStyle':
return rawValue as Settings[K];
case 'showPricing':
return (rawValue === 'true') as unknown as Settings[K];
case 'sessionSharing':
return JSON.parse(rawValue) as Settings[K];
case 'seenAnnouncementIds':
return JSON.parse(rawValue) as Settings[K];
default:
return null;
}
} catch {
return null;
}
}
interface NotificationData {
title: string;
@@ -86,8 +124,8 @@ type ElectronAPI = {
getMenuBarIconState: () => Promise<boolean>;
setDockIcon: (show: boolean) => Promise<boolean>;
getDockIconState: () => Promise<boolean>;
getSettings: () => Promise<Settings>;
saveSettings: (settings: Settings) => Promise<boolean>;
getSetting: <K extends SettingKey>(key: K) => Promise<Settings[K]>;
setSetting: <K extends SettingKey>(key: K, value: Settings[K]) => Promise<void>;
getSecretKey: () => Promise<string>;
getGoosedHostPort: () => Promise<string | null>;
setWakelock: (enable: boolean) => Promise<boolean>;
@@ -190,8 +228,33 @@ const electronAPI: ElectronAPI = {
getMenuBarIconState: () => ipcRenderer.invoke('get-menu-bar-icon-state'),
setDockIcon: (show: boolean) => ipcRenderer.invoke('set-dock-icon', show),
getDockIconState: () => ipcRenderer.invoke('get-dock-icon-state'),
getSettings: () => ipcRenderer.invoke('get-settings'),
saveSettings: (settings: unknown) => ipcRenderer.invoke('save-settings', settings),
getSetting: async <K extends SettingKey>(key: K): Promise<Settings[K]> => {
try {
// Check for localStorage value first (lazy migration)
const localStorageKey = localStorageKeyMap[key];
if (localStorageKey) {
const rawValue = localStorage.getItem(localStorageKey);
if (rawValue !== null) {
const parsed = parseLocalStorageValue(key, rawValue);
if (parsed !== null) {
return parsed;
}
}
}
return await ipcRenderer.invoke('get-setting', key);
} catch (error) {
console.error(`Failed to get setting '${key}', using default`, error);
return defaultSettings[key];
}
},
setSetting: async <K extends SettingKey>(key: K, value: Settings[K]): Promise<void> => {
// Clear any localStorage version when writing
const localStorageKey = localStorageKeyMap[key];
if (localStorageKey) {
localStorage.removeItem(localStorageKey);
}
return ipcRenderer.invoke('set-setting', key, value);
},
getSecretKey: () => ipcRenderer.invoke('get-secret-key'),
getGoosedHostPort: () => ipcRenderer.invoke('get-goosed-host-port'),
setWakelock: (enable: boolean) => ipcRenderer.invoke('set-wakelock', enable),
-1
View File
@@ -6,7 +6,6 @@ import SuspenseLoader from './suspense-loader';
import { client } from './api/client.gen';
import { setTelemetryEnabled } from './utils/analytics';
import { readConfig } from './api';
const App = lazy(() => import('./App'));
const TELEMETRY_CONFIG_KEY = 'GOOSE_TELEMETRY_ENABLED';
+7 -19
View File
@@ -26,27 +26,15 @@ export async function openSharedSessionFromDeepLink(
throw new Error('Invalid URL: Missing share token');
}
// If no baseUrl is provided, check if there's one in localStorage
// If no baseUrl is provided, check if there's one in settings
if (!baseUrl) {
const savedSessionConfig = localStorage.getItem('session_sharing_config');
if (savedSessionConfig) {
try {
const config = JSON.parse(savedSessionConfig);
if (config.enabled && config.baseUrl) {
baseUrl = config.baseUrl;
} else {
throw new Error(
'Session sharing is not enabled or base URL is not configured. Check the settings page.'
);
}
} catch (error) {
console.error('Error parsing session sharing config:', error);
throw new Error(
'Session sharing is not enabled or base URL is not configured. Check the settings page.'
);
}
const config = await window.electron.getSetting('sessionSharing');
if (config.enabled && config.baseUrl) {
baseUrl = config.baseUrl;
} else {
throw new Error('Session sharing is not configured');
throw new Error(
'Session sharing is not enabled or base URL is not configured. Check the settings page.'
);
}
}
+39 -25
View File
@@ -42,36 +42,50 @@ Object.assign(navigator, {
},
});
// Mock settings store for tests
const mockSettings: Record<string, unknown> = {
showMenuBarIcon: true,
showDockIcon: true,
enableWakelock: false,
spellcheckEnabled: true,
keyboardShortcuts: {
focusWindow: 'CommandOrControl+Alt+G',
quickLauncher: 'CommandOrControl+Alt+Shift+G',
newChat: 'CommandOrControl+T',
newChatWindow: 'CommandOrControl+N',
openDirectory: 'CommandOrControl+O',
settings: 'CommandOrControl+,',
find: 'CommandOrControl+F',
findNext: 'CommandOrControl+G',
findPrevious: 'CommandOrControl+Shift+G',
alwaysOnTop: 'CommandOrControl+Shift+T',
},
externalGoosed: {
enabled: false,
url: '',
secret: '',
},
theme: 'light',
useSystemTheme: true,
responseStyle: 'concise',
showPricing: true,
sessionSharing: {
enabled: false,
baseUrl: '',
},
seenAnnouncementIds: [],
};
// Mock window.electron for renderer process
Object.defineProperty(window, 'electron', {
writable: true,
value: {
platform: 'darwin',
getSettings: vi.fn(() =>
Promise.resolve({
envToggles: {
GOOSE_SERVER__MEMORY: false,
GOOSE_SERVER__COMPUTER_CONTROLLER: false,
},
showMenuBarIcon: true,
showDockIcon: true,
enableWakelock: false,
spellcheckEnabled: true,
keyboardShortcuts: {
focusWindow: 'CommandOrControl+Alt+G',
quickLauncher: 'CommandOrControl+Alt+Shift+G',
newChat: 'CommandOrControl+T',
newChatWindow: 'CommandOrControl+N',
openDirectory: 'CommandOrControl+O',
settings: 'CommandOrControl+,',
find: 'CommandOrControl+F',
findNext: 'CommandOrControl+G',
findPrevious: 'CommandOrControl+Shift+G',
alwaysOnTop: 'CommandOrControl+Shift+T',
},
})
),
saveSettings: vi.fn(() => Promise.resolve(true)),
getSetting: vi.fn((key: string) => Promise.resolve(mockSettings[key])),
setSetting: vi.fn((key: string, value: unknown) => {
mockSettings[key] = value;
return Promise.resolve();
}),
showMessageBox: vi.fn(() => Promise.resolve({ response: 0 })),
},
});
+43 -2
View File
@@ -21,16 +21,32 @@ export type DefaultKeyboardShortcuts = {
[K in keyof KeyboardShortcuts]: string;
};
export interface SessionSharingConfig {
enabled: boolean;
baseUrl: string;
}
export interface Settings {
// Desktop app settings
showMenuBarIcon: boolean;
showDockIcon: boolean;
enableWakelock: boolean;
spellcheckEnabled: boolean;
externalGoosed?: ExternalGoosedConfig;
externalGoosed: ExternalGoosedConfig;
globalShortcut?: string | null;
keyboardShortcuts?: KeyboardShortcuts;
keyboardShortcuts: KeyboardShortcuts;
// UI preferences (migrated from localStorage)
theme: 'dark' | 'light';
useSystemTheme: boolean;
responseStyle: string;
showPricing: boolean;
sessionSharing: SessionSharingConfig;
seenAnnouncementIds: string[];
}
export type SettingKey = keyof Settings;
export const defaultKeyboardShortcuts: DefaultKeyboardShortcuts = {
focusWindow: 'CommandOrControl+Alt+G',
quickLauncher: 'CommandOrControl+Alt+Shift+G',
@@ -44,6 +60,31 @@ export const defaultKeyboardShortcuts: DefaultKeyboardShortcuts = {
alwaysOnTop: 'CommandOrControl+Shift+T',
};
export const defaultSettings: Settings = {
// Desktop app settings
showMenuBarIcon: true,
showDockIcon: true,
enableWakelock: false,
spellcheckEnabled: true,
keyboardShortcuts: defaultKeyboardShortcuts,
externalGoosed: {
enabled: false,
url: '',
secret: '',
},
// UI preferences
theme: 'light',
useSystemTheme: true,
responseStyle: 'concise',
showPricing: true,
sessionSharing: {
enabled: false,
baseUrl: '',
},
seenAnnouncementIds: [],
};
export function getKeyboardShortcuts(settings: Settings): KeyboardShortcuts {
if (!settings.keyboardShortcuts && settings.globalShortcut !== undefined) {
const focusShortcut = settings.globalShortcut;