From 8caec151994041b8d5d41e7078e83a9eb4e339e3 Mon Sep 17 00:00:00 2001 From: Shirone Date: Sun, 25 Jan 2026 22:52:58 +0100 Subject: [PATCH] refactor(store): Extract utility functions into store/utils/ Move pure utility functions from app-store.ts and type files into dedicated utils modules for better separation of concerns: - theme-utils.ts: Theme and font storage utilities - shortcut-utils.ts: Keyboard shortcut parsing/formatting - usage-utils.ts: Usage limit checking All utilities are re-exported from store/utils/index.ts and app-store.ts maintains backward compatibility for existing imports. Co-Authored-By: Claude Opus 4.5 --- apps/ui/src/lib/electron.ts | 2 +- apps/ui/src/store/app-store.ts | 156 +++++----------------- apps/ui/src/store/types/ui-types.ts | 116 ---------------- apps/ui/src/store/types/usage-types.ts | 33 ----- apps/ui/src/store/utils/index.ts | 13 ++ apps/ui/src/store/utils/shortcut-utils.ts | 117 ++++++++++++++++ apps/ui/src/store/utils/theme-utils.ts | 117 ++++++++++++++++ apps/ui/src/store/utils/usage-utils.ts | 34 +++++ 8 files changed, 318 insertions(+), 270 deletions(-) create mode 100644 apps/ui/src/store/utils/index.ts create mode 100644 apps/ui/src/store/utils/shortcut-utils.ts create mode 100644 apps/ui/src/store/utils/theme-utils.ts create mode 100644 apps/ui/src/store/utils/usage-utils.ts diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index 3ce3c7aa4..89aa07baa 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -1,6 +1,6 @@ // Type definitions for Electron IPC API import type { SessionListItem, Message } from '@/types/electron'; -import type { ClaudeUsageResponse, CodexUsageResponse } from '@/store/types/usage-types'; +import type { ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store'; import type { IssueValidationVerdict, IssueValidationConfidence, diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index cc2b3691f..88220c3dc 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -4,7 +4,7 @@ import type { Project, TrashedProject } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron'; import { getHttpApiClient } from '@/lib/http-api-client'; import { createLogger } from '@automaker/utils/logger'; -import { setItem, getItem } from '@/lib/storage'; +// Note: setItem/getItem moved to ./utils/theme-utils.ts import { UI_SANS_FONT_OPTIONS, UI_MONO_FONT_OPTIONS, @@ -63,9 +63,6 @@ import { type BoardViewMode, type ShortcutKey, type KeyboardShortcuts, - DEFAULT_KEYBOARD_SHORTCUTS, - parseShortcut, - formatShortcut, // Settings types type ApiKeys, // Chat types @@ -100,9 +97,29 @@ import { type CodexRateLimitWindow, type CodexUsage, type CodexUsageResponse, - isClaudeUsageAtLimit, } from './types'; +// Import utility functions from modular utils files +import { + THEME_STORAGE_KEY, + getStoredTheme, + getStoredFontSans, + getStoredFontMono, + parseShortcut, + formatShortcut, + DEFAULT_KEYBOARD_SHORTCUTS, + isClaudeUsageAtLimit, +} from './utils'; + +// Import internal theme utils (not re-exported publicly) +import { + getEffectiveFont, + saveThemeToStorage, + saveFontSansToStorage, + saveFontMonoToStorage, + persistEffectiveThemeForProject, +} from './utils/theme-utils'; + const logger = createLogger('AppStore'); const OPENCODE_BEDROCK_PROVIDER_ID = 'amazon-bedrock'; const OPENCODE_BEDROCK_MODEL_PREFIX = `${OPENCODE_BEDROCK_PROVIDER_ID}/`; @@ -158,139 +175,38 @@ export type { }; // Re-export values from ./types for backward compatibility +export { generateSplitId }; + +// Re-export utilities from ./utils for backward compatibility export { - DEFAULT_KEYBOARD_SHORTCUTS, + THEME_STORAGE_KEY, + getStoredTheme, + getStoredFontSans, + getStoredFontMono, parseShortcut, formatShortcut, + DEFAULT_KEYBOARD_SHORTCUTS, isClaudeUsageAtLimit, - generateSplitId, }; -// NOTE: Type definitions moved to ./types/ directory +// NOTE: Type definitions moved to ./types/ directory, utilities moved to ./utils/ directory // The following inline types have been replaced with imports above: // - ViewMode, ThemeMode, BoardViewMode (./types/ui-types.ts) -// - ShortcutKey, KeyboardShortcuts, DEFAULT_KEYBOARD_SHORTCUTS, parseShortcut, formatShortcut (./types/ui-types.ts) +// - ShortcutKey, KeyboardShortcuts (./types/ui-types.ts) // - ApiKeys (./types/settings-types.ts) // - ImageAttachment, TextFileAttachment, ChatMessage, ChatSession, FeatureImage (./types/chat-types.ts) // - Terminal types (./types/terminal-types.ts) // - ClaudeModel, Feature, FileTreeNode, ProjectAnalysis (./types/project-types.ts) // - InitScriptState, AutoModeActivity, AppState, AppActions (./types/state-types.ts) // - Claude/Codex usage types (./types/usage-types.ts) - -// LocalStorage keys for persistence (fallback when server settings aren't available) -export const THEME_STORAGE_KEY = 'automaker:theme'; -export const FONT_SANS_STORAGE_KEY = 'automaker:font-sans'; -export const FONT_MONO_STORAGE_KEY = 'automaker:font-mono'; +// The following utility functions have been moved to ./utils/: +// - Theme utilities: THEME_STORAGE_KEY, getStoredTheme, getStoredFontSans, getStoredFontMono, etc. (./utils/theme-utils.ts) +// - Shortcut utilities: parseShortcut, formatShortcut, DEFAULT_KEYBOARD_SHORTCUTS (./utils/shortcut-utils.ts) +// - Usage utilities: isClaudeUsageAtLimit (./utils/usage-utils.ts) // Maximum number of output lines to keep in init script state (prevents unbounded memory growth) export const MAX_INIT_OUTPUT_LINES = 500; -/** - * Get the theme from localStorage as a fallback - * Used before server settings are loaded (e.g., on login/setup pages) - */ -export function getStoredTheme(): ThemeMode | null { - const stored = getItem(THEME_STORAGE_KEY); - if (stored) return stored as ThemeMode; - - // Backwards compatibility: older versions stored theme inside the Zustand persist blob. - // We intentionally keep reading it as a fallback so users don't get a "default theme flash" - // on login/logged-out pages if THEME_STORAGE_KEY hasn't been written yet. - try { - const legacy = getItem('automaker-storage'); - if (!legacy) return null; - interface LegacyStorageFormat { - state?: { theme?: string }; - theme?: string; - } - const parsed = JSON.parse(legacy) as LegacyStorageFormat; - const theme = parsed.state?.theme ?? parsed.theme; - if (typeof theme === 'string' && theme.length > 0) { - return theme as ThemeMode; - } - } catch { - // Ignore legacy parse errors - } - - return null; -} - -/** - * Helper to get effective font value with validation - * Returns the font to use (project override -> global -> null for default) - * @param projectFont - The project-specific font override - * @param globalFont - The global font setting - * @param fontOptions - The list of valid font options for validation - */ -function getEffectiveFont( - projectFont: string | undefined, - globalFont: string | null, - fontOptions: readonly { value: string; label: string }[] -): string | null { - const isValidFont = (font: string | null | undefined): boolean => { - if (!font || font === DEFAULT_FONT_VALUE) return true; - return fontOptions.some((opt) => opt.value === font); - }; - - if (projectFont) { - if (!isValidFont(projectFont)) return null; // Fallback to default if font not in list - return projectFont === DEFAULT_FONT_VALUE ? null : projectFont; - } - if (!isValidFont(globalFont)) return null; // Fallback to default if font not in list - return globalFont === DEFAULT_FONT_VALUE ? null : globalFont; -} - -/** - * Save theme to localStorage for immediate persistence - * This is used as a fallback when server settings can't be loaded - */ -function saveThemeToStorage(theme: ThemeMode): void { - setItem(THEME_STORAGE_KEY, theme); -} - -/** - * Get fonts from localStorage as a fallback - * Used before server settings are loaded (e.g., on login/setup pages) - */ -export function getStoredFontSans(): string | null { - return getItem(FONT_SANS_STORAGE_KEY); -} - -export function getStoredFontMono(): string | null { - return getItem(FONT_MONO_STORAGE_KEY); -} - -/** - * Save fonts to localStorage for immediate persistence - * This is used as a fallback when server settings can't be loaded - */ -function saveFontSansToStorage(fontFamily: string | null): void { - if (fontFamily) { - setItem(FONT_SANS_STORAGE_KEY, fontFamily); - } else { - // Remove from storage if null (using default) - localStorage.removeItem(FONT_SANS_STORAGE_KEY); - } -} - -function saveFontMonoToStorage(fontFamily: string | null): void { - if (fontFamily) { - setItem(FONT_MONO_STORAGE_KEY, fontFamily); - } else { - // Remove from storage if null (using default) - localStorage.removeItem(FONT_MONO_STORAGE_KEY); - } -} - -function persistEffectiveThemeForProject(project: Project | null, fallbackTheme: ThemeMode): void { - const projectTheme = project?.theme as ThemeMode | undefined; - const themeToStore = projectTheme ?? fallbackTheme; - saveThemeToStorage(themeToStore); -} - -// NOTE: Type definitions have been moved to ./types/ directory -// Types are imported at the top of this file and re-exported for backward compatibility - // Default background settings for board backgrounds export const defaultBackgroundSettings: { imagePath: string | null; diff --git a/apps/ui/src/store/types/ui-types.ts b/apps/ui/src/store/types/ui-types.ts index 238942dfd..8fa38c86f 100644 --- a/apps/ui/src/store/types/ui-types.ts +++ b/apps/ui/src/store/types/ui-types.ts @@ -68,82 +68,6 @@ export interface ShortcutKey { alt?: boolean; // Alt/Option key modifier } -// Helper to parse shortcut string to ShortcutKey object -export function parseShortcut(shortcut: string | undefined | null): ShortcutKey { - if (!shortcut) return { key: '' }; - const parts = shortcut.split('+').map((p) => p.trim()); - const result: ShortcutKey = { key: parts[parts.length - 1] }; - - // Normalize common OS-specific modifiers (Cmd/Ctrl/Win/Super symbols) into cmdCtrl - for (let i = 0; i < parts.length - 1; i++) { - const modifier = parts[i].toLowerCase(); - if (modifier === 'shift') result.shift = true; - else if ( - modifier === 'cmd' || - modifier === 'ctrl' || - modifier === 'win' || - modifier === 'super' || - modifier === '⌘' || - modifier === '^' || - modifier === '⊞' || - modifier === '◆' - ) - result.cmdCtrl = true; - else if (modifier === 'alt' || modifier === 'opt' || modifier === 'option' || modifier === '⌥') - result.alt = true; - } - - return result; -} - -// Helper to format ShortcutKey to display string -export function formatShortcut(shortcut: string | undefined | null, forDisplay = false): string { - if (!shortcut) return ''; - const parsed = parseShortcut(shortcut); - const parts: string[] = []; - - // Prefer User-Agent Client Hints when available; fall back to legacy - const platform: 'darwin' | 'win32' | 'linux' = (() => { - if (typeof navigator === 'undefined') return 'linux'; - - const uaPlatform = ( - navigator as Navigator & { userAgentData?: { platform?: string } } - ).userAgentData?.platform?.toLowerCase?.(); - const legacyPlatform = navigator.platform?.toLowerCase?.(); - const platformString = uaPlatform || legacyPlatform || ''; - - if (platformString.includes('mac')) return 'darwin'; - if (platformString.includes('win')) return 'win32'; - return 'linux'; - })(); - - // Primary modifier - OS-specific - if (parsed.cmdCtrl) { - if (forDisplay) { - parts.push(platform === 'darwin' ? '⌘' : platform === 'win32' ? '⊞' : '◆'); - } else { - parts.push(platform === 'darwin' ? 'Cmd' : platform === 'win32' ? 'Win' : 'Super'); - } - } - - // Alt/Option - if (parsed.alt) { - parts.push( - forDisplay ? (platform === 'darwin' ? '⌥' : 'Alt') : platform === 'darwin' ? 'Opt' : 'Alt' - ); - } - - // Shift - if (parsed.shift) { - parts.push(forDisplay ? '⇧' : 'Shift'); - } - - parts.push(parsed.key.toUpperCase()); - - // Add spacing when displaying symbols - return parts.join(forDisplay ? ' ' : '+'); -} - // Keyboard Shortcuts - stored as strings like "K", "Shift+N", "Cmd+K" export interface KeyboardShortcuts { // Navigation shortcuts @@ -180,43 +104,3 @@ export interface KeyboardShortcuts { closeTerminal: string; newTerminalTab: string; } - -// Default keyboard shortcuts -export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = { - // Navigation - board: 'K', - graph: 'H', - agent: 'A', - spec: 'D', - context: 'C', - memory: 'Y', - settings: 'S', - projectSettings: 'Shift+S', - terminal: 'T', - ideation: 'I', - notifications: 'X', - githubIssues: 'G', - githubPrs: 'R', - - // UI - toggleSidebar: '`', - - // Actions - // Note: Some shortcuts share the same key (e.g., "N" for addFeature, newSession) - // This is intentional as they are context-specific and only active in their respective views - addFeature: 'N', // Only active in board view - addContextFile: 'N', // Only active in context view - startNext: 'G', // Only active in board view - newSession: 'N', // Only active in agent view - openProject: 'O', // Global shortcut - projectPicker: 'P', // Global shortcut - cyclePrevProject: 'Q', // Global shortcut - cycleNextProject: 'E', // Global shortcut - - // Terminal shortcuts (only active in terminal view) - // Using Alt modifier to avoid conflicts with both terminal signals AND browser shortcuts - splitTerminalRight: 'Alt+D', - splitTerminalDown: 'Alt+S', - closeTerminal: 'Alt+W', - newTerminalTab: 'Alt+T', -}; diff --git a/apps/ui/src/store/types/usage-types.ts b/apps/ui/src/store/types/usage-types.ts index 6a45e7b4f..e097526c2 100644 --- a/apps/ui/src/store/types/usage-types.ts +++ b/apps/ui/src/store/types/usage-types.ts @@ -58,36 +58,3 @@ export interface CodexUsage { // Response type for Codex usage API (can be success or error) export type CodexUsageResponse = CodexUsage | { error: string; message?: string }; - -/** - * Check if Claude usage is at its limit (any of: session >= 100%, weekly >= 100%, OR cost >= limit) - * Returns true if any limit is reached, meaning auto mode should pause feature pickup. - */ -export function isClaudeUsageAtLimit(claudeUsage: ClaudeUsage | null): boolean { - if (!claudeUsage) { - // No usage data available - don't block - return false; - } - - // Check session limit (5-hour window) - if (claudeUsage.sessionPercentage >= 100) { - return true; - } - - // Check weekly limit - if (claudeUsage.weeklyPercentage >= 100) { - return true; - } - - // Check cost limit (if configured) - if ( - claudeUsage.costLimit !== null && - claudeUsage.costLimit > 0 && - claudeUsage.costUsed !== null && - claudeUsage.costUsed >= claudeUsage.costLimit - ) { - return true; - } - - return false; -} diff --git a/apps/ui/src/store/utils/index.ts b/apps/ui/src/store/utils/index.ts new file mode 100644 index 000000000..1c9cb7011 --- /dev/null +++ b/apps/ui/src/store/utils/index.ts @@ -0,0 +1,13 @@ +// Theme utilities (PUBLIC) +export { + THEME_STORAGE_KEY, + getStoredTheme, + getStoredFontSans, + getStoredFontMono, +} from './theme-utils'; + +// Shortcut utilities (PUBLIC) +export { parseShortcut, formatShortcut, DEFAULT_KEYBOARD_SHORTCUTS } from './shortcut-utils'; + +// Usage utilities (PUBLIC) +export { isClaudeUsageAtLimit } from './usage-utils'; diff --git a/apps/ui/src/store/utils/shortcut-utils.ts b/apps/ui/src/store/utils/shortcut-utils.ts new file mode 100644 index 000000000..3c82b1c89 --- /dev/null +++ b/apps/ui/src/store/utils/shortcut-utils.ts @@ -0,0 +1,117 @@ +import type { ShortcutKey, KeyboardShortcuts } from '../types/ui-types'; + +// Helper to parse shortcut string to ShortcutKey object +export function parseShortcut(shortcut: string | undefined | null): ShortcutKey { + if (!shortcut) return { key: '' }; + const parts = shortcut.split('+').map((p) => p.trim()); + const result: ShortcutKey = { key: parts[parts.length - 1] }; + + // Normalize common OS-specific modifiers (Cmd/Ctrl/Win/Super symbols) into cmdCtrl + for (let i = 0; i < parts.length - 1; i++) { + const modifier = parts[i].toLowerCase(); + if (modifier === 'shift') result.shift = true; + else if ( + modifier === 'cmd' || + modifier === 'ctrl' || + modifier === 'win' || + modifier === 'super' || + modifier === '⌘' || + modifier === '^' || + modifier === '⊞' || + modifier === '◆' + ) + result.cmdCtrl = true; + else if (modifier === 'alt' || modifier === 'opt' || modifier === 'option' || modifier === '⌥') + result.alt = true; + } + + return result; +} + +// Helper to format ShortcutKey to display string +export function formatShortcut(shortcut: string | undefined | null, forDisplay = false): string { + if (!shortcut) return ''; + const parsed = parseShortcut(shortcut); + const parts: string[] = []; + + // Prefer User-Agent Client Hints when available; fall back to legacy + const platform: 'darwin' | 'win32' | 'linux' = (() => { + if (typeof navigator === 'undefined') return 'linux'; + + const uaPlatform = ( + navigator as Navigator & { userAgentData?: { platform?: string } } + ).userAgentData?.platform?.toLowerCase?.(); + const legacyPlatform = navigator.platform?.toLowerCase?.(); + const platformString = uaPlatform || legacyPlatform || ''; + + if (platformString.includes('mac')) return 'darwin'; + if (platformString.includes('win')) return 'win32'; + return 'linux'; + })(); + + // Primary modifier - OS-specific + if (parsed.cmdCtrl) { + if (forDisplay) { + parts.push(platform === 'darwin' ? '⌘' : platform === 'win32' ? '⊞' : '◆'); + } else { + parts.push(platform === 'darwin' ? 'Cmd' : platform === 'win32' ? 'Win' : 'Super'); + } + } + + // Alt/Option + if (parsed.alt) { + parts.push( + forDisplay ? (platform === 'darwin' ? '⌥' : 'Alt') : platform === 'darwin' ? 'Opt' : 'Alt' + ); + } + + // Shift + if (parsed.shift) { + parts.push(forDisplay ? '⇧' : 'Shift'); + } + + parts.push(parsed.key.toUpperCase()); + + // Add spacing when displaying symbols + return parts.join(forDisplay ? ' ' : '+'); +} + +// Default keyboard shortcuts +export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = { + // Navigation + board: 'K', + graph: 'H', + agent: 'A', + spec: 'D', + context: 'C', + memory: 'Y', + settings: 'S', + projectSettings: 'Shift+S', + terminal: 'T', + ideation: 'I', + notifications: 'X', + githubIssues: 'G', + githubPrs: 'R', + + // UI + toggleSidebar: '`', + + // Actions + // Note: Some shortcuts share the same key (e.g., "N" for addFeature, newSession) + // This is intentional as they are context-specific and only active in their respective views + addFeature: 'N', // Only active in board view + addContextFile: 'N', // Only active in context view + startNext: 'G', // Only active in board view + newSession: 'N', // Only active in agent view + openProject: 'O', // Global shortcut + projectPicker: 'P', // Global shortcut + cyclePrevProject: 'Q', // Global shortcut + cycleNextProject: 'E', // Global shortcut + + // Terminal shortcuts (only active in terminal view) + // Using Alt modifier to avoid conflicts with both terminal signals AND browser shortcuts + splitTerminalRight: 'Alt+D', + splitTerminalDown: 'Alt+S', + closeTerminal: 'Alt+W', + newTerminalTab: 'Alt+T', +}; diff --git a/apps/ui/src/store/utils/theme-utils.ts b/apps/ui/src/store/utils/theme-utils.ts new file mode 100644 index 000000000..8f67c6bfe --- /dev/null +++ b/apps/ui/src/store/utils/theme-utils.ts @@ -0,0 +1,117 @@ +import { getItem, setItem, removeItem } from '@/lib/storage'; +import { DEFAULT_FONT_VALUE } from '@/config/ui-font-options'; +import type { Project } from '@/lib/electron'; +import type { ThemeMode } from '../types/ui-types'; + +// LocalStorage keys for persistence (fallback when server settings aren't available) +export const THEME_STORAGE_KEY = 'automaker:theme'; +const FONT_SANS_STORAGE_KEY = 'automaker:font-sans'; +const FONT_MONO_STORAGE_KEY = 'automaker:font-mono'; + +/** + * Get the theme from localStorage as a fallback + * Used before server settings are loaded (e.g., on login/setup pages) + */ +export function getStoredTheme(): ThemeMode | null { + const stored = getItem(THEME_STORAGE_KEY); + if (stored) return stored as ThemeMode; + + // Backwards compatibility: older versions stored theme inside the Zustand persist blob. + // We intentionally keep reading it as a fallback so users don't get a "default theme flash" + // on login/logged-out pages if THEME_STORAGE_KEY hasn't been written yet. + try { + const legacy = getItem('automaker-storage'); + if (!legacy) return null; + interface LegacyStorageFormat { + state?: { theme?: string }; + theme?: string; + } + const parsed = JSON.parse(legacy) as LegacyStorageFormat; + const theme = parsed.state?.theme ?? parsed.theme; + if (typeof theme === 'string' && theme.length > 0) { + return theme as ThemeMode; + } + } catch { + // Ignore legacy parse errors + } + + return null; +} + +/** + * Helper to get effective font value with validation + * Returns the font to use (project override -> global -> null for default) + * @param projectFont - The project-specific font override + * @param globalFont - The global font setting + * @param fontOptions - The list of valid font options for validation + */ +export function getEffectiveFont( + projectFont: string | undefined, + globalFont: string | null, + fontOptions: readonly { value: string; label: string }[] +): string | null { + const isValidFont = (font: string | null | undefined): boolean => { + if (!font || font === DEFAULT_FONT_VALUE) return true; + return fontOptions.some((opt) => opt.value === font); + }; + + if (projectFont) { + if (isValidFont(projectFont)) { + return projectFont === DEFAULT_FONT_VALUE ? null : projectFont; + } + // Invalid project font -> fall through to check global font + } + if (!isValidFont(globalFont)) return null; // Fallback to default if font not in list + return globalFont === DEFAULT_FONT_VALUE ? null : globalFont; +} + +/** + * Save theme to localStorage for immediate persistence + * This is used as a fallback when server settings can't be loaded + */ +export function saveThemeToStorage(theme: ThemeMode): void { + setItem(THEME_STORAGE_KEY, theme); +} + +/** + * Get fonts from localStorage as a fallback + * Used before server settings are loaded (e.g., on login/setup pages) + */ +export function getStoredFontSans(): string | null { + return getItem(FONT_SANS_STORAGE_KEY); +} + +export function getStoredFontMono(): string | null { + return getItem(FONT_MONO_STORAGE_KEY); +} + +/** + * Save fonts to localStorage for immediate persistence + * This is used as a fallback when server settings can't be loaded + */ +export function saveFontSansToStorage(fontFamily: string | null): void { + if (fontFamily) { + setItem(FONT_SANS_STORAGE_KEY, fontFamily); + } else { + // Remove from storage if null (using default) + removeItem(FONT_SANS_STORAGE_KEY); + } +} + +export function saveFontMonoToStorage(fontFamily: string | null): void { + if (fontFamily) { + setItem(FONT_MONO_STORAGE_KEY, fontFamily); + } else { + // Remove from storage if null (using default) + removeItem(FONT_MONO_STORAGE_KEY); + } +} + +export function persistEffectiveThemeForProject( + project: Project | null, + fallbackTheme: ThemeMode +): void { + const projectTheme = project?.theme as ThemeMode | undefined; + const themeToStore = projectTheme ?? fallbackTheme; + saveThemeToStorage(themeToStore); +} diff --git a/apps/ui/src/store/utils/usage-utils.ts b/apps/ui/src/store/utils/usage-utils.ts new file mode 100644 index 000000000..7b82fb128 --- /dev/null +++ b/apps/ui/src/store/utils/usage-utils.ts @@ -0,0 +1,34 @@ +import type { ClaudeUsage } from '../types/usage-types'; + +/** + * Check if Claude usage is at its limit (any of: session >= 100%, weekly >= 100%, OR cost >= limit) + * Returns true if any limit is reached, meaning auto mode should pause feature pickup. + */ +export function isClaudeUsageAtLimit(claudeUsage: ClaudeUsage | null): boolean { + if (!claudeUsage) { + // No usage data available - don't block + return false; + } + + // Check session limit (5-hour window) + if (claudeUsage.sessionPercentage >= 100) { + return true; + } + + // Check weekly limit + if (claudeUsage.weeklyPercentage >= 100) { + return true; + } + + // Check cost limit (if configured) + if ( + claudeUsage.costLimit !== null && + claudeUsage.costLimit > 0 && + claudeUsage.costUsed !== null && + claudeUsage.costUsed >= claudeUsage.costLimit + ) { + return true; + } + + return false; +}