mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
Add comprehensive prompt customization system allowing users to customize
all AI prompts (Auto Mode, Agent Runner, Backlog Plan, Enhancement) through
the Settings UI.
## Features
### Core Customization System
- New TypeScript types for prompt customization with enabled flag
- CustomPrompt interface with value and enabled state
- Prompts preserved even when disabled (no data loss)
- Merged prompt system (custom overrides defaults when enabled)
- Persistent storage in ~/.automaker/settings.json
### Settings UI
- New "Prompt Customization" section in Settings
- 4 tabs: Auto Mode, Agent, Backlog Plan, Enhancement
- Toggle-based editing (read-only default → editable custom)
- Dynamic textarea height based on prompt length (120px-600px)
- Visual state indicators (Custom/Default labels)
### Warning System
- Critical prompt warnings for Backlog Plan (JSON format requirement)
- Field-level warnings when editing critical prompts
- Info banners for Auto Mode planning markers
- Color-coded warnings (blue=info, amber=critical)
### Backend Integration
- Auto Mode service loads prompts from settings
- Agent service loads prompts from settings
- Backlog Plan service loads prompts from settings
- Enhancement endpoint loads prompts from settings
- Settings sync includes promptCustomization field
### Files Changed
- libs/types/src/prompts.ts - Type definitions
- libs/prompts/src/defaults.ts - Default prompt values
- libs/prompts/src/merge.ts - Merge utilities
- apps/ui/src/components/views/settings-view/prompts/ - UI components
- apps/server/src/lib/settings-helpers.ts - getPromptCustomization()
- All service files updated to use customizable prompts
## Technical Details
Prompt storage format:
```json
{
"promptCustomization": {
"autoMode": {
"planningLite": {
"value": "Custom prompt text...",
"enabled": true
}
}
}
}
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
363 lines
12 KiB
TypeScript
363 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 { getHttpApiClient } from '@/lib/http-api-client';
|
|
import { isElectron } from '@/lib/electron';
|
|
import { getItem, removeItem } from '@/lib/storage';
|
|
import { useAppStore } from '@/store/app-store';
|
|
|
|
/**
|
|
* 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,
|
|
validationModel: state.validationModel,
|
|
autoLoadClaudeMd: state.autoLoadClaudeMd,
|
|
enableSandboxMode: state.enableSandboxMode,
|
|
keyboardShortcuts: state.keyboardShortcuts,
|
|
aiProfiles: state.aiProfiles,
|
|
mcpServers: state.mcpServers,
|
|
mcpAutoApproveTools: state.mcpAutoApproveTools,
|
|
mcpUnrestrictedTools: state.mcpUnrestrictedTools,
|
|
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) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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).
|
|
*
|
|
* Only functions in Electron mode. Returns false if not in Electron or on error.
|
|
*
|
|
* @returns Promise resolving to true if load succeeded, false otherwise
|
|
*/
|
|
export async function loadMCPServersFromServer(): Promise<boolean> {
|
|
if (!isElectron()) return false;
|
|
|
|
try {
|
|
const api = getHttpApiClient();
|
|
const result = await api.settings.getGlobal();
|
|
|
|
if (!result.success || !result.settings) {
|
|
console.error('[Settings Load] Failed to load settings:', result.error);
|
|
return false;
|
|
}
|
|
|
|
const mcpServers = result.settings.mcpServers || [];
|
|
const mcpAutoApproveTools = result.settings.mcpAutoApproveTools ?? true;
|
|
const mcpUnrestrictedTools = result.settings.mcpUnrestrictedTools ?? true;
|
|
|
|
// Clear existing and add all from server
|
|
// We need to update the store directly since we can't use hooks here
|
|
useAppStore.setState({ mcpServers, mcpAutoApproveTools, mcpUnrestrictedTools });
|
|
|
|
console.log(`[Settings Load] Loaded ${mcpServers.length} MCP servers from server`);
|
|
return true;
|
|
} catch (error) {
|
|
console.error('[Settings Load] Failed to load MCP servers:', error);
|
|
return false;
|
|
}
|
|
}
|