mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
- Added a new function to retrieve subagents configuration from settings, allowing users to enable/disable subagents and select sources for loading them. - Updated the AgentService to incorporate subagents configuration, dynamically adding tools based on the settings. - Enhanced the UI components to manage subagents, including a settings section for enabling/disabling and selecting sources. - Introduced a new hook for managing subagents settings state and interactions. These changes improve the flexibility and usability of subagents within the application, enhancing user experience and configuration options.
357 lines
12 KiB
TypeScript
357 lines
12 KiB
TypeScript
/**
|
|
* 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 { createLogger } from '@automaker/utils/logger';
|
|
import { getHttpApiClient, waitForApiKeyInit } from '@/lib/http-api-client';
|
|
import { isElectron } from '@/lib/electron';
|
|
import { getItem, removeItem } from '@/lib/storage';
|
|
import { useAppStore } from '@/store/app-store';
|
|
|
|
const logger = createLogger('SettingsMigration');
|
|
|
|
/**
|
|
* 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 {
|
|
// Wait for API key to be initialized before making any API calls
|
|
// This prevents 401 errors on startup in Electron mode
|
|
await waitForApiKeyInit();
|
|
|
|
const api = getHttpApiClient();
|
|
|
|
// Check if server has settings files
|
|
const status = await api.settings.getStatus();
|
|
|
|
if (!status.success) {
|
|
logger.error('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) {
|
|
logger.info('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) {
|
|
logger.info('No localStorage data to migrate');
|
|
setState({ checked: true, migrated: false, error: null });
|
|
return;
|
|
}
|
|
|
|
logger.info('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) {
|
|
logger.info('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 {
|
|
logger.warn('Migration had errors:', result.errors);
|
|
setState({
|
|
checked: true,
|
|
migrated: false,
|
|
error: result.errors.join(', '),
|
|
});
|
|
}
|
|
} catch (error) {
|
|
logger.error('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.
|
|
*
|
|
* @returns Promise resolving to true if sync succeeded, false otherwise
|
|
*/
|
|
export async function syncSettingsToServer(): Promise<boolean> {
|
|
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,
|
|
validationModel: state.validationModel,
|
|
phaseModels: state.phaseModels,
|
|
autoLoadClaudeMd: state.autoLoadClaudeMd,
|
|
enableSandboxMode: state.enableSandboxMode,
|
|
skipSandboxWarning: state.skipSandboxWarning,
|
|
keyboardShortcuts: state.keyboardShortcuts,
|
|
aiProfiles: state.aiProfiles,
|
|
mcpServers: state.mcpServers,
|
|
enableSkills: state.enableSkills,
|
|
skillsSources: state.skillsSources,
|
|
enableSubagents: state.enableSubagents,
|
|
subagentsSources: state.subagentsSources,
|
|
promptCustomization: state.promptCustomization,
|
|
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) {
|
|
logger.error('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.
|
|
*
|
|
* @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> {
|
|
try {
|
|
const api = getHttpApiClient();
|
|
const result = await api.settings.updateCredentials({ apiKeys });
|
|
return result.success;
|
|
} catch (error) {
|
|
logger.error('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.
|
|
*
|
|
* @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> {
|
|
try {
|
|
const api = getHttpApiClient();
|
|
const result = await api.settings.updateProject(projectPath, updates);
|
|
return result.success;
|
|
} catch (error) {
|
|
logger.error('Failed to sync project settings:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load MCP servers from server settings file into the store
|
|
*
|
|
* Fetches the global settings from the server and updates the store's
|
|
* mcpServers state. Useful when settings were modified externally
|
|
* (e.g., by editing the settings.json file directly).
|
|
*
|
|
* @returns Promise resolving to true if load succeeded, false otherwise
|
|
*/
|
|
export async function loadMCPServersFromServer(): Promise<boolean> {
|
|
try {
|
|
const api = getHttpApiClient();
|
|
const result = await api.settings.getGlobal();
|
|
|
|
if (!result.success || !result.settings) {
|
|
logger.error('Failed to load settings:', result.error);
|
|
return false;
|
|
}
|
|
|
|
const mcpServers = result.settings.mcpServers || [];
|
|
|
|
// Clear existing and add all from server
|
|
// We need to update the store directly since we can't use hooks here
|
|
useAppStore.setState({ mcpServers });
|
|
|
|
logger.info(`Loaded ${mcpServers.length} MCP servers from server`);
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('Failed to load MCP servers:', error);
|
|
return false;
|
|
}
|
|
}
|