From bc0ef47323b6ef07ba78483040d8e3cc25c91878 Mon Sep 17 00:00:00 2001 From: Stephan Rieche Date: Mon, 29 Dec 2025 23:17:20 +0100 Subject: [PATCH 1/7] feat: add customizable AI prompts with enhanced UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/server/src/index.ts | 2 +- apps/server/src/lib/settings-helpers.ts | 48 +- .../src/routes/backlog-plan/generate-plan.ts | 75 +-- .../server/src/routes/enhance-prompt/index.ts | 6 +- .../routes/enhance-prompt/routes/enhance.ts | 24 +- apps/server/src/services/agent-service.ts | 43 +- apps/server/src/services/auto-mode-service.ts | 184 +------- .../ui/src/components/views/settings-view.tsx | 10 + .../views/settings-view/config/navigation.ts | 2 + .../settings-view/hooks/use-settings-view.ts | 1 + .../views/settings-view/prompts/index.ts | 1 + .../prompts/prompt-customization-section.tsx | 445 ++++++++++++++++++ apps/ui/src/hooks/use-settings-migration.ts | 1 + apps/ui/src/store/app-store.ts | 18 + libs/prompts/src/defaults.ts | 403 ++++++++++++++++ libs/prompts/src/index.ts | 37 ++ libs/prompts/src/merge.ts | 130 +++++ libs/types/src/index.ts | 15 + libs/types/src/prompts.ts | 153 ++++++ libs/types/src/settings.ts | 5 + 20 files changed, 1338 insertions(+), 265 deletions(-) create mode 100644 apps/ui/src/components/views/settings-view/prompts/index.ts create mode 100644 apps/ui/src/components/views/settings-view/prompts/prompt-customization-section.tsx create mode 100644 libs/prompts/src/defaults.ts create mode 100644 libs/prompts/src/merge.ts create mode 100644 libs/types/src/prompts.ts diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 4c8f0421..5bc4b7b7 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -155,7 +155,7 @@ app.use('/api/agent', createAgentRoutes(agentService, events)); app.use('/api/sessions', createSessionsRoutes(agentService)); app.use('/api/features', createFeaturesRoutes(featureLoader)); app.use('/api/auto-mode', createAutoModeRoutes(autoModeService)); -app.use('/api/enhance-prompt', createEnhancePromptRoutes()); +app.use('/api/enhance-prompt', createEnhancePromptRoutes(settingsService)); app.use('/api/worktree', createWorktreeRoutes()); app.use('/api/git', createGitRoutes()); app.use('/api/setup', createSetupRoutes()); diff --git a/apps/server/src/lib/settings-helpers.ts b/apps/server/src/lib/settings-helpers.ts index fee359d5..08168ae9 100644 --- a/apps/server/src/lib/settings-helpers.ts +++ b/apps/server/src/lib/settings-helpers.ts @@ -4,7 +4,13 @@ import type { SettingsService } from '../services/settings-service.js'; import type { ContextFilesResult, ContextFileInfo } from '@automaker/utils'; -import type { MCPServerConfig, McpServerConfig } from '@automaker/types'; +import type { MCPServerConfig, McpServerConfig, PromptCustomization } from '@automaker/types'; +import { + mergeAutoModePrompts, + mergeAgentPrompts, + mergeBacklogPlanPrompts, + mergeEnhancementPrompts, +} from '@automaker/prompts'; /** * Get the autoLoadClaudeMd setting, with project settings taking precedence over global. @@ -255,3 +261,43 @@ function convertToSdkFormat(server: MCPServerConfig): McpServerConfig { env: server.env, }; } + +/** + * Get prompt customization from global settings and merge with defaults. + * Returns prompts merged with built-in defaults - custom prompts override defaults. + * + * @param settingsService - Optional settings service instance + * @param logPrefix - Prefix for log messages + * @returns Promise resolving to merged prompts for all categories + */ +export async function getPromptCustomization( + settingsService?: SettingsService | null, + logPrefix = '[PromptHelper]' +): Promise<{ + autoMode: ReturnType; + agent: ReturnType; + backlogPlan: ReturnType; + enhancement: ReturnType; +}> { + let customization: PromptCustomization = {}; + + if (settingsService) { + try { + const globalSettings = await settingsService.getGlobalSettings(); + customization = globalSettings.promptCustomization || {}; + console.log(`${logPrefix} Loaded prompt customization from settings`); + } catch (error) { + console.error(`${logPrefix} Failed to load prompt customization:`, error); + // Fall through to use empty customization (all defaults) + } + } else { + console.log(`${logPrefix} SettingsService not available, using default prompts`); + } + + return { + autoMode: mergeAutoModePrompts(customization.autoMode), + agent: mergeAgentPrompts(customization.agent), + backlogPlan: mergeBacklogPlanPrompts(customization.backlogPlan), + enhancement: mergeEnhancementPrompts(customization.enhancement), + }; +} diff --git a/apps/server/src/routes/backlog-plan/generate-plan.ts b/apps/server/src/routes/backlog-plan/generate-plan.ts index 737f4222..f67cac04 100644 --- a/apps/server/src/routes/backlog-plan/generate-plan.ts +++ b/apps/server/src/routes/backlog-plan/generate-plan.ts @@ -8,7 +8,7 @@ import { FeatureLoader } from '../../services/feature-loader.js'; import { ProviderFactory } from '../../providers/provider-factory.js'; import { logger, setRunningState, getErrorMessage } from './common.js'; import type { SettingsService } from '../../services/settings-service.js'; -import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js'; +import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js'; const featureLoader = new FeatureLoader(); @@ -79,72 +79,17 @@ export async function generateBacklogPlan( content: `Loaded ${features.length} features from backlog`, }); + // Load prompts from settings + const prompts = await getPromptCustomization(settingsService, '[BacklogPlan]'); + // Build the system prompt - const systemPrompt = `You are an AI assistant helping to modify a software project's feature backlog. -You will be given the current list of features and a user request to modify the backlog. + const systemPrompt = prompts.backlogPlan.systemPrompt; -IMPORTANT CONTEXT (automatically injected): -- Remember to update the dependency graph if deleting existing features -- Remember to define dependencies on new features hooked into relevant existing ones -- Maintain dependency graph integrity (no orphaned dependencies) -- When deleting a feature, identify which other features depend on it - -Your task is to analyze the request and produce a structured JSON plan with: -1. Features to ADD (include title, description, category, and dependencies) -2. Features to UPDATE (specify featureId and the updates) -3. Features to DELETE (specify featureId) -4. A summary of the changes -5. Any dependency updates needed (removed dependencies due to deletions, new dependencies for new features) - -Respond with ONLY a JSON object in this exact format: -\`\`\`json -{ - "changes": [ - { - "type": "add", - "feature": { - "title": "Feature title", - "description": "Feature description", - "category": "Category name", - "dependencies": ["existing-feature-id"], - "priority": 1 - }, - "reason": "Why this feature should be added" - }, - { - "type": "update", - "featureId": "existing-feature-id", - "feature": { - "title": "Updated title" - }, - "reason": "Why this feature should be updated" - }, - { - "type": "delete", - "featureId": "feature-id-to-delete", - "reason": "Why this feature should be deleted" - } - ], - "summary": "Brief overview of all proposed changes", - "dependencyUpdates": [ - { - "featureId": "feature-that-depended-on-deleted", - "removedDependencies": ["deleted-feature-id"], - "addedDependencies": [] - } - ] -} -\`\`\``; - - // Build the user prompt - const userPrompt = `Current Features in Backlog: -${formatFeaturesForPrompt(features)} - ---- - -User Request: ${prompt} - -Please analyze the current backlog and the user's request, then provide a JSON plan for the modifications.`; + // Build the user prompt from template + const currentFeatures = formatFeaturesForPrompt(features); + const userPrompt = prompts.backlogPlan.userPromptTemplate + .replace('{{currentFeatures}}', currentFeatures) + .replace('{{userRequest}}', prompt); events.emit('backlog-plan:event', { type: 'backlog_plan_progress', diff --git a/apps/server/src/routes/enhance-prompt/index.ts b/apps/server/src/routes/enhance-prompt/index.ts index 952bf347..db50ea4c 100644 --- a/apps/server/src/routes/enhance-prompt/index.ts +++ b/apps/server/src/routes/enhance-prompt/index.ts @@ -6,17 +6,19 @@ */ import { Router } from 'express'; +import type { SettingsService } from '../../services/settings-service.js'; import { createEnhanceHandler } from './routes/enhance.js'; /** * Create the enhance-prompt router * + * @param settingsService - Settings service for loading custom prompts * @returns Express router with enhance-prompt endpoints */ -export function createEnhancePromptRoutes(): Router { +export function createEnhancePromptRoutes(settingsService?: SettingsService): Router { const router = Router(); - router.post('/', createEnhanceHandler()); + router.post('/', createEnhanceHandler(settingsService)); return router; } diff --git a/apps/server/src/routes/enhance-prompt/routes/enhance.ts b/apps/server/src/routes/enhance-prompt/routes/enhance.ts index e0edd515..0b06891d 100644 --- a/apps/server/src/routes/enhance-prompt/routes/enhance.ts +++ b/apps/server/src/routes/enhance-prompt/routes/enhance.ts @@ -10,8 +10,9 @@ import { query } from '@anthropic-ai/claude-agent-sdk'; import { createLogger } from '@automaker/utils'; import { resolveModelString } from '@automaker/model-resolver'; import { CLAUDE_MODEL_MAP } from '@automaker/types'; +import type { SettingsService } from '../../../services/settings-service.js'; +import { getPromptCustomization } from '../../../lib/settings-helpers.js'; import { - getSystemPrompt, buildUserPrompt, isValidEnhancementMode, type EnhancementMode, @@ -83,9 +84,12 @@ async function extractTextFromStream( /** * Create the enhance request handler * + * @param settingsService - Optional settings service for loading custom prompts * @returns Express request handler for text enhancement */ -export function createEnhanceHandler(): (req: Request, res: Response) => Promise { +export function createEnhanceHandler( + settingsService?: SettingsService +): (req: Request, res: Response) => Promise { return async (req: Request, res: Response): Promise => { try { const { originalText, enhancementMode, model } = req.body as EnhanceRequestBody; @@ -128,8 +132,20 @@ export function createEnhanceHandler(): (req: Request, res: Response) => Promise logger.info(`Enhancing text with mode: ${validMode}, length: ${trimmedText.length} chars`); - // Get the system prompt for this mode - const systemPrompt = getSystemPrompt(validMode); + // Load enhancement prompts from settings (merges custom + defaults) + const prompts = await getPromptCustomization(settingsService, '[EnhancePrompt]'); + + // Get the system prompt for this mode from merged prompts + const systemPrompt = + validMode === 'improve' + ? prompts.enhancement.improveSystemPrompt + : validMode === 'technical' + ? prompts.enhancement.technicalSystemPrompt + : validMode === 'simplify' + ? prompts.enhancement.simplifySystemPrompt + : prompts.enhancement.acceptanceSystemPrompt; + + logger.debug(`Using ${validMode} system prompt (length: ${systemPrompt.length} chars)`); // Build the user prompt with few-shot examples // This helps the model understand this is text transformation, not a coding task diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index 63c220db..102e6241 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -23,6 +23,7 @@ import { filterClaudeMdFromContext, getMCPServersFromSettings, getMCPPermissionSettings, + getPromptCustomization, } from '../lib/settings-helpers.js'; interface Message { @@ -75,6 +76,7 @@ export class AgentService { private metadataFile: string; private events: EventEmitter; private settingsService: SettingsService | null = null; + private agentSystemPrompt: string | null = null; constructor(dataDir: string, events: EventEmitter, settingsService?: SettingsService) { this.stateDir = path.join(dataDir, 'agent-sessions'); @@ -246,7 +248,7 @@ export class AgentService { const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd); // Build combined system prompt with base prompt and context files - const baseSystemPrompt = this.getSystemPrompt(); + const baseSystemPrompt = await this.getSystemPrompt(); const combinedSystemPrompt = contextFilesPrompt ? `${contextFilesPrompt}\n\n${baseSystemPrompt}` : baseSystemPrompt; @@ -781,38 +783,13 @@ export class AgentService { this.events.emit('agent:stream', { sessionId, ...data }); } - private getSystemPrompt(): string { - return `You are an AI assistant helping users build software. You are part of the Automaker application, -which is designed to help developers plan, design, and implement software projects autonomously. - -**Feature Storage:** -Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. -Use the UpdateFeatureStatus tool to manage features, not direct file edits. - -Your role is to: -- Help users define their project requirements and specifications -- Ask clarifying questions to better understand their needs -- Suggest technical approaches and architectures -- Guide them through the development process -- Be conversational and helpful -- Write, edit, and modify code files as requested -- Execute commands and tests -- Search and analyze the codebase - -When discussing projects, help users think through: -- Core functionality and features -- Technical stack choices -- Data models and architecture -- User experience considerations -- Testing strategies - -You have full access to the codebase and can: -- Read files to understand existing code -- Write new files -- Edit existing files -- Run bash commands -- Search for code patterns -- Execute tests and builds`; + private async getSystemPrompt(): Promise { + // Load from settings if not already cached + if (!this.agentSystemPrompt) { + const prompts = await getPromptCustomization(this.settingsService, '[AgentService]'); + this.agentSystemPrompt = prompts.agent.systemPrompt; + } + return this.agentSystemPrompt; } private generateId(): string { diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 9ba48361..a7414541 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -39,6 +39,7 @@ import { filterClaudeMdFromContext, getMCPServersFromSettings, getMCPPermissionSettings, + getPromptCustomization, } from '../lib/settings-helpers.js'; const execAsync = promisify(exec); @@ -67,162 +68,6 @@ interface PlanSpec { tasks?: ParsedTask[]; } -const PLANNING_PROMPTS = { - lite: `## Planning Phase (Lite Mode) - -IMPORTANT: Do NOT output exploration text, tool usage, or thinking before the plan. Start DIRECTLY with the planning outline format below. Silently analyze the codebase first, then output ONLY the structured plan. - -Create a brief planning outline: - -1. **Goal**: What are we accomplishing? (1 sentence) -2. **Approach**: How will we do it? (2-3 sentences) -3. **Files to Touch**: List files and what changes -4. **Tasks**: Numbered task list (3-7 items) -5. **Risks**: Any gotchas to watch for - -After generating the outline, output: -"[PLAN_GENERATED] Planning outline complete." - -Then proceed with implementation.`, - - lite_with_approval: `## Planning Phase (Lite Mode) - -IMPORTANT: Do NOT output exploration text, tool usage, or thinking before the plan. Start DIRECTLY with the planning outline format below. Silently analyze the codebase first, then output ONLY the structured plan. - -Create a brief planning outline: - -1. **Goal**: What are we accomplishing? (1 sentence) -2. **Approach**: How will we do it? (2-3 sentences) -3. **Files to Touch**: List files and what changes -4. **Tasks**: Numbered task list (3-7 items) -5. **Risks**: Any gotchas to watch for - -After generating the outline, output: -"[SPEC_GENERATED] Please review the planning outline above. Reply with 'approved' to proceed or provide feedback for revisions." - -DO NOT proceed with implementation until you receive explicit approval.`, - - spec: `## Specification Phase (Spec Mode) - -IMPORTANT: Do NOT output exploration text, tool usage, or thinking before the spec. Start DIRECTLY with the specification format below. Silently analyze the codebase first, then output ONLY the structured specification. - -Generate a specification with an actionable task breakdown. WAIT for approval before implementing. - -### Specification Format - -1. **Problem**: What problem are we solving? (user perspective) - -2. **Solution**: Brief approach (1-2 sentences) - -3. **Acceptance Criteria**: 3-5 items in GIVEN-WHEN-THEN format - - GIVEN [context], WHEN [action], THEN [outcome] - -4. **Files to Modify**: - | File | Purpose | Action | - |------|---------|--------| - | path/to/file | description | create/modify/delete | - -5. **Implementation Tasks**: - Use this EXACT format for each task (the system will parse these): - \`\`\`tasks - - [ ] T001: [Description] | File: [path/to/file] - - [ ] T002: [Description] | File: [path/to/file] - - [ ] T003: [Description] | File: [path/to/file] - \`\`\` - - Task ID rules: - - Sequential: T001, T002, T003, etc. - - Description: Clear action (e.g., "Create user model", "Add API endpoint") - - File: Primary file affected (helps with context) - - Order by dependencies (foundational tasks first) - -6. **Verification**: How to confirm feature works - -After generating the spec, output on its own line: -"[SPEC_GENERATED] Please review the specification above. Reply with 'approved' to proceed or provide feedback for revisions." - -DO NOT proceed with implementation until you receive explicit approval. - -When approved, execute tasks SEQUENTIALLY in order. For each task: -1. BEFORE starting, output: "[TASK_START] T###: Description" -2. Implement the task -3. AFTER completing, output: "[TASK_COMPLETE] T###: Brief summary" - -This allows real-time progress tracking during implementation.`, - - full: `## Full Specification Phase (Full SDD Mode) - -IMPORTANT: Do NOT output exploration text, tool usage, or thinking before the spec. Start DIRECTLY with the specification format below. Silently analyze the codebase first, then output ONLY the structured specification. - -Generate a comprehensive specification with phased task breakdown. WAIT for approval before implementing. - -### Specification Format - -1. **Problem Statement**: 2-3 sentences from user perspective - -2. **User Story**: As a [user], I want [goal], so that [benefit] - -3. **Acceptance Criteria**: Multiple scenarios with GIVEN-WHEN-THEN - - **Happy Path**: GIVEN [context], WHEN [action], THEN [expected outcome] - - **Edge Cases**: GIVEN [edge condition], WHEN [action], THEN [handling] - - **Error Handling**: GIVEN [error condition], WHEN [action], THEN [error response] - -4. **Technical Context**: - | Aspect | Value | - |--------|-------| - | Affected Files | list of files | - | Dependencies | external libs if any | - | Constraints | technical limitations | - | Patterns to Follow | existing patterns in codebase | - -5. **Non-Goals**: What this feature explicitly does NOT include - -6. **Implementation Tasks**: - Use this EXACT format for each task (the system will parse these): - \`\`\`tasks - ## Phase 1: Foundation - - [ ] T001: [Description] | File: [path/to/file] - - [ ] T002: [Description] | File: [path/to/file] - - ## Phase 2: Core Implementation - - [ ] T003: [Description] | File: [path/to/file] - - [ ] T004: [Description] | File: [path/to/file] - - ## Phase 3: Integration & Testing - - [ ] T005: [Description] | File: [path/to/file] - - [ ] T006: [Description] | File: [path/to/file] - \`\`\` - - Task ID rules: - - Sequential across all phases: T001, T002, T003, etc. - - Description: Clear action verb + target - - File: Primary file affected - - Order by dependencies within each phase - - Phase structure helps organize complex work - -7. **Success Metrics**: How we know it's done (measurable criteria) - -8. **Risks & Mitigations**: - | Risk | Mitigation | - |------|------------| - | description | approach | - -After generating the spec, output on its own line: -"[SPEC_GENERATED] Please review the comprehensive specification above. Reply with 'approved' to proceed or provide feedback for revisions." - -DO NOT proceed with implementation until you receive explicit approval. - -When approved, execute tasks SEQUENTIALLY by phase. For each task: -1. BEFORE starting, output: "[TASK_START] T###: Description" -2. Implement the task -3. AFTER completing, output: "[TASK_COMPLETE] T###: Brief summary" - -After completing all tasks in a phase, output: -"[PHASE_COMPLETE] Phase N complete" - -This allows real-time progress tracking during implementation.`, -}; - /** * Parse tasks from generated spec content * Looks for the ```tasks code block and extracts task lines @@ -355,6 +200,7 @@ export class AutoModeService { private config: AutoModeConfig | null = null; private pendingApprovals = new Map(); private settingsService: SettingsService | null = null; + private planningPrompts: Record | null = null; constructor(events: EventEmitter, settingsService?: SettingsService) { this.events = events; @@ -593,7 +439,7 @@ export class AutoModeService { } else { // Normal flow: build prompt with planning phase const featurePrompt = this.buildFeaturePrompt(feature); - const planningPrefix = this.getPlanningPromptPrefix(feature); + const planningPrefix = await this.getPlanningPromptPrefix(feature); prompt = planningPrefix + featurePrompt; // Emit planning mode info @@ -1756,23 +1602,43 @@ Format your response as a structured markdown document.`; return firstLine.substring(0, 57) + '...'; } + /** + * Load planning prompts from settings + */ + private async loadPlanningPrompts(): Promise { + if (this.planningPrompts) { + return; // Already loaded + } + + const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]'); + this.planningPrompts = { + lite: prompts.autoMode.planningLite, + lite_with_approval: prompts.autoMode.planningLiteWithApproval, + spec: prompts.autoMode.planningSpec, + full: prompts.autoMode.planningFull, + }; + } + /** * Get the planning prompt prefix based on feature's planning mode */ - private getPlanningPromptPrefix(feature: Feature): string { + private async getPlanningPromptPrefix(feature: Feature): Promise { const mode = feature.planningMode || 'skip'; if (mode === 'skip') { return ''; // No planning phase } + // Load prompts if not already loaded + await this.loadPlanningPrompts(); + // For lite mode, use the approval variant if requirePlanApproval is true let promptKey: string = mode; if (mode === 'lite' && feature.requirePlanApproval === true) { promptKey = 'lite_with_approval'; } - const planningPrompt = PLANNING_PROMPTS[promptKey as keyof typeof PLANNING_PROMPTS]; + const planningPrompt = this.planningPrompts![promptKey]; if (!planningPrompt) { return ''; } diff --git a/apps/ui/src/components/views/settings-view.tsx b/apps/ui/src/components/views/settings-view.tsx index 70d19e5f..fbca8470 100644 --- a/apps/ui/src/components/views/settings-view.tsx +++ b/apps/ui/src/components/views/settings-view.tsx @@ -19,6 +19,7 @@ import { KeyboardShortcutsSection } from './settings-view/keyboard-shortcuts/key import { FeatureDefaultsSection } from './settings-view/feature-defaults/feature-defaults-section'; import { DangerZoneSection } from './settings-view/danger-zone/danger-zone-section'; import { MCPServersSection } from './settings-view/mcp-servers'; +import { PromptCustomizationSection } from './settings-view/prompts'; import type { Project as SettingsProject, Theme } from './settings-view/shared/types'; import type { Project as ElectronProject } from '@/lib/electron'; @@ -53,6 +54,8 @@ export function SettingsView() { setAutoLoadClaudeMd, enableSandboxMode, setEnableSandboxMode, + promptCustomization, + setPromptCustomization, } = useAppStore(); // Hide usage tracking when using API key (only show for Claude Code CLI users) @@ -119,6 +122,13 @@ export function SettingsView() { ); case 'mcp-servers': return ; + case 'prompts': + return ( + + ); case 'ai-enhancement': return ; case 'appearance': diff --git a/apps/ui/src/components/views/settings-view/config/navigation.ts b/apps/ui/src/components/views/settings-view/config/navigation.ts index d478bb4d..879bb470 100644 --- a/apps/ui/src/components/views/settings-view/config/navigation.ts +++ b/apps/ui/src/components/views/settings-view/config/navigation.ts @@ -10,6 +10,7 @@ import { Trash2, Sparkles, Plug, + MessageSquareText, } from 'lucide-react'; import type { SettingsViewId } from '../hooks/use-settings-view'; @@ -24,6 +25,7 @@ export const NAV_ITEMS: NavigationItem[] = [ { id: 'api-keys', label: 'API Keys', icon: Key }, { id: 'claude', label: 'Claude', icon: Terminal }, { id: 'mcp-servers', label: 'MCP Servers', icon: Plug }, + { id: 'prompts', label: 'Prompt Customization', icon: MessageSquareText }, { id: 'ai-enhancement', label: 'AI Enhancement', icon: Sparkles }, { id: 'appearance', label: 'Appearance', icon: Palette }, { id: 'terminal', label: 'Terminal', icon: SquareTerminal }, diff --git a/apps/ui/src/components/views/settings-view/hooks/use-settings-view.ts b/apps/ui/src/components/views/settings-view/hooks/use-settings-view.ts index 48c406b2..da7d4f0a 100644 --- a/apps/ui/src/components/views/settings-view/hooks/use-settings-view.ts +++ b/apps/ui/src/components/views/settings-view/hooks/use-settings-view.ts @@ -4,6 +4,7 @@ export type SettingsViewId = | 'api-keys' | 'claude' | 'mcp-servers' + | 'prompts' | 'ai-enhancement' | 'appearance' | 'terminal' diff --git a/apps/ui/src/components/views/settings-view/prompts/index.ts b/apps/ui/src/components/views/settings-view/prompts/index.ts new file mode 100644 index 00000000..fd8d3989 --- /dev/null +++ b/apps/ui/src/components/views/settings-view/prompts/index.ts @@ -0,0 +1 @@ +export { PromptCustomizationSection } from './prompt-customization-section'; diff --git a/apps/ui/src/components/views/settings-view/prompts/prompt-customization-section.tsx b/apps/ui/src/components/views/settings-view/prompts/prompt-customization-section.tsx new file mode 100644 index 00000000..c1b1888a --- /dev/null +++ b/apps/ui/src/components/views/settings-view/prompts/prompt-customization-section.tsx @@ -0,0 +1,445 @@ +import { useState } from 'react'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Button } from '@/components/ui/button'; +import { Switch } from '@/components/ui/switch'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + MessageSquareText, + Bot, + KanbanSquare, + Sparkles, + RotateCcw, + Info, + AlertTriangle, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { PromptCustomization, CustomPrompt } from '@automaker/types'; +import { + DEFAULT_AUTO_MODE_PROMPTS, + DEFAULT_AGENT_PROMPTS, + DEFAULT_BACKLOG_PLAN_PROMPTS, + DEFAULT_ENHANCEMENT_PROMPTS, +} from '@automaker/prompts'; + +interface PromptCustomizationSectionProps { + promptCustomization?: PromptCustomization; + onPromptCustomizationChange: (customization: PromptCustomization) => void; +} + +interface PromptFieldProps { + label: string; + description: string; + defaultValue: string; + customValue?: CustomPrompt; + onCustomValueChange: (value: CustomPrompt | undefined) => void; + critical?: boolean; // Whether this prompt requires strict output format +} + +/** + * Calculate dynamic minimum height based on content length + * Ensures long prompts have adequate space + */ +function calculateMinHeight(text: string): string { + const lines = text.split('\n').length; + const estimatedLines = Math.max(lines, Math.ceil(text.length / 80)); + + // Min 120px, scales up for longer content, max 600px + const minHeight = Math.min(Math.max(120, estimatedLines * 20), 600); + return `${minHeight}px`; +} + +/** + * PromptField Component + * + * Shows a prompt with a toggle to switch between default and custom mode. + * - Toggle OFF: Shows default prompt in read-only mode, custom value is preserved but not used + * - Toggle ON: Allows editing, custom value is used instead of default + * + * IMPORTANT: Custom value is ALWAYS preserved, even when toggle is OFF. + * This prevents users from losing their work when temporarily switching to default. + */ +function PromptField({ + label, + description, + defaultValue, + customValue, + onCustomValueChange, + critical = false, +}: PromptFieldProps) { + const isEnabled = customValue?.enabled ?? false; + const displayValue = isEnabled ? (customValue?.value ?? defaultValue) : defaultValue; + const minHeight = calculateMinHeight(displayValue); + + const handleToggle = (enabled: boolean) => { + if (enabled) { + // Enable custom mode - preserve existing custom value or initialize with default + const value = customValue?.value ?? defaultValue; + onCustomValueChange({ value, enabled: true }); + } else { + // Disable custom mode - preserve custom value but mark as disabled + const value = customValue?.value ?? defaultValue; + onCustomValueChange({ value, enabled: false }); + } + }; + + const handleTextChange = (newValue: string) => { + // Only allow editing when enabled + if (isEnabled) { + onCustomValueChange({ value: newValue, enabled: true }); + } + }; + + return ( +
+ {critical && isEnabled && ( +
+ +
+

Critical Prompt

+

+ This prompt requires a specific output format. Changing it incorrectly may break + functionality. Only modify if you understand the expected structure. +

+
+
+ )} +
+ +
+ {isEnabled ? 'Custom' : 'Default'} + +
+
+