mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
Move all built-in prompts from inline code to separate .md files in src/prompts/
for easier editing without code changes. Prompts use {{TEMPLATE_VARIABLES}} that
are substituted at runtime using the central substituteTemplateVariables function.
Changes:
- Add src/prompts/ directory with 7 prompt files (wizard, AutoRun, etc.)
- Add index.ts for central exports using Vite ?raw imports
- Add esbuild plugin in build-cli.mjs to support ?raw imports for CLI
- Update wizardPrompts.ts and phaseGenerator.ts to use central substitution
- Update CLAUDE.md documentation with new prompt location references
- Add TypeScript declaration for *.md?raw imports in global.d.ts
Claude ID: 38553613-f82f-4ce1-973e-fa80d42af3da
Maestro ID: b9bc0d08-5be2-4fdf-93cd-5618a8d53b35
1219 lines
45 KiB
TypeScript
1219 lines
45 KiB
TypeScript
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||
import type { LLMProvider, ThemeId, ThemeColors, Shortcut, CustomAICommand, GlobalStats, AutoRunStats, OnboardingStats, LeaderboardRegistration } from '../types';
|
||
import { DEFAULT_CUSTOM_THEME_COLORS } from '../constants/themes';
|
||
import { DEFAULT_SHORTCUTS } from '../constants/shortcuts';
|
||
import { commitCommandPrompt } from '../../prompts';
|
||
|
||
// Default global stats
|
||
const DEFAULT_GLOBAL_STATS: GlobalStats = {
|
||
totalSessions: 0,
|
||
totalMessages: 0,
|
||
totalInputTokens: 0,
|
||
totalOutputTokens: 0,
|
||
totalCacheReadTokens: 0,
|
||
totalCacheCreationTokens: 0,
|
||
totalCostUsd: 0,
|
||
totalActiveTimeMs: 0,
|
||
};
|
||
|
||
// Default auto-run stats
|
||
const DEFAULT_AUTO_RUN_STATS: AutoRunStats = {
|
||
cumulativeTimeMs: 0,
|
||
longestRunMs: 0,
|
||
longestRunTimestamp: 0,
|
||
totalRuns: 0,
|
||
currentBadgeLevel: 0,
|
||
lastBadgeUnlockLevel: 0,
|
||
lastAcknowledgedBadgeLevel: 0,
|
||
badgeHistory: [],
|
||
};
|
||
|
||
// Default onboarding stats (all local, no external telemetry)
|
||
const DEFAULT_ONBOARDING_STATS: OnboardingStats = {
|
||
// Wizard statistics
|
||
wizardStartCount: 0,
|
||
wizardCompletionCount: 0,
|
||
wizardAbandonCount: 0,
|
||
wizardResumeCount: 0,
|
||
averageWizardDurationMs: 0,
|
||
totalWizardDurationMs: 0,
|
||
lastWizardCompletedAt: 0,
|
||
|
||
// Tour statistics
|
||
tourStartCount: 0,
|
||
tourCompletionCount: 0,
|
||
tourSkipCount: 0,
|
||
tourStepsViewedTotal: 0,
|
||
averageTourStepsViewed: 0,
|
||
|
||
// Conversation statistics
|
||
totalConversationExchanges: 0,
|
||
averageConversationExchanges: 0,
|
||
totalConversationsCompleted: 0,
|
||
|
||
// Phase generation statistics
|
||
totalPhasesGenerated: 0,
|
||
averagePhasesPerWizard: 0,
|
||
totalTasksGenerated: 0,
|
||
averageTasksPerPhase: 0,
|
||
};
|
||
|
||
// Default AI commands that ship with Maestro
|
||
// Template variables available: {{AGENT_NAME}}, {{AGENT_PATH}}, {{TAB_NAME}}, {{AGENT_GROUP}}, {{AGENT_SESSION_ID}}, {{DATE}}, {{TIME}}, etc.
|
||
const DEFAULT_AI_COMMANDS: CustomAICommand[] = [
|
||
{
|
||
id: 'commit',
|
||
command: '/commit',
|
||
description: 'Commit outstanding changes and push up',
|
||
prompt: commitCommandPrompt,
|
||
isBuiltIn: true,
|
||
},
|
||
];
|
||
|
||
export interface UseSettingsReturn {
|
||
// Loading state
|
||
settingsLoaded: boolean;
|
||
|
||
// LLM settings
|
||
llmProvider: LLMProvider;
|
||
modelSlug: string;
|
||
apiKey: string;
|
||
setLlmProvider: (value: LLMProvider) => void;
|
||
setModelSlug: (value: string) => void;
|
||
setApiKey: (value: string) => void;
|
||
|
||
// Agent settings
|
||
defaultAgent: string;
|
||
setDefaultAgent: (value: string) => void;
|
||
|
||
// Shell settings
|
||
defaultShell: string;
|
||
setDefaultShell: (value: string) => void;
|
||
|
||
// GitHub CLI settings
|
||
ghPath: string;
|
||
setGhPath: (value: string) => void;
|
||
|
||
// Font settings
|
||
fontFamily: string;
|
||
fontSize: number;
|
||
customFonts: string[];
|
||
setFontFamily: (value: string) => void;
|
||
setFontSize: (value: number) => void;
|
||
setCustomFonts: (value: string[]) => void;
|
||
|
||
// UI settings
|
||
activeThemeId: ThemeId;
|
||
setActiveThemeId: (value: ThemeId) => void;
|
||
customThemeColors: ThemeColors;
|
||
setCustomThemeColors: (value: ThemeColors) => void;
|
||
customThemeBaseId: ThemeId;
|
||
setCustomThemeBaseId: (value: ThemeId) => void;
|
||
enterToSendAI: boolean;
|
||
setEnterToSendAI: (value: boolean) => void;
|
||
enterToSendTerminal: boolean;
|
||
setEnterToSendTerminal: (value: boolean) => void;
|
||
defaultSaveToHistory: boolean;
|
||
setDefaultSaveToHistory: (value: boolean) => void;
|
||
leftSidebarWidth: number;
|
||
rightPanelWidth: number;
|
||
markdownEditMode: boolean;
|
||
setLeftSidebarWidth: (value: number) => void;
|
||
setRightPanelWidth: (value: number) => void;
|
||
setMarkdownEditMode: (value: boolean) => void;
|
||
showHiddenFiles: boolean;
|
||
setShowHiddenFiles: (value: boolean) => void;
|
||
|
||
// Terminal settings
|
||
terminalWidth: number;
|
||
setTerminalWidth: (value: number) => void;
|
||
|
||
// Logging settings
|
||
logLevel: string;
|
||
setLogLevel: (value: string) => void;
|
||
maxLogBuffer: number;
|
||
setMaxLogBuffer: (value: number) => void;
|
||
|
||
// Output settings
|
||
maxOutputLines: number;
|
||
setMaxOutputLines: (value: number) => void;
|
||
|
||
// Notification settings
|
||
osNotificationsEnabled: boolean;
|
||
setOsNotificationsEnabled: (value: boolean) => void;
|
||
audioFeedbackEnabled: boolean;
|
||
setAudioFeedbackEnabled: (value: boolean) => void;
|
||
audioFeedbackCommand: string;
|
||
setAudioFeedbackCommand: (value: string) => void;
|
||
toastDuration: number;
|
||
setToastDuration: (value: number) => void;
|
||
|
||
// Update settings
|
||
checkForUpdatesOnStartup: boolean;
|
||
setCheckForUpdatesOnStartup: (value: boolean) => void;
|
||
|
||
// Log Viewer settings
|
||
logViewerSelectedLevels: string[];
|
||
setLogViewerSelectedLevels: (value: string[]) => void;
|
||
|
||
// Shortcuts
|
||
shortcuts: Record<string, Shortcut>;
|
||
setShortcuts: (value: Record<string, Shortcut>) => void;
|
||
|
||
// Custom AI Commands
|
||
customAICommands: CustomAICommand[];
|
||
setCustomAICommands: (value: CustomAICommand[]) => void;
|
||
|
||
// Global Stats (persistent across restarts)
|
||
globalStats: GlobalStats;
|
||
setGlobalStats: (value: GlobalStats) => void;
|
||
updateGlobalStats: (delta: Partial<GlobalStats>) => void;
|
||
|
||
// Auto-run Stats (persistent across restarts)
|
||
autoRunStats: AutoRunStats;
|
||
setAutoRunStats: (value: AutoRunStats) => void;
|
||
recordAutoRunComplete: (elapsedTimeMs: number) => { newBadgeLevel: number | null; isNewRecord: boolean };
|
||
updateAutoRunProgress: (currentRunElapsedMs: number) => { newBadgeLevel: number | null; isNewRecord: boolean };
|
||
acknowledgeBadge: (level: number) => void;
|
||
getUnacknowledgedBadgeLevel: () => number | null;
|
||
|
||
// UI collapse states (persistent)
|
||
ungroupedCollapsed: boolean;
|
||
setUngroupedCollapsed: (value: boolean) => void;
|
||
|
||
// Onboarding settings
|
||
wizardCompleted: boolean;
|
||
setWizardCompleted: (value: boolean) => void;
|
||
tourCompleted: boolean;
|
||
setTourCompleted: (value: boolean) => void;
|
||
firstAutoRunCompleted: boolean;
|
||
setFirstAutoRunCompleted: (value: boolean) => void;
|
||
|
||
// Onboarding Stats (persistent, local-only analytics)
|
||
onboardingStats: OnboardingStats;
|
||
setOnboardingStats: (value: OnboardingStats) => void;
|
||
recordWizardStart: () => void;
|
||
recordWizardComplete: (durationMs: number, conversationExchanges: number, phasesGenerated: number, tasksGenerated: number) => void;
|
||
recordWizardAbandon: () => void;
|
||
recordWizardResume: () => void;
|
||
recordTourStart: () => void;
|
||
recordTourComplete: (stepsViewed: number) => void;
|
||
recordTourSkip: (stepsViewed: number) => void;
|
||
getOnboardingAnalytics: () => {
|
||
wizardCompletionRate: number;
|
||
tourCompletionRate: number;
|
||
averageConversationExchanges: number;
|
||
averagePhasesPerWizard: number;
|
||
};
|
||
|
||
// Leaderboard Registration (persistent)
|
||
leaderboardRegistration: LeaderboardRegistration | null;
|
||
setLeaderboardRegistration: (value: LeaderboardRegistration | null) => void;
|
||
isLeaderboardRegistered: boolean;
|
||
}
|
||
|
||
export function useSettings(): UseSettingsReturn {
|
||
// Loading state
|
||
const [settingsLoaded, setSettingsLoaded] = useState(false);
|
||
|
||
// LLM Config
|
||
const [llmProvider, setLlmProviderState] = useState<LLMProvider>('openrouter');
|
||
const [modelSlug, setModelSlugState] = useState('anthropic/claude-3.5-sonnet');
|
||
const [apiKey, setApiKeyState] = useState('');
|
||
|
||
// Agent Config
|
||
const [defaultAgent, setDefaultAgentState] = useState('claude-code');
|
||
|
||
// Shell Config
|
||
const [defaultShell, setDefaultShellState] = useState('zsh');
|
||
|
||
// GitHub CLI Config
|
||
const [ghPath, setGhPathState] = useState('');
|
||
|
||
// Font Config
|
||
const [fontFamily, setFontFamilyState] = useState('Roboto Mono, Menlo, "Courier New", monospace');
|
||
const [fontSize, setFontSizeState] = useState(14);
|
||
const [customFonts, setCustomFontsState] = useState<string[]>([]);
|
||
|
||
// UI Config
|
||
const [activeThemeId, setActiveThemeIdState] = useState<ThemeId>('dracula');
|
||
const [customThemeColors, setCustomThemeColorsState] = useState<ThemeColors>(DEFAULT_CUSTOM_THEME_COLORS);
|
||
const [customThemeBaseId, setCustomThemeBaseIdState] = useState<ThemeId>('dracula');
|
||
const [enterToSendAI, setEnterToSendAIState] = useState(false); // AI mode defaults to Command+Enter
|
||
const [enterToSendTerminal, setEnterToSendTerminalState] = useState(true); // Terminal defaults to Enter
|
||
const [defaultSaveToHistory, setDefaultSaveToHistoryState] = useState(true); // History toggle defaults to on
|
||
const [leftSidebarWidth, setLeftSidebarWidthState] = useState(256);
|
||
const [rightPanelWidth, setRightPanelWidthState] = useState(384);
|
||
const [markdownEditMode, setMarkdownEditModeState] = useState(false);
|
||
const [showHiddenFiles, setShowHiddenFilesState] = useState(true); // Default: show hidden files
|
||
|
||
// Terminal Config
|
||
const [terminalWidth, setTerminalWidthState] = useState(100);
|
||
|
||
// Logging Config
|
||
const [logLevel, setLogLevelState] = useState('info');
|
||
const [maxLogBuffer, setMaxLogBufferState] = useState(5000);
|
||
|
||
// Output Config
|
||
const [maxOutputLines, setMaxOutputLinesState] = useState(25);
|
||
|
||
// Notification Config
|
||
const [osNotificationsEnabled, setOsNotificationsEnabledState] = useState(true); // Default: on
|
||
const [audioFeedbackEnabled, setAudioFeedbackEnabledState] = useState(false); // Default: off
|
||
const [audioFeedbackCommand, setAudioFeedbackCommandState] = useState('say'); // Default: macOS say command
|
||
const [toastDuration, setToastDurationState] = useState(20); // Default: 20 seconds, 0 = never auto-dismiss
|
||
|
||
// Update Config
|
||
const [checkForUpdatesOnStartup, setCheckForUpdatesOnStartupState] = useState(true); // Default: on
|
||
|
||
// Log Viewer Config
|
||
const [logViewerSelectedLevels, setLogViewerSelectedLevelsState] = useState<string[]>(['debug', 'info', 'warn', 'error', 'toast']);
|
||
|
||
// Shortcuts
|
||
const [shortcuts, setShortcutsState] = useState<Record<string, Shortcut>>(DEFAULT_SHORTCUTS);
|
||
|
||
// Custom AI Commands
|
||
const [customAICommands, setCustomAICommandsState] = useState<CustomAICommand[]>(DEFAULT_AI_COMMANDS);
|
||
|
||
// Global Stats (persistent)
|
||
const [globalStats, setGlobalStatsState] = useState<GlobalStats>(DEFAULT_GLOBAL_STATS);
|
||
|
||
// Auto-run Stats (persistent)
|
||
const [autoRunStats, setAutoRunStatsState] = useState<AutoRunStats>(DEFAULT_AUTO_RUN_STATS);
|
||
|
||
// UI collapse states (persistent)
|
||
const [ungroupedCollapsed, setUngroupedCollapsedState] = useState(false);
|
||
|
||
// Onboarding settings (persistent)
|
||
const [wizardCompleted, setWizardCompletedState] = useState(false);
|
||
const [tourCompleted, setTourCompletedState] = useState(false);
|
||
const [firstAutoRunCompleted, setFirstAutoRunCompletedState] = useState(false);
|
||
|
||
// Onboarding Stats (persistent, local-only analytics)
|
||
const [onboardingStats, setOnboardingStatsState] = useState<OnboardingStats>(DEFAULT_ONBOARDING_STATS);
|
||
|
||
// Leaderboard Registration (persistent)
|
||
const [leaderboardRegistration, setLeaderboardRegistrationState] = useState<LeaderboardRegistration | null>(null);
|
||
|
||
// Wrapper functions that persist to electron-store
|
||
// PERF: All wrapped in useCallback to prevent re-renders
|
||
const setLlmProvider = useCallback((value: LLMProvider) => {
|
||
setLlmProviderState(value);
|
||
window.maestro.settings.set('llmProvider', value);
|
||
}, []);
|
||
|
||
const setModelSlug = useCallback((value: string) => {
|
||
setModelSlugState(value);
|
||
window.maestro.settings.set('modelSlug', value);
|
||
}, []);
|
||
|
||
const setApiKey = useCallback((value: string) => {
|
||
setApiKeyState(value);
|
||
window.maestro.settings.set('apiKey', value);
|
||
}, []);
|
||
|
||
const setDefaultAgent = useCallback((value: string) => {
|
||
setDefaultAgentState(value);
|
||
window.maestro.settings.set('defaultAgent', value);
|
||
}, []);
|
||
|
||
const setDefaultShell = useCallback((value: string) => {
|
||
setDefaultShellState(value);
|
||
window.maestro.settings.set('defaultShell', value);
|
||
}, []);
|
||
|
||
const setGhPath = useCallback((value: string) => {
|
||
setGhPathState(value);
|
||
window.maestro.settings.set('ghPath', value);
|
||
}, []);
|
||
|
||
const setFontFamily = useCallback((value: string) => {
|
||
setFontFamilyState(value);
|
||
window.maestro.settings.set('fontFamily', value);
|
||
}, []);
|
||
|
||
const setFontSize = useCallback((value: number) => {
|
||
setFontSizeState(value);
|
||
window.maestro.settings.set('fontSize', value);
|
||
}, []);
|
||
|
||
const setCustomFonts = useCallback((value: string[]) => {
|
||
setCustomFontsState(value);
|
||
window.maestro.settings.set('customFonts', value);
|
||
}, []);
|
||
|
||
const setActiveThemeId = useCallback((value: ThemeId) => {
|
||
setActiveThemeIdState(value);
|
||
window.maestro.settings.set('activeThemeId', value);
|
||
}, []);
|
||
|
||
const setCustomThemeColors = useCallback((value: ThemeColors) => {
|
||
setCustomThemeColorsState(value);
|
||
window.maestro.settings.set('customThemeColors', value);
|
||
}, []);
|
||
|
||
const setCustomThemeBaseId = useCallback((value: ThemeId) => {
|
||
setCustomThemeBaseIdState(value);
|
||
window.maestro.settings.set('customThemeBaseId', value);
|
||
}, []);
|
||
|
||
const setEnterToSendAI = useCallback((value: boolean) => {
|
||
setEnterToSendAIState(value);
|
||
window.maestro.settings.set('enterToSendAI', value);
|
||
}, []);
|
||
|
||
const setEnterToSendTerminal = useCallback((value: boolean) => {
|
||
setEnterToSendTerminalState(value);
|
||
window.maestro.settings.set('enterToSendTerminal', value);
|
||
}, []);
|
||
|
||
const setDefaultSaveToHistory = useCallback((value: boolean) => {
|
||
setDefaultSaveToHistoryState(value);
|
||
window.maestro.settings.set('defaultSaveToHistory', value);
|
||
}, []);
|
||
|
||
const setLeftSidebarWidth = useCallback((width: number) => {
|
||
const clampedWidth = Math.max(256, Math.min(600, width));
|
||
setLeftSidebarWidthState(clampedWidth);
|
||
window.maestro.settings.set('leftSidebarWidth', clampedWidth);
|
||
}, []);
|
||
|
||
const setRightPanelWidth = useCallback((width: number) => {
|
||
setRightPanelWidthState(width);
|
||
window.maestro.settings.set('rightPanelWidth', width);
|
||
}, []);
|
||
|
||
const setMarkdownEditMode = useCallback((value: boolean) => {
|
||
setMarkdownEditModeState(value);
|
||
window.maestro.settings.set('markdownEditMode', value);
|
||
}, []);
|
||
|
||
const setShowHiddenFiles = useCallback((value: boolean) => {
|
||
setShowHiddenFilesState(value);
|
||
window.maestro.settings.set('showHiddenFiles', value);
|
||
}, []);
|
||
|
||
const setShortcuts = useCallback((value: Record<string, Shortcut>) => {
|
||
setShortcutsState(value);
|
||
window.maestro.settings.set('shortcuts', value);
|
||
}, []);
|
||
|
||
const setTerminalWidth = useCallback((value: number) => {
|
||
setTerminalWidthState(value);
|
||
window.maestro.settings.set('terminalWidth', value);
|
||
}, []);
|
||
|
||
const setLogLevel = useCallback(async (value: string) => {
|
||
setLogLevelState(value);
|
||
await window.maestro.logger.setLogLevel(value);
|
||
}, []);
|
||
|
||
const setMaxLogBuffer = useCallback(async (value: number) => {
|
||
setMaxLogBufferState(value);
|
||
await window.maestro.logger.setMaxLogBuffer(value);
|
||
}, []);
|
||
|
||
const setMaxOutputLines = useCallback((value: number) => {
|
||
setMaxOutputLinesState(value);
|
||
window.maestro.settings.set('maxOutputLines', value);
|
||
}, []);
|
||
|
||
const setOsNotificationsEnabled = useCallback((value: boolean) => {
|
||
setOsNotificationsEnabledState(value);
|
||
window.maestro.settings.set('osNotificationsEnabled', value);
|
||
}, []);
|
||
|
||
const setAudioFeedbackEnabled = useCallback((value: boolean) => {
|
||
setAudioFeedbackEnabledState(value);
|
||
window.maestro.settings.set('audioFeedbackEnabled', value);
|
||
}, []);
|
||
|
||
const setAudioFeedbackCommand = useCallback((value: string) => {
|
||
setAudioFeedbackCommandState(value);
|
||
window.maestro.settings.set('audioFeedbackCommand', value);
|
||
}, []);
|
||
|
||
const setToastDuration = useCallback((value: number) => {
|
||
setToastDurationState(value);
|
||
window.maestro.settings.set('toastDuration', value);
|
||
}, []);
|
||
|
||
const setCheckForUpdatesOnStartup = useCallback((value: boolean) => {
|
||
setCheckForUpdatesOnStartupState(value);
|
||
window.maestro.settings.set('checkForUpdatesOnStartup', value);
|
||
}, []);
|
||
|
||
const setLogViewerSelectedLevels = useCallback((value: string[]) => {
|
||
setLogViewerSelectedLevelsState(value);
|
||
window.maestro.settings.set('logViewerSelectedLevels', value);
|
||
}, []);
|
||
|
||
const setCustomAICommands = useCallback((value: CustomAICommand[]) => {
|
||
setCustomAICommandsState(value);
|
||
window.maestro.settings.set('customAICommands', value);
|
||
}, []);
|
||
|
||
const setGlobalStats = useCallback((value: GlobalStats) => {
|
||
setGlobalStatsState(value);
|
||
window.maestro.settings.set('globalStats', value);
|
||
}, []);
|
||
|
||
// Update global stats by adding deltas to existing values
|
||
const updateGlobalStats = useCallback((delta: Partial<GlobalStats>) => {
|
||
setGlobalStatsState(prev => {
|
||
const updated: GlobalStats = {
|
||
totalSessions: prev.totalSessions + (delta.totalSessions || 0),
|
||
totalMessages: prev.totalMessages + (delta.totalMessages || 0),
|
||
totalInputTokens: prev.totalInputTokens + (delta.totalInputTokens || 0),
|
||
totalOutputTokens: prev.totalOutputTokens + (delta.totalOutputTokens || 0),
|
||
totalCacheReadTokens: prev.totalCacheReadTokens + (delta.totalCacheReadTokens || 0),
|
||
totalCacheCreationTokens: prev.totalCacheCreationTokens + (delta.totalCacheCreationTokens || 0),
|
||
totalCostUsd: prev.totalCostUsd + (delta.totalCostUsd || 0),
|
||
totalActiveTimeMs: prev.totalActiveTimeMs + (delta.totalActiveTimeMs || 0),
|
||
};
|
||
window.maestro.settings.set('globalStats', updated);
|
||
return updated;
|
||
});
|
||
}, []);
|
||
|
||
const setAutoRunStats = useCallback((value: AutoRunStats) => {
|
||
setAutoRunStatsState(value);
|
||
window.maestro.settings.set('autoRunStats', value);
|
||
}, []);
|
||
|
||
// Import badge calculation from constants (moved inline to avoid circular dependency)
|
||
const getBadgeLevelForTime = (cumulativeTimeMs: number): number => {
|
||
// Time thresholds in milliseconds
|
||
const MINUTE = 60 * 1000;
|
||
const HOUR = 60 * MINUTE;
|
||
const DAY = 24 * HOUR;
|
||
const WEEK = 7 * DAY;
|
||
const MONTH = 30 * DAY;
|
||
|
||
const thresholds = [
|
||
15 * MINUTE, // Level 1: 15 minutes
|
||
1 * HOUR, // Level 2: 1 hour
|
||
8 * HOUR, // Level 3: 8 hours
|
||
1 * DAY, // Level 4: 1 day
|
||
1 * WEEK, // Level 5: 1 week
|
||
1 * MONTH, // Level 6: 1 month
|
||
3 * MONTH, // Level 7: 3 months
|
||
6 * MONTH, // Level 8: 6 months
|
||
365 * DAY, // Level 9: 1 year
|
||
5 * 365 * DAY, // Level 10: 5 years
|
||
10 * 365 * DAY, // Level 11: 10 years
|
||
];
|
||
|
||
let level = 0;
|
||
for (let i = 0; i < thresholds.length; i++) {
|
||
if (cumulativeTimeMs >= thresholds[i]) {
|
||
level = i + 1;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
return level;
|
||
};
|
||
|
||
// Record an auto-run completion and check for new badges/records
|
||
// NOTE: Cumulative time is tracked incrementally during the run via updateAutoRunProgress(),
|
||
// so we don't add elapsedTimeMs to cumulative here - only check for longest run record and increment totalRuns
|
||
const recordAutoRunComplete = useCallback((elapsedTimeMs: number): { newBadgeLevel: number | null; isNewRecord: boolean } => {
|
||
let newBadgeLevel: number | null = null;
|
||
let isNewRecord = false;
|
||
|
||
setAutoRunStatsState(prev => {
|
||
// Don't add to cumulative time - it was already added incrementally during the run
|
||
// Just check current badge level in case a badge wasn't triggered during incremental updates
|
||
const newBadgeLevelCalc = getBadgeLevelForTime(prev.cumulativeTimeMs);
|
||
|
||
// Check if this would be a new badge (edge case: badge threshold crossed between updates)
|
||
if (newBadgeLevelCalc > prev.lastBadgeUnlockLevel) {
|
||
newBadgeLevel = newBadgeLevelCalc;
|
||
}
|
||
|
||
// Check if this is a new longest run record
|
||
isNewRecord = elapsedTimeMs > prev.longestRunMs;
|
||
|
||
// Build updated badge history if new badge unlocked
|
||
let updatedBadgeHistory = prev.badgeHistory || [];
|
||
if (newBadgeLevel !== null) {
|
||
updatedBadgeHistory = [
|
||
...updatedBadgeHistory,
|
||
{ level: newBadgeLevel, unlockedAt: Date.now() }
|
||
];
|
||
}
|
||
|
||
const updated: AutoRunStats = {
|
||
cumulativeTimeMs: prev.cumulativeTimeMs, // Already updated incrementally
|
||
longestRunMs: isNewRecord ? elapsedTimeMs : prev.longestRunMs,
|
||
longestRunTimestamp: isNewRecord ? Date.now() : prev.longestRunTimestamp,
|
||
totalRuns: prev.totalRuns + 1,
|
||
currentBadgeLevel: newBadgeLevelCalc,
|
||
lastBadgeUnlockLevel: newBadgeLevel !== null ? newBadgeLevelCalc : prev.lastBadgeUnlockLevel,
|
||
lastAcknowledgedBadgeLevel: prev.lastAcknowledgedBadgeLevel ?? 0,
|
||
badgeHistory: updatedBadgeHistory,
|
||
};
|
||
|
||
window.maestro.settings.set('autoRunStats', updated);
|
||
return updated;
|
||
});
|
||
|
||
return { newBadgeLevel, isNewRecord };
|
||
}, []);
|
||
|
||
// Track progress during an active auto-run (called periodically, e.g., every minute)
|
||
// deltaMs is the time elapsed since the last call (NOT total elapsed time)
|
||
// This updates cumulative time and longest run WITHOUT incrementing totalRuns
|
||
// Returns badge/record info so caller can show standing ovation during run
|
||
const updateAutoRunProgress = useCallback((deltaMs: number): { newBadgeLevel: number | null; isNewRecord: boolean } => {
|
||
let newBadgeLevel: number | null = null;
|
||
let isNewRecord = false;
|
||
|
||
setAutoRunStatsState(prev => {
|
||
// Add the delta to cumulative time
|
||
const newCumulativeTime = prev.cumulativeTimeMs + deltaMs;
|
||
const newBadgeLevelCalc = getBadgeLevelForTime(newCumulativeTime);
|
||
|
||
// Check if this unlocks a new badge
|
||
if (newBadgeLevelCalc > prev.lastBadgeUnlockLevel) {
|
||
newBadgeLevel = newBadgeLevelCalc;
|
||
}
|
||
|
||
// Note: We don't update longestRunMs here because we don't know the total
|
||
// run time yet. That's handled when the run completes.
|
||
|
||
// Build updated badge history if new badge unlocked
|
||
let updatedBadgeHistory = prev.badgeHistory || [];
|
||
if (newBadgeLevel !== null) {
|
||
updatedBadgeHistory = [
|
||
...updatedBadgeHistory,
|
||
{ level: newBadgeLevel, unlockedAt: Date.now() }
|
||
];
|
||
}
|
||
|
||
const updated: AutoRunStats = {
|
||
cumulativeTimeMs: newCumulativeTime,
|
||
longestRunMs: prev.longestRunMs, // Don't update until run completes
|
||
longestRunTimestamp: prev.longestRunTimestamp,
|
||
totalRuns: prev.totalRuns, // Don't increment - run not complete yet
|
||
currentBadgeLevel: newBadgeLevelCalc,
|
||
lastBadgeUnlockLevel: newBadgeLevel !== null ? newBadgeLevelCalc : prev.lastBadgeUnlockLevel,
|
||
lastAcknowledgedBadgeLevel: prev.lastAcknowledgedBadgeLevel ?? 0,
|
||
badgeHistory: updatedBadgeHistory,
|
||
};
|
||
|
||
window.maestro.settings.set('autoRunStats', updated);
|
||
return updated;
|
||
});
|
||
|
||
return { newBadgeLevel, isNewRecord };
|
||
}, []);
|
||
|
||
// Acknowledge that user has seen the standing ovation for a badge level
|
||
const acknowledgeBadge = useCallback((level: number) => {
|
||
setAutoRunStatsState(prev => {
|
||
const updated: AutoRunStats = {
|
||
...prev,
|
||
lastAcknowledgedBadgeLevel: Math.max(level, prev.lastAcknowledgedBadgeLevel ?? 0),
|
||
};
|
||
window.maestro.settings.set('autoRunStats', updated);
|
||
return updated;
|
||
});
|
||
}, []);
|
||
|
||
// Get the highest unacknowledged badge level (if any)
|
||
const getUnacknowledgedBadgeLevel = useCallback((): number | null => {
|
||
const acknowledged = autoRunStats.lastAcknowledgedBadgeLevel ?? 0;
|
||
const current = autoRunStats.currentBadgeLevel;
|
||
if (current > acknowledged) {
|
||
return current;
|
||
}
|
||
return null;
|
||
}, [autoRunStats.lastAcknowledgedBadgeLevel, autoRunStats.currentBadgeLevel]);
|
||
|
||
// UI collapse state setters
|
||
const setUngroupedCollapsed = useCallback((value: boolean) => {
|
||
console.log('[useSettings] setUngroupedCollapsed called with:', value);
|
||
setUngroupedCollapsedState(value);
|
||
window.maestro.settings.set('ungroupedCollapsed', value)
|
||
.then(() => console.log('[useSettings] ungroupedCollapsed persisted successfully'))
|
||
.catch((err: unknown) => console.error('[useSettings] Failed to persist ungroupedCollapsed:', err));
|
||
}, []);
|
||
|
||
// Onboarding setters
|
||
const setWizardCompleted = useCallback((value: boolean) => {
|
||
setWizardCompletedState(value);
|
||
window.maestro.settings.set('wizardCompleted', value);
|
||
}, []);
|
||
|
||
const setTourCompleted = useCallback((value: boolean) => {
|
||
setTourCompletedState(value);
|
||
window.maestro.settings.set('tourCompleted', value);
|
||
}, []);
|
||
|
||
const setFirstAutoRunCompleted = useCallback((value: boolean) => {
|
||
setFirstAutoRunCompletedState(value);
|
||
window.maestro.settings.set('firstAutoRunCompleted', value);
|
||
}, []);
|
||
|
||
// Onboarding Stats functions
|
||
const setOnboardingStats = useCallback((value: OnboardingStats) => {
|
||
setOnboardingStatsState(value);
|
||
window.maestro.settings.set('onboardingStats', value);
|
||
}, []);
|
||
|
||
// Record when wizard is started
|
||
const recordWizardStart = useCallback(() => {
|
||
setOnboardingStatsState(prev => {
|
||
const updated: OnboardingStats = {
|
||
...prev,
|
||
wizardStartCount: prev.wizardStartCount + 1,
|
||
};
|
||
window.maestro.settings.set('onboardingStats', updated);
|
||
return updated;
|
||
});
|
||
}, []);
|
||
|
||
// Record when wizard is completed successfully
|
||
const recordWizardComplete = useCallback((
|
||
durationMs: number,
|
||
conversationExchanges: number,
|
||
phasesGenerated: number,
|
||
tasksGenerated: number
|
||
) => {
|
||
setOnboardingStatsState(prev => {
|
||
const newCompletionCount = prev.wizardCompletionCount + 1;
|
||
const newTotalDuration = prev.totalWizardDurationMs + durationMs;
|
||
const newTotalExchanges = prev.totalConversationExchanges + conversationExchanges;
|
||
const newTotalPhases = prev.totalPhasesGenerated + phasesGenerated;
|
||
const newTotalTasks = prev.totalTasksGenerated + tasksGenerated;
|
||
|
||
const updated: OnboardingStats = {
|
||
...prev,
|
||
wizardCompletionCount: newCompletionCount,
|
||
totalWizardDurationMs: newTotalDuration,
|
||
averageWizardDurationMs: Math.round(newTotalDuration / newCompletionCount),
|
||
lastWizardCompletedAt: Date.now(),
|
||
|
||
// Conversation stats
|
||
totalConversationExchanges: newTotalExchanges,
|
||
totalConversationsCompleted: prev.totalConversationsCompleted + 1,
|
||
averageConversationExchanges: newCompletionCount > 0
|
||
? Math.round((newTotalExchanges / newCompletionCount) * 10) / 10
|
||
: 0,
|
||
|
||
// Phase generation stats
|
||
totalPhasesGenerated: newTotalPhases,
|
||
averagePhasesPerWizard: newCompletionCount > 0
|
||
? Math.round((newTotalPhases / newCompletionCount) * 10) / 10
|
||
: 0,
|
||
totalTasksGenerated: newTotalTasks,
|
||
averageTasksPerPhase: newTotalPhases > 0
|
||
? Math.round((newTotalTasks / newTotalPhases) * 10) / 10
|
||
: 0,
|
||
};
|
||
window.maestro.settings.set('onboardingStats', updated);
|
||
return updated;
|
||
});
|
||
}, []);
|
||
|
||
// Record when wizard is abandoned (closed before completion)
|
||
const recordWizardAbandon = useCallback(() => {
|
||
setOnboardingStatsState(prev => {
|
||
const updated: OnboardingStats = {
|
||
...prev,
|
||
wizardAbandonCount: prev.wizardAbandonCount + 1,
|
||
};
|
||
window.maestro.settings.set('onboardingStats', updated);
|
||
return updated;
|
||
});
|
||
}, []);
|
||
|
||
// Record when wizard is resumed from saved state
|
||
const recordWizardResume = useCallback(() => {
|
||
setOnboardingStatsState(prev => {
|
||
const updated: OnboardingStats = {
|
||
...prev,
|
||
wizardResumeCount: prev.wizardResumeCount + 1,
|
||
};
|
||
window.maestro.settings.set('onboardingStats', updated);
|
||
return updated;
|
||
});
|
||
}, []);
|
||
|
||
// Record when tour is started
|
||
const recordTourStart = useCallback(() => {
|
||
setOnboardingStatsState(prev => {
|
||
const updated: OnboardingStats = {
|
||
...prev,
|
||
tourStartCount: prev.tourStartCount + 1,
|
||
};
|
||
window.maestro.settings.set('onboardingStats', updated);
|
||
return updated;
|
||
});
|
||
}, []);
|
||
|
||
// Record when tour is completed (all steps viewed)
|
||
const recordTourComplete = useCallback((stepsViewed: number) => {
|
||
setOnboardingStatsState(prev => {
|
||
const newCompletionCount = prev.tourCompletionCount + 1;
|
||
const newTotalStepsViewed = prev.tourStepsViewedTotal + stepsViewed;
|
||
const totalTours = newCompletionCount + prev.tourSkipCount;
|
||
|
||
const updated: OnboardingStats = {
|
||
...prev,
|
||
tourCompletionCount: newCompletionCount,
|
||
tourStepsViewedTotal: newTotalStepsViewed,
|
||
averageTourStepsViewed: totalTours > 0
|
||
? Math.round((newTotalStepsViewed / totalTours) * 10) / 10
|
||
: stepsViewed,
|
||
};
|
||
window.maestro.settings.set('onboardingStats', updated);
|
||
return updated;
|
||
});
|
||
}, []);
|
||
|
||
// Record when tour is skipped before completion
|
||
const recordTourSkip = useCallback((stepsViewed: number) => {
|
||
setOnboardingStatsState(prev => {
|
||
const newSkipCount = prev.tourSkipCount + 1;
|
||
const newTotalStepsViewed = prev.tourStepsViewedTotal + stepsViewed;
|
||
const totalTours = prev.tourCompletionCount + newSkipCount;
|
||
|
||
const updated: OnboardingStats = {
|
||
...prev,
|
||
tourSkipCount: newSkipCount,
|
||
tourStepsViewedTotal: newTotalStepsViewed,
|
||
averageTourStepsViewed: totalTours > 0
|
||
? Math.round((newTotalStepsViewed / totalTours) * 10) / 10
|
||
: stepsViewed,
|
||
};
|
||
window.maestro.settings.set('onboardingStats', updated);
|
||
return updated;
|
||
});
|
||
}, []);
|
||
|
||
// Get computed analytics for display
|
||
const getOnboardingAnalytics = useCallback(() => {
|
||
const totalWizardAttempts = onboardingStats.wizardStartCount;
|
||
const totalTourAttempts = onboardingStats.tourStartCount;
|
||
|
||
return {
|
||
wizardCompletionRate: totalWizardAttempts > 0
|
||
? Math.round((onboardingStats.wizardCompletionCount / totalWizardAttempts) * 100)
|
||
: 0,
|
||
tourCompletionRate: totalTourAttempts > 0
|
||
? Math.round((onboardingStats.tourCompletionCount / totalTourAttempts) * 100)
|
||
: 0,
|
||
averageConversationExchanges: onboardingStats.averageConversationExchanges,
|
||
averagePhasesPerWizard: onboardingStats.averagePhasesPerWizard,
|
||
};
|
||
}, [
|
||
onboardingStats.wizardStartCount,
|
||
onboardingStats.tourStartCount,
|
||
onboardingStats.wizardCompletionCount,
|
||
onboardingStats.tourCompletionCount,
|
||
onboardingStats.averageConversationExchanges,
|
||
onboardingStats.averagePhasesPerWizard,
|
||
]);
|
||
|
||
// Leaderboard Registration setter
|
||
const setLeaderboardRegistration = useCallback((value: LeaderboardRegistration | null) => {
|
||
setLeaderboardRegistrationState(value);
|
||
window.maestro.settings.set('leaderboardRegistration', value);
|
||
}, []);
|
||
|
||
// Computed property for checking if registered
|
||
const isLeaderboardRegistered = useMemo(() => {
|
||
return leaderboardRegistration !== null && leaderboardRegistration.emailConfirmed;
|
||
}, [leaderboardRegistration]);
|
||
|
||
// Load settings from electron-store on mount
|
||
useEffect(() => {
|
||
const loadSettings = async () => {
|
||
const savedEnterToSendAI = await window.maestro.settings.get('enterToSendAI');
|
||
const savedEnterToSendTerminal = await window.maestro.settings.get('enterToSendTerminal');
|
||
const savedDefaultSaveToHistory = await window.maestro.settings.get('defaultSaveToHistory');
|
||
|
||
const savedLlmProvider = await window.maestro.settings.get('llmProvider');
|
||
const savedModelSlug = await window.maestro.settings.get('modelSlug');
|
||
const savedApiKey = await window.maestro.settings.get('apiKey');
|
||
const savedDefaultAgent = await window.maestro.settings.get('defaultAgent');
|
||
const savedDefaultShell = await window.maestro.settings.get('defaultShell');
|
||
const savedGhPath = await window.maestro.settings.get('ghPath');
|
||
const savedFontSize = await window.maestro.settings.get('fontSize');
|
||
const savedFontFamily = await window.maestro.settings.get('fontFamily');
|
||
const savedCustomFonts = await window.maestro.settings.get('customFonts');
|
||
const savedLeftSidebarWidth = await window.maestro.settings.get('leftSidebarWidth');
|
||
const savedRightPanelWidth = await window.maestro.settings.get('rightPanelWidth');
|
||
const savedMarkdownEditMode = await window.maestro.settings.get('markdownEditMode');
|
||
const savedShowHiddenFiles = await window.maestro.settings.get('showHiddenFiles');
|
||
const savedShortcuts = await window.maestro.settings.get('shortcuts');
|
||
const savedActiveThemeId = await window.maestro.settings.get('activeThemeId');
|
||
const savedCustomThemeColors = await window.maestro.settings.get('customThemeColors');
|
||
const savedCustomThemeBaseId = await window.maestro.settings.get('customThemeBaseId');
|
||
const savedTerminalWidth = await window.maestro.settings.get('terminalWidth');
|
||
const savedLogLevel = await window.maestro.logger.getLogLevel();
|
||
const savedMaxLogBuffer = await window.maestro.logger.getMaxLogBuffer();
|
||
const savedMaxOutputLines = await window.maestro.settings.get('maxOutputLines');
|
||
const savedOsNotificationsEnabled = await window.maestro.settings.get('osNotificationsEnabled');
|
||
const savedAudioFeedbackEnabled = await window.maestro.settings.get('audioFeedbackEnabled');
|
||
const savedAudioFeedbackCommand = await window.maestro.settings.get('audioFeedbackCommand');
|
||
const savedToastDuration = await window.maestro.settings.get('toastDuration');
|
||
const savedCheckForUpdatesOnStartup = await window.maestro.settings.get('checkForUpdatesOnStartup');
|
||
const savedLogViewerSelectedLevels = await window.maestro.settings.get('logViewerSelectedLevels');
|
||
const savedCustomAICommands = await window.maestro.settings.get('customAICommands');
|
||
const savedGlobalStats = await window.maestro.settings.get('globalStats');
|
||
const savedAutoRunStats = await window.maestro.settings.get('autoRunStats');
|
||
const savedUngroupedCollapsed = await window.maestro.settings.get('ungroupedCollapsed');
|
||
const savedWizardCompleted = await window.maestro.settings.get('wizardCompleted');
|
||
const savedTourCompleted = await window.maestro.settings.get('tourCompleted');
|
||
const savedFirstAutoRunCompleted = await window.maestro.settings.get('firstAutoRunCompleted');
|
||
const savedOnboardingStats = await window.maestro.settings.get('onboardingStats');
|
||
const savedLeaderboardRegistration = await window.maestro.settings.get('leaderboardRegistration');
|
||
|
||
if (savedEnterToSendAI !== undefined) setEnterToSendAIState(savedEnterToSendAI);
|
||
if (savedEnterToSendTerminal !== undefined) setEnterToSendTerminalState(savedEnterToSendTerminal);
|
||
if (savedDefaultSaveToHistory !== undefined) setDefaultSaveToHistoryState(savedDefaultSaveToHistory);
|
||
|
||
if (savedLlmProvider !== undefined) setLlmProviderState(savedLlmProvider);
|
||
if (savedModelSlug !== undefined) setModelSlugState(savedModelSlug);
|
||
if (savedApiKey !== undefined) setApiKeyState(savedApiKey);
|
||
if (savedDefaultAgent !== undefined) setDefaultAgentState(savedDefaultAgent);
|
||
if (savedDefaultShell !== undefined) setDefaultShellState(savedDefaultShell);
|
||
if (savedGhPath !== undefined) setGhPathState(savedGhPath);
|
||
if (savedFontSize !== undefined) setFontSizeState(savedFontSize);
|
||
if (savedFontFamily !== undefined) setFontFamilyState(savedFontFamily);
|
||
if (savedCustomFonts !== undefined) setCustomFontsState(savedCustomFonts);
|
||
if (savedLeftSidebarWidth !== undefined) setLeftSidebarWidthState(Math.max(256, Math.min(600, savedLeftSidebarWidth)));
|
||
if (savedRightPanelWidth !== undefined) setRightPanelWidthState(savedRightPanelWidth);
|
||
if (savedMarkdownEditMode !== undefined) setMarkdownEditModeState(savedMarkdownEditMode);
|
||
if (savedShowHiddenFiles !== undefined) setShowHiddenFilesState(savedShowHiddenFiles);
|
||
if (savedActiveThemeId !== undefined) setActiveThemeIdState(savedActiveThemeId);
|
||
if (savedCustomThemeColors !== undefined) setCustomThemeColorsState(savedCustomThemeColors);
|
||
if (savedCustomThemeBaseId !== undefined) setCustomThemeBaseIdState(savedCustomThemeBaseId);
|
||
if (savedTerminalWidth !== undefined) setTerminalWidthState(savedTerminalWidth);
|
||
if (savedLogLevel !== undefined) setLogLevelState(savedLogLevel);
|
||
if (savedMaxLogBuffer !== undefined) setMaxLogBufferState(savedMaxLogBuffer);
|
||
if (savedMaxOutputLines !== undefined) setMaxOutputLinesState(savedMaxOutputLines);
|
||
if (savedOsNotificationsEnabled !== undefined) setOsNotificationsEnabledState(savedOsNotificationsEnabled);
|
||
if (savedAudioFeedbackEnabled !== undefined) setAudioFeedbackEnabledState(savedAudioFeedbackEnabled);
|
||
if (savedAudioFeedbackCommand !== undefined) setAudioFeedbackCommandState(savedAudioFeedbackCommand);
|
||
if (savedToastDuration !== undefined) setToastDurationState(savedToastDuration);
|
||
if (savedCheckForUpdatesOnStartup !== undefined) setCheckForUpdatesOnStartupState(savedCheckForUpdatesOnStartup);
|
||
if (savedLogViewerSelectedLevels !== undefined) setLogViewerSelectedLevelsState(savedLogViewerSelectedLevels);
|
||
|
||
// Merge saved shortcuts with defaults (in case new shortcuts were added)
|
||
if (savedShortcuts !== undefined) {
|
||
// Migration: Fix shortcuts that were recorded with macOS Alt+key special characters
|
||
// On macOS, Alt+L produces '¬', Alt+P produces 'π', etc. These should be 'l', 'p', etc.
|
||
const macAltCharMap: Record<string, string> = {
|
||
'¬': 'l', // Alt+L
|
||
'π': 'p', // Alt+P
|
||
'†': 't', // Alt+T
|
||
'∫': 'b', // Alt+B
|
||
'∂': 'd', // Alt+D
|
||
'ƒ': 'f', // Alt+F
|
||
'©': 'g', // Alt+G
|
||
'˙': 'h', // Alt+H
|
||
'ˆ': 'i', // Alt+I (circumflex)
|
||
'∆': 'j', // Alt+J
|
||
'˚': 'k', // Alt+K
|
||
'¯': 'm', // Alt+M (macron, though some keyboards differ)
|
||
'˜': 'n', // Alt+N
|
||
'ø': 'o', // Alt+O
|
||
'®': 'r', // Alt+R
|
||
'ß': 's', // Alt+S
|
||
'√': 'v', // Alt+V
|
||
'∑': 'w', // Alt+W
|
||
'≈': 'x', // Alt+X
|
||
'¥': 'y', // Alt+Y
|
||
'Ω': 'z', // Alt+Z
|
||
};
|
||
|
||
const migratedShortcuts: Record<string, Shortcut> = {};
|
||
let needsMigration = false;
|
||
|
||
for (const [id, shortcut] of Object.entries(savedShortcuts as Record<string, Shortcut>)) {
|
||
const migratedKeys = shortcut.keys.map(key => {
|
||
if (macAltCharMap[key]) {
|
||
needsMigration = true;
|
||
return macAltCharMap[key];
|
||
}
|
||
return key;
|
||
});
|
||
migratedShortcuts[id] = { ...shortcut, keys: migratedKeys };
|
||
}
|
||
|
||
// If migration was needed, save the corrected shortcuts
|
||
if (needsMigration) {
|
||
window.maestro.settings.set('shortcuts', migratedShortcuts);
|
||
}
|
||
|
||
// Merge: use default labels (in case they changed) but preserve user's custom keys
|
||
const mergedShortcuts: Record<string, Shortcut> = {};
|
||
for (const [id, defaultShortcut] of Object.entries(DEFAULT_SHORTCUTS)) {
|
||
const savedShortcut = migratedShortcuts[id];
|
||
mergedShortcuts[id] = {
|
||
...defaultShortcut,
|
||
// Preserve user's custom keys if they exist
|
||
keys: savedShortcut?.keys ?? defaultShortcut.keys,
|
||
};
|
||
}
|
||
setShortcutsState(mergedShortcuts);
|
||
}
|
||
|
||
// Merge saved AI commands with defaults (ensure built-in commands always exist)
|
||
if (savedCustomAICommands !== undefined) {
|
||
// Start with defaults, then merge saved commands (by ID to avoid duplicates)
|
||
const commandsById = new Map<string, CustomAICommand>();
|
||
DEFAULT_AI_COMMANDS.forEach(cmd => commandsById.set(cmd.id, cmd));
|
||
savedCustomAICommands.forEach((cmd: CustomAICommand) => {
|
||
// For built-in commands, merge to allow user edits but preserve isBuiltIn flag
|
||
if (commandsById.has(cmd.id)) {
|
||
const existing = commandsById.get(cmd.id)!;
|
||
commandsById.set(cmd.id, { ...cmd, isBuiltIn: existing.isBuiltIn });
|
||
} else {
|
||
commandsById.set(cmd.id, cmd);
|
||
}
|
||
});
|
||
setCustomAICommandsState(Array.from(commandsById.values()));
|
||
}
|
||
|
||
// Load global stats
|
||
if (savedGlobalStats !== undefined) {
|
||
setGlobalStatsState({ ...DEFAULT_GLOBAL_STATS, ...savedGlobalStats });
|
||
}
|
||
|
||
// Load auto-run stats
|
||
if (savedAutoRunStats !== undefined) {
|
||
setAutoRunStatsState({ ...DEFAULT_AUTO_RUN_STATS, ...savedAutoRunStats });
|
||
}
|
||
|
||
// Load onboarding settings
|
||
// UI collapse states
|
||
if (savedUngroupedCollapsed !== undefined) setUngroupedCollapsedState(savedUngroupedCollapsed);
|
||
|
||
if (savedWizardCompleted !== undefined) setWizardCompletedState(savedWizardCompleted);
|
||
if (savedTourCompleted !== undefined) setTourCompletedState(savedTourCompleted);
|
||
if (savedFirstAutoRunCompleted !== undefined) setFirstAutoRunCompletedState(savedFirstAutoRunCompleted);
|
||
|
||
// Load onboarding stats
|
||
if (savedOnboardingStats !== undefined) {
|
||
setOnboardingStatsState({ ...DEFAULT_ONBOARDING_STATS, ...savedOnboardingStats });
|
||
}
|
||
|
||
// Load leaderboard registration
|
||
if (savedLeaderboardRegistration !== undefined) {
|
||
setLeaderboardRegistrationState(savedLeaderboardRegistration as LeaderboardRegistration | null);
|
||
}
|
||
|
||
// Mark settings as loaded
|
||
setSettingsLoaded(true);
|
||
};
|
||
loadSettings();
|
||
}, []);
|
||
|
||
// Apply font size to HTML root element so rem-based Tailwind classes scale
|
||
// Only apply after settings are loaded to prevent layout shift from default->saved font size
|
||
useEffect(() => {
|
||
if (settingsLoaded) {
|
||
document.documentElement.style.fontSize = `${fontSize}px`;
|
||
}
|
||
}, [fontSize, settingsLoaded]);
|
||
|
||
// PERF: Memoize return object to prevent unnecessary re-renders in consumers
|
||
return useMemo(() => ({
|
||
settingsLoaded,
|
||
llmProvider,
|
||
modelSlug,
|
||
apiKey,
|
||
setLlmProvider,
|
||
setModelSlug,
|
||
setApiKey,
|
||
defaultAgent,
|
||
setDefaultAgent,
|
||
defaultShell,
|
||
setDefaultShell,
|
||
ghPath,
|
||
setGhPath,
|
||
fontFamily,
|
||
fontSize,
|
||
customFonts,
|
||
setFontFamily,
|
||
setFontSize,
|
||
setCustomFonts,
|
||
activeThemeId,
|
||
setActiveThemeId,
|
||
customThemeColors,
|
||
setCustomThemeColors,
|
||
customThemeBaseId,
|
||
setCustomThemeBaseId,
|
||
enterToSendAI,
|
||
setEnterToSendAI,
|
||
enterToSendTerminal,
|
||
setEnterToSendTerminal,
|
||
defaultSaveToHistory,
|
||
setDefaultSaveToHistory,
|
||
leftSidebarWidth,
|
||
rightPanelWidth,
|
||
markdownEditMode,
|
||
setLeftSidebarWidth,
|
||
setRightPanelWidth,
|
||
setMarkdownEditMode,
|
||
showHiddenFiles,
|
||
setShowHiddenFiles,
|
||
terminalWidth,
|
||
setTerminalWidth,
|
||
logLevel,
|
||
setLogLevel,
|
||
maxLogBuffer,
|
||
setMaxLogBuffer,
|
||
maxOutputLines,
|
||
setMaxOutputLines,
|
||
osNotificationsEnabled,
|
||
setOsNotificationsEnabled,
|
||
audioFeedbackEnabled,
|
||
setAudioFeedbackEnabled,
|
||
audioFeedbackCommand,
|
||
setAudioFeedbackCommand,
|
||
toastDuration,
|
||
setToastDuration,
|
||
checkForUpdatesOnStartup,
|
||
setCheckForUpdatesOnStartup,
|
||
logViewerSelectedLevels,
|
||
setLogViewerSelectedLevels,
|
||
shortcuts,
|
||
setShortcuts,
|
||
customAICommands,
|
||
setCustomAICommands,
|
||
globalStats,
|
||
setGlobalStats,
|
||
updateGlobalStats,
|
||
autoRunStats,
|
||
setAutoRunStats,
|
||
recordAutoRunComplete,
|
||
updateAutoRunProgress,
|
||
acknowledgeBadge,
|
||
getUnacknowledgedBadgeLevel,
|
||
ungroupedCollapsed,
|
||
setUngroupedCollapsed,
|
||
wizardCompleted,
|
||
setWizardCompleted,
|
||
tourCompleted,
|
||
setTourCompleted,
|
||
firstAutoRunCompleted,
|
||
setFirstAutoRunCompleted,
|
||
onboardingStats,
|
||
setOnboardingStats,
|
||
recordWizardStart,
|
||
recordWizardComplete,
|
||
recordWizardAbandon,
|
||
recordWizardResume,
|
||
recordTourStart,
|
||
recordTourComplete,
|
||
recordTourSkip,
|
||
getOnboardingAnalytics,
|
||
leaderboardRegistration,
|
||
setLeaderboardRegistration,
|
||
isLeaderboardRegistered,
|
||
}), [
|
||
// State values
|
||
settingsLoaded,
|
||
llmProvider,
|
||
modelSlug,
|
||
apiKey,
|
||
defaultAgent,
|
||
defaultShell,
|
||
ghPath,
|
||
fontFamily,
|
||
fontSize,
|
||
customFonts,
|
||
activeThemeId,
|
||
customThemeColors,
|
||
customThemeBaseId,
|
||
enterToSendAI,
|
||
enterToSendTerminal,
|
||
defaultSaveToHistory,
|
||
leftSidebarWidth,
|
||
rightPanelWidth,
|
||
markdownEditMode,
|
||
showHiddenFiles,
|
||
terminalWidth,
|
||
logLevel,
|
||
maxLogBuffer,
|
||
maxOutputLines,
|
||
osNotificationsEnabled,
|
||
audioFeedbackEnabled,
|
||
audioFeedbackCommand,
|
||
toastDuration,
|
||
checkForUpdatesOnStartup,
|
||
logViewerSelectedLevels,
|
||
shortcuts,
|
||
customAICommands,
|
||
globalStats,
|
||
autoRunStats,
|
||
ungroupedCollapsed,
|
||
wizardCompleted,
|
||
tourCompleted,
|
||
firstAutoRunCompleted,
|
||
onboardingStats,
|
||
// Setter functions (stable via useCallback)
|
||
setLlmProvider,
|
||
setModelSlug,
|
||
setApiKey,
|
||
setDefaultAgent,
|
||
setDefaultShell,
|
||
setGhPath,
|
||
setFontFamily,
|
||
setFontSize,
|
||
setCustomFonts,
|
||
setActiveThemeId,
|
||
setCustomThemeColors,
|
||
setCustomThemeBaseId,
|
||
setEnterToSendAI,
|
||
setEnterToSendTerminal,
|
||
setDefaultSaveToHistory,
|
||
setLeftSidebarWidth,
|
||
setRightPanelWidth,
|
||
setMarkdownEditMode,
|
||
setShowHiddenFiles,
|
||
setTerminalWidth,
|
||
setLogLevel,
|
||
setMaxLogBuffer,
|
||
setMaxOutputLines,
|
||
setOsNotificationsEnabled,
|
||
setAudioFeedbackEnabled,
|
||
setAudioFeedbackCommand,
|
||
setToastDuration,
|
||
setCheckForUpdatesOnStartup,
|
||
setLogViewerSelectedLevels,
|
||
setShortcuts,
|
||
setCustomAICommands,
|
||
setGlobalStats,
|
||
updateGlobalStats,
|
||
setAutoRunStats,
|
||
recordAutoRunComplete,
|
||
updateAutoRunProgress,
|
||
acknowledgeBadge,
|
||
getUnacknowledgedBadgeLevel,
|
||
setUngroupedCollapsed,
|
||
setWizardCompleted,
|
||
setTourCompleted,
|
||
setFirstAutoRunCompleted,
|
||
setOnboardingStats,
|
||
recordWizardStart,
|
||
recordWizardComplete,
|
||
recordWizardAbandon,
|
||
recordWizardResume,
|
||
recordTourStart,
|
||
recordTourComplete,
|
||
recordTourSkip,
|
||
getOnboardingAnalytics,
|
||
leaderboardRegistration,
|
||
setLeaderboardRegistration,
|
||
isLeaderboardRegistered,
|
||
]);
|
||
}
|