mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
Merge main into massive-terminal-upgrade
Resolves merge conflicts: - apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger - apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions - apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling) - apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes - apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
9
apps/ui/src/hooks/index.ts
Normal file
9
apps/ui/src/hooks/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { useAutoMode } from './use-auto-mode';
|
||||
export { useBoardBackgroundSettings } from './use-board-background-settings';
|
||||
export { useElectronAgent } from './use-electron-agent';
|
||||
export { useKeyboardShortcuts } from './use-keyboard-shortcuts';
|
||||
export { useMessageQueue } from './use-message-queue';
|
||||
export { useResponsiveKanban } from './use-responsive-kanban';
|
||||
export { useScrollTracking } from './use-scroll-tracking';
|
||||
export { useSettingsMigration } from './use-settings-migration';
|
||||
export { useWindowState } from './use-window-state';
|
||||
181
apps/ui/src/hooks/use-board-background-settings.ts
Normal file
181
apps/ui/src/hooks/use-board-background-settings.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
/**
|
||||
* Hook for managing board background settings with automatic persistence to server
|
||||
*/
|
||||
export function useBoardBackgroundSettings() {
|
||||
const store = useAppStore();
|
||||
const httpClient = getHttpApiClient();
|
||||
|
||||
// Helper to persist settings to server
|
||||
const persistSettings = useCallback(
|
||||
async (projectPath: string, settingsToUpdate: Record<string, unknown>) => {
|
||||
try {
|
||||
const result = await httpClient.settings.updateProject(projectPath, {
|
||||
boardBackground: settingsToUpdate,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Failed to persist settings:', result.error);
|
||||
toast.error('Failed to save settings');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to persist settings:', error);
|
||||
toast.error('Failed to save settings');
|
||||
}
|
||||
},
|
||||
[httpClient]
|
||||
);
|
||||
|
||||
// Get current background settings for a project
|
||||
const getCurrentSettings = useCallback(
|
||||
(projectPath: string) => {
|
||||
const current = store.boardBackgroundByProject[projectPath];
|
||||
return (
|
||||
current || {
|
||||
imagePath: null,
|
||||
cardOpacity: 100,
|
||||
columnOpacity: 100,
|
||||
columnBorderEnabled: true,
|
||||
cardGlassmorphism: true,
|
||||
cardBorderEnabled: true,
|
||||
cardBorderOpacity: 100,
|
||||
hideScrollbar: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
[store.boardBackgroundByProject]
|
||||
);
|
||||
|
||||
// Persisting wrappers for store actions
|
||||
const setBoardBackground = useCallback(
|
||||
async (projectPath: string, imagePath: string | null) => {
|
||||
// Get current settings first
|
||||
const current = getCurrentSettings(projectPath);
|
||||
|
||||
// Prepare the updated settings
|
||||
const toUpdate = {
|
||||
...current,
|
||||
imagePath,
|
||||
imageVersion: imagePath ? Date.now() : undefined,
|
||||
};
|
||||
|
||||
// Update local store
|
||||
store.setBoardBackground(projectPath, imagePath);
|
||||
|
||||
// Persist to server
|
||||
await persistSettings(projectPath, toUpdate);
|
||||
},
|
||||
[store, persistSettings, getCurrentSettings]
|
||||
);
|
||||
|
||||
const setCardOpacity = useCallback(
|
||||
async (projectPath: string, opacity: number) => {
|
||||
const current = getCurrentSettings(projectPath);
|
||||
store.setCardOpacity(projectPath, opacity);
|
||||
await persistSettings(projectPath, { ...current, cardOpacity: opacity });
|
||||
},
|
||||
[store, persistSettings, getCurrentSettings]
|
||||
);
|
||||
|
||||
const setColumnOpacity = useCallback(
|
||||
async (projectPath: string, opacity: number) => {
|
||||
const current = getCurrentSettings(projectPath);
|
||||
store.setColumnOpacity(projectPath, opacity);
|
||||
await persistSettings(projectPath, { ...current, columnOpacity: opacity });
|
||||
},
|
||||
[store, persistSettings, getCurrentSettings]
|
||||
);
|
||||
|
||||
const setColumnBorderEnabled = useCallback(
|
||||
async (projectPath: string, enabled: boolean) => {
|
||||
const current = getCurrentSettings(projectPath);
|
||||
store.setColumnBorderEnabled(projectPath, enabled);
|
||||
await persistSettings(projectPath, {
|
||||
...current,
|
||||
columnBorderEnabled: enabled,
|
||||
});
|
||||
},
|
||||
[store, persistSettings, getCurrentSettings]
|
||||
);
|
||||
|
||||
const setCardGlassmorphism = useCallback(
|
||||
async (projectPath: string, enabled: boolean) => {
|
||||
const current = getCurrentSettings(projectPath);
|
||||
store.setCardGlassmorphism(projectPath, enabled);
|
||||
await persistSettings(projectPath, {
|
||||
...current,
|
||||
cardGlassmorphism: enabled,
|
||||
});
|
||||
},
|
||||
[store, persistSettings, getCurrentSettings]
|
||||
);
|
||||
|
||||
const setCardBorderEnabled = useCallback(
|
||||
async (projectPath: string, enabled: boolean) => {
|
||||
const current = getCurrentSettings(projectPath);
|
||||
store.setCardBorderEnabled(projectPath, enabled);
|
||||
await persistSettings(projectPath, {
|
||||
...current,
|
||||
cardBorderEnabled: enabled,
|
||||
});
|
||||
},
|
||||
[store, persistSettings, getCurrentSettings]
|
||||
);
|
||||
|
||||
const setCardBorderOpacity = useCallback(
|
||||
async (projectPath: string, opacity: number) => {
|
||||
const current = getCurrentSettings(projectPath);
|
||||
store.setCardBorderOpacity(projectPath, opacity);
|
||||
await persistSettings(projectPath, {
|
||||
...current,
|
||||
cardBorderOpacity: opacity,
|
||||
});
|
||||
},
|
||||
[store, persistSettings, getCurrentSettings]
|
||||
);
|
||||
|
||||
const setHideScrollbar = useCallback(
|
||||
async (projectPath: string, hide: boolean) => {
|
||||
const current = getCurrentSettings(projectPath);
|
||||
store.setHideScrollbar(projectPath, hide);
|
||||
await persistSettings(projectPath, { ...current, hideScrollbar: hide });
|
||||
},
|
||||
[store, persistSettings, getCurrentSettings]
|
||||
);
|
||||
|
||||
const clearBoardBackground = useCallback(
|
||||
async (projectPath: string) => {
|
||||
store.clearBoardBackground(projectPath);
|
||||
// Clear the boardBackground settings
|
||||
await persistSettings(projectPath, {
|
||||
imagePath: null,
|
||||
imageVersion: undefined,
|
||||
cardOpacity: 100,
|
||||
columnOpacity: 100,
|
||||
columnBorderEnabled: true,
|
||||
cardGlassmorphism: true,
|
||||
cardBorderEnabled: true,
|
||||
cardBorderOpacity: 100,
|
||||
hideScrollbar: false,
|
||||
});
|
||||
},
|
||||
[store, persistSettings]
|
||||
);
|
||||
|
||||
return {
|
||||
setBoardBackground,
|
||||
setCardOpacity,
|
||||
setColumnOpacity,
|
||||
setColumnBorderEnabled,
|
||||
setCardGlassmorphism,
|
||||
setCardBorderEnabled,
|
||||
setCardBorderOpacity,
|
||||
setHideScrollbar,
|
||||
clearBoardBackground,
|
||||
getCurrentSettings,
|
||||
};
|
||||
}
|
||||
315
apps/ui/src/hooks/use-settings-migration.ts
Normal file
315
apps/ui/src/hooks/use-settings-migration.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* Settings Migration Hook and Sync Functions
|
||||
*
|
||||
* Handles migrating user settings from localStorage to persistent file-based storage
|
||||
* on app startup. Also provides utility functions for syncing individual setting
|
||||
* categories to the server.
|
||||
*
|
||||
* Migration flow:
|
||||
* 1. useSettingsMigration() hook checks server for existing settings files
|
||||
* 2. If none exist, collects localStorage data and sends to /api/settings/migrate
|
||||
* 3. After successful migration, clears deprecated localStorage keys
|
||||
* 4. Maintains automaker-storage in localStorage as fast cache for Zustand
|
||||
*
|
||||
* Sync functions for incremental updates:
|
||||
* - syncSettingsToServer: Writes global settings to file
|
||||
* - syncCredentialsToServer: Writes API keys to file
|
||||
* - syncProjectSettingsToServer: Writes project-specific overrides
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { isElectron } from '@/lib/electron';
|
||||
import { getItem, removeItem } from '@/lib/storage';
|
||||
|
||||
/**
|
||||
* State returned by useSettingsMigration hook
|
||||
*/
|
||||
interface MigrationState {
|
||||
/** Whether migration check has completed */
|
||||
checked: boolean;
|
||||
/** Whether migration actually occurred */
|
||||
migrated: boolean;
|
||||
/** Error message if migration failed (null if success/no-op) */
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* localStorage keys that may contain settings to migrate
|
||||
*
|
||||
* These keys are collected and sent to the server for migration.
|
||||
* The automaker-storage key is handled specially as it's still used by Zustand.
|
||||
*/
|
||||
const LOCALSTORAGE_KEYS = [
|
||||
'automaker-storage',
|
||||
'automaker-setup',
|
||||
'worktree-panel-collapsed',
|
||||
'file-browser-recent-folders',
|
||||
'automaker:lastProjectDir',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* localStorage keys to remove after successful migration
|
||||
*
|
||||
* automaker-storage is intentionally NOT in this list because Zustand still uses it
|
||||
* as a cache. These other keys have been migrated and are no longer needed.
|
||||
*/
|
||||
const KEYS_TO_CLEAR_AFTER_MIGRATION = [
|
||||
'worktree-panel-collapsed',
|
||||
'file-browser-recent-folders',
|
||||
'automaker:lastProjectDir',
|
||||
// Legacy keys from older versions
|
||||
'automaker_projects',
|
||||
'automaker_current_project',
|
||||
'automaker_trashed_projects',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* React hook to handle settings migration from localStorage to file-based storage
|
||||
*
|
||||
* Runs automatically once on component mount. Returns state indicating whether
|
||||
* migration check is complete, whether migration occurred, and any errors.
|
||||
*
|
||||
* Only runs in Electron mode (isElectron() must be true). Web mode uses different
|
||||
* storage mechanisms.
|
||||
*
|
||||
* The hook uses a ref to ensure it only runs once despite multiple mounts.
|
||||
*
|
||||
* @returns MigrationState with checked, migrated, and error fields
|
||||
*/
|
||||
export function useSettingsMigration(): MigrationState {
|
||||
const [state, setState] = useState<MigrationState>({
|
||||
checked: false,
|
||||
migrated: false,
|
||||
error: null,
|
||||
});
|
||||
const migrationAttempted = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Only run once
|
||||
if (migrationAttempted.current) return;
|
||||
migrationAttempted.current = true;
|
||||
|
||||
async function checkAndMigrate() {
|
||||
// Only run migration in Electron mode (web mode uses different storage)
|
||||
if (!isElectron()) {
|
||||
setState({ checked: true, migrated: false, error: null });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const api = getHttpApiClient();
|
||||
|
||||
// Check if server has settings files
|
||||
const status = await api.settings.getStatus();
|
||||
|
||||
if (!status.success) {
|
||||
console.error('[Settings Migration] Failed to get status:', status);
|
||||
setState({
|
||||
checked: true,
|
||||
migrated: false,
|
||||
error: 'Failed to check settings status',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If settings files already exist, no migration needed
|
||||
if (!status.needsMigration) {
|
||||
console.log('[Settings Migration] Settings files exist, no migration needed');
|
||||
setState({ checked: true, migrated: false, error: null });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have localStorage data to migrate
|
||||
const automakerStorage = getItem('automaker-storage');
|
||||
if (!automakerStorage) {
|
||||
console.log('[Settings Migration] No localStorage data to migrate');
|
||||
setState({ checked: true, migrated: false, error: null });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Settings Migration] Starting migration...');
|
||||
|
||||
// Collect all localStorage data
|
||||
const localStorageData: Record<string, string> = {};
|
||||
for (const key of LOCALSTORAGE_KEYS) {
|
||||
const value = getItem(key);
|
||||
if (value) {
|
||||
localStorageData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Send to server for migration
|
||||
const result = await api.settings.migrate(localStorageData);
|
||||
|
||||
if (result.success) {
|
||||
console.log('[Settings Migration] Migration successful:', {
|
||||
globalSettings: result.migratedGlobalSettings,
|
||||
credentials: result.migratedCredentials,
|
||||
projects: result.migratedProjectCount,
|
||||
});
|
||||
|
||||
// Clear old localStorage keys (but keep automaker-storage for Zustand)
|
||||
for (const key of KEYS_TO_CLEAR_AFTER_MIGRATION) {
|
||||
removeItem(key);
|
||||
}
|
||||
|
||||
setState({ checked: true, migrated: true, error: null });
|
||||
} else {
|
||||
console.warn('[Settings Migration] Migration had errors:', result.errors);
|
||||
setState({
|
||||
checked: true,
|
||||
migrated: false,
|
||||
error: result.errors.join(', '),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Settings Migration] Migration failed:', error);
|
||||
setState({
|
||||
checked: true,
|
||||
migrated: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkAndMigrate();
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync current global settings to file-based server storage
|
||||
*
|
||||
* Reads the current Zustand state from localStorage and sends all global settings
|
||||
* to the server to be written to {dataDir}/settings.json.
|
||||
*
|
||||
* Call this when important global settings change (theme, UI preferences, profiles, etc.)
|
||||
* Safe to call from store subscribers or change handlers.
|
||||
*
|
||||
* Only functions in Electron mode. Returns false if not in Electron or on error.
|
||||
*
|
||||
* @returns Promise resolving to true if sync succeeded, false otherwise
|
||||
*/
|
||||
export async function syncSettingsToServer(): Promise<boolean> {
|
||||
if (!isElectron()) return false;
|
||||
|
||||
try {
|
||||
const api = getHttpApiClient();
|
||||
const automakerStorage = getItem('automaker-storage');
|
||||
|
||||
if (!automakerStorage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(automakerStorage);
|
||||
const state = parsed.state || parsed;
|
||||
|
||||
// Extract settings to sync
|
||||
const updates = {
|
||||
theme: state.theme,
|
||||
sidebarOpen: state.sidebarOpen,
|
||||
chatHistoryOpen: state.chatHistoryOpen,
|
||||
kanbanCardDetailLevel: state.kanbanCardDetailLevel,
|
||||
maxConcurrency: state.maxConcurrency,
|
||||
defaultSkipTests: state.defaultSkipTests,
|
||||
enableDependencyBlocking: state.enableDependencyBlocking,
|
||||
useWorktrees: state.useWorktrees,
|
||||
showProfilesOnly: state.showProfilesOnly,
|
||||
defaultPlanningMode: state.defaultPlanningMode,
|
||||
defaultRequirePlanApproval: state.defaultRequirePlanApproval,
|
||||
defaultAIProfileId: state.defaultAIProfileId,
|
||||
muteDoneSound: state.muteDoneSound,
|
||||
enhancementModel: state.enhancementModel,
|
||||
keyboardShortcuts: state.keyboardShortcuts,
|
||||
aiProfiles: state.aiProfiles,
|
||||
projects: state.projects,
|
||||
trashedProjects: state.trashedProjects,
|
||||
projectHistory: state.projectHistory,
|
||||
projectHistoryIndex: state.projectHistoryIndex,
|
||||
lastSelectedSessionByProject: state.lastSelectedSessionByProject,
|
||||
};
|
||||
|
||||
const result = await api.settings.updateGlobal(updates);
|
||||
return result.success;
|
||||
} catch (error) {
|
||||
console.error('[Settings Sync] Failed to sync settings:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync API credentials to file-based server storage
|
||||
*
|
||||
* Sends API keys (partial update supported) to the server to be written to
|
||||
* {dataDir}/credentials.json. Credentials are kept separate from settings for security.
|
||||
*
|
||||
* Call this when API keys are added or updated in settings UI.
|
||||
* Only requires providing the keys that have changed.
|
||||
*
|
||||
* Only functions in Electron mode. Returns false if not in Electron or on error.
|
||||
*
|
||||
* @param apiKeys - Partial credential object with optional anthropic, google, openai keys
|
||||
* @returns Promise resolving to true if sync succeeded, false otherwise
|
||||
*/
|
||||
export async function syncCredentialsToServer(apiKeys: {
|
||||
anthropic?: string;
|
||||
google?: string;
|
||||
openai?: string;
|
||||
}): Promise<boolean> {
|
||||
if (!isElectron()) return false;
|
||||
|
||||
try {
|
||||
const api = getHttpApiClient();
|
||||
const result = await api.settings.updateCredentials({ apiKeys });
|
||||
return result.success;
|
||||
} catch (error) {
|
||||
console.error('[Settings Sync] Failed to sync credentials:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync project-specific settings to file-based server storage
|
||||
*
|
||||
* Sends project settings (theme, worktree config, board customization) to the server
|
||||
* to be written to {projectPath}/.automaker/settings.json.
|
||||
*
|
||||
* These settings override global settings for specific projects.
|
||||
* Supports partial updates - only include fields that have changed.
|
||||
*
|
||||
* Call this when project settings are modified in the board or settings UI.
|
||||
* Only functions in Electron mode. Returns false if not in Electron or on error.
|
||||
*
|
||||
* @param projectPath - Absolute path to project directory
|
||||
* @param updates - Partial ProjectSettings with optional theme, worktree, and board settings
|
||||
* @returns Promise resolving to true if sync succeeded, false otherwise
|
||||
*/
|
||||
export async function syncProjectSettingsToServer(
|
||||
projectPath: string,
|
||||
updates: {
|
||||
theme?: string;
|
||||
useWorktrees?: boolean;
|
||||
boardBackground?: Record<string, unknown>;
|
||||
currentWorktree?: { path: string | null; branch: string };
|
||||
worktrees?: Array<{
|
||||
path: string;
|
||||
branch: string;
|
||||
isMain: boolean;
|
||||
hasChanges?: boolean;
|
||||
changedFilesCount?: number;
|
||||
}>;
|
||||
}
|
||||
): Promise<boolean> {
|
||||
if (!isElectron()) return false;
|
||||
|
||||
try {
|
||||
const api = getHttpApiClient();
|
||||
const result = await api.settings.updateProject(projectPath, updates);
|
||||
return result.success;
|
||||
} catch (error) {
|
||||
console.error('[Settings Sync] Failed to sync project settings:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user