Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/ui/src/lib/electron.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
156 changes: 36 additions & 120 deletions apps/ui/src/store/app-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -63,9 +63,6 @@ import {
type BoardViewMode,
type ShortcutKey,
type KeyboardShortcuts,
DEFAULT_KEYBOARD_SHORTCUTS,
parseShortcut,
formatShortcut,
// Settings types
type ApiKeys,
// Chat types
Expand Down Expand Up @@ -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}/`;
Expand Down Expand Up @@ -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;
Expand Down
116 changes: 0 additions & 116 deletions apps/ui/src/store/types/ui-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
};
33 changes: 0 additions & 33 deletions apps/ui/src/store/types/usage-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
13 changes: 13 additions & 0 deletions apps/ui/src/store/utils/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading