feat: add customizable AI prompts with enhanced UX

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>
This commit is contained in:
Stephan Rieche
2025-12-29 23:17:20 +01:00
parent 25c9259b50
commit bc0ef47323
20 changed files with 1338 additions and 265 deletions

View File

@@ -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());

View File

@@ -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<typeof mergeAutoModePrompts>;
agent: ReturnType<typeof mergeAgentPrompts>;
backlogPlan: ReturnType<typeof mergeBacklogPlanPrompts>;
enhancement: ReturnType<typeof mergeEnhancementPrompts>;
}> {
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),
};
}

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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<void> {
export function createEnhanceHandler(
settingsService?: SettingsService
): (req: Request, res: Response) => Promise<void> {
return async (req: Request, res: Response): Promise<void> => {
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

View File

@@ -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<string> {
// 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 {

View File

@@ -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<string, PendingApproval>();
private settingsService: SettingsService | null = null;
private planningPrompts: Record<string, string> | 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<void> {
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<string> {
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 '';
}

View File

@@ -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 <MCPServersSection />;
case 'prompts':
return (
<PromptCustomizationSection
promptCustomization={promptCustomization}
onPromptCustomizationChange={setPromptCustomization}
/>
);
case 'ai-enhancement':
return <AIEnhancementSection />;
case 'appearance':

View File

@@ -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 },

View File

@@ -4,6 +4,7 @@ export type SettingsViewId =
| 'api-keys'
| 'claude'
| 'mcp-servers'
| 'prompts'
| 'ai-enhancement'
| 'appearance'
| 'terminal'

View File

@@ -0,0 +1 @@
export { PromptCustomizationSection } from './prompt-customization-section';

View File

@@ -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 (
<div className="space-y-2">
{critical && isEnabled && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-amber-500/10 border border-amber-500/20">
<AlertTriangle className="w-4 h-4 text-amber-500 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-xs font-medium text-amber-500">Critical Prompt</p>
<p className="text-xs text-muted-foreground mt-1">
This prompt requires a specific output format. Changing it incorrectly may break
functionality. Only modify if you understand the expected structure.
</p>
</div>
</div>
)}
<div className="flex items-center justify-between">
<Label htmlFor={label} className="text-sm font-medium">
{label}
</Label>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{isEnabled ? 'Custom' : 'Default'}</span>
<Switch
checked={isEnabled}
onCheckedChange={handleToggle}
className="data-[state=checked]:bg-brand-500"
/>
</div>
</div>
<Textarea
id={label}
value={displayValue}
onChange={(e) => handleTextChange(e.target.value)}
readOnly={!isEnabled}
style={{ minHeight }}
className={cn(
'font-mono text-xs resize-y',
!isEnabled && 'cursor-not-allowed bg-muted/50 text-muted-foreground'
)}
/>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
);
}
/**
* PromptCustomizationSection Component
*
* Allows users to customize AI prompts for different parts of the application:
* - Auto Mode (feature implementation)
* - Agent Runner (interactive chat)
* - Backlog Plan (Kanban planning)
* - Enhancement (feature description improvement)
*/
export function PromptCustomizationSection({
promptCustomization = {},
onPromptCustomizationChange,
}: PromptCustomizationSectionProps) {
const [activeTab, setActiveTab] = useState('auto-mode');
const updatePrompt = (
category: keyof PromptCustomization,
field: string,
value: CustomPrompt | undefined
) => {
const updated = {
...promptCustomization,
[category]: {
...promptCustomization[category],
[field]: value,
},
};
onPromptCustomizationChange(updated);
};
const resetToDefaults = (category: keyof PromptCustomization) => {
const updated = {
...promptCustomization,
[category]: {},
};
onPromptCustomizationChange(updated);
};
const resetAllToDefaults = () => {
onPromptCustomizationChange({});
};
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
data-testid="prompt-customization-section"
>
{/* Header */}
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<MessageSquareText className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">
Prompt Customization
</h2>
</div>
<Button variant="outline" size="sm" onClick={resetAllToDefaults} className="gap-2">
<RotateCcw className="w-4 h-4" />
Reset All to Defaults
</Button>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Customize AI prompts for Auto Mode, Agent Runner, and other features.
</p>
</div>
{/* Info Banner */}
<div className="px-6 pt-6">
<div className="flex items-start gap-3 p-4 rounded-xl bg-blue-500/10 border border-blue-500/20">
<Info className="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" />
<div className="space-y-1">
<p className="text-sm text-foreground font-medium">How to Customize Prompts</p>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
Toggle the switch to enable custom mode and edit the prompt. When disabled, the
default built-in prompt is used. You can use the default as a starting point by
enabling the toggle.
</p>
</div>
</div>
</div>
{/* Tabs */}
<div className="p-6">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-4 w-full">
<TabsTrigger value="auto-mode" className="gap-2">
<Bot className="w-4 h-4" />
Auto Mode
</TabsTrigger>
<TabsTrigger value="agent" className="gap-2">
<MessageSquareText className="w-4 h-4" />
Agent
</TabsTrigger>
<TabsTrigger value="backlog-plan" className="gap-2">
<KanbanSquare className="w-4 h-4" />
Backlog Plan
</TabsTrigger>
<TabsTrigger value="enhancement" className="gap-2">
<Sparkles className="w-4 h-4" />
Enhancement
</TabsTrigger>
</TabsList>
{/* Auto Mode Tab */}
<TabsContent value="auto-mode" className="space-y-6 mt-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-foreground">Auto Mode Prompts</h3>
<Button
variant="ghost"
size="sm"
onClick={() => resetToDefaults('autoMode')}
className="gap-2"
>
<RotateCcw className="w-3 h-3" />
Reset Section
</Button>
</div>
{/* Info Banner for Auto Mode */}
<div className="flex items-start gap-3 p-4 rounded-xl bg-blue-500/10 border border-blue-500/20">
<Info className="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" />
<div className="space-y-1">
<p className="text-sm text-foreground font-medium">Planning Mode Markers</p>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
Planning prompts use special markers like{' '}
<code className="px-1 py-0.5 rounded bg-muted text-xs">[PLAN_GENERATED]</code> and{' '}
<code className="px-1 py-0.5 rounded bg-muted text-xs">[SPEC_GENERATED]</code> to
control the Auto Mode workflow. These markers must be preserved for proper
functionality.
</p>
</div>
</div>
<div className="space-y-4">
<PromptField
label="Planning: Lite Mode"
description="Quick planning outline without approval requirement"
defaultValue={DEFAULT_AUTO_MODE_PROMPTS.planningLite}
customValue={promptCustomization?.autoMode?.planningLite}
onCustomValueChange={(value) => updatePrompt('autoMode', 'planningLite', value)}
critical={true}
/>
<PromptField
label="Planning: Lite with Approval"
description="Planning outline that waits for user approval"
defaultValue={DEFAULT_AUTO_MODE_PROMPTS.planningLiteWithApproval}
customValue={promptCustomization?.autoMode?.planningLiteWithApproval}
onCustomValueChange={(value) =>
updatePrompt('autoMode', 'planningLiteWithApproval', value)
}
critical={true}
/>
<PromptField
label="Planning: Spec Mode"
description="Detailed specification with task breakdown"
defaultValue={DEFAULT_AUTO_MODE_PROMPTS.planningSpec}
customValue={promptCustomization?.autoMode?.planningSpec}
onCustomValueChange={(value) => updatePrompt('autoMode', 'planningSpec', value)}
critical={true}
/>
<PromptField
label="Planning: Full SDD Mode"
description="Comprehensive Software Design Document with phased implementation"
defaultValue={DEFAULT_AUTO_MODE_PROMPTS.planningFull}
customValue={promptCustomization?.autoMode?.planningFull}
onCustomValueChange={(value) => updatePrompt('autoMode', 'planningFull', value)}
critical={true}
/>
</div>
</TabsContent>
{/* Agent Tab */}
<TabsContent value="agent" className="space-y-6 mt-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-foreground">Agent Runner Prompts</h3>
<Button
variant="ghost"
size="sm"
onClick={() => resetToDefaults('agent')}
className="gap-2"
>
<RotateCcw className="w-3 h-3" />
Reset Section
</Button>
</div>
<div className="space-y-4">
<PromptField
label="System Prompt"
description="Defines the AI's role and behavior in interactive chat sessions"
defaultValue={DEFAULT_AGENT_PROMPTS.systemPrompt}
customValue={promptCustomization?.agent?.systemPrompt}
onCustomValueChange={(value) => updatePrompt('agent', 'systemPrompt', value)}
/>
</div>
</TabsContent>
{/* Backlog Plan Tab */}
<TabsContent value="backlog-plan" className="space-y-6 mt-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-foreground">Backlog Planning Prompts</h3>
<Button
variant="ghost"
size="sm"
onClick={() => resetToDefaults('backlogPlan')}
className="gap-2"
>
<RotateCcw className="w-3 h-3" />
Reset Section
</Button>
</div>
{/* Critical Warning for Backlog Plan */}
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
<AlertTriangle className="w-5 h-5 text-amber-500 mt-0.5 flex-shrink-0" />
<div className="space-y-1">
<p className="text-sm text-foreground font-medium">Warning: Critical Prompts</p>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
Backlog plan prompts require a strict JSON output format. Modifying these prompts
incorrectly can break the backlog planning feature and potentially corrupt your
feature data. Only customize if you fully understand the expected output
structure.
</p>
</div>
</div>
<div className="space-y-4">
<PromptField
label="System Prompt"
description="Defines how the AI modifies the feature backlog (Plan button on Kanban board)"
defaultValue={DEFAULT_BACKLOG_PLAN_PROMPTS.systemPrompt}
customValue={promptCustomization?.backlogPlan?.systemPrompt}
onCustomValueChange={(value) => updatePrompt('backlogPlan', 'systemPrompt', value)}
critical={true}
/>
</div>
</TabsContent>
{/* Enhancement Tab */}
<TabsContent value="enhancement" className="space-y-6 mt-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-foreground">Enhancement Prompts</h3>
<Button
variant="ghost"
size="sm"
onClick={() => resetToDefaults('enhancement')}
className="gap-2"
>
<RotateCcw className="w-3 h-3" />
Reset Section
</Button>
</div>
<div className="space-y-4">
<PromptField
label="Improve Mode"
description="Transform vague requests into clear, actionable tasks"
defaultValue={DEFAULT_ENHANCEMENT_PROMPTS.improveSystemPrompt}
customValue={promptCustomization?.enhancement?.improveSystemPrompt}
onCustomValueChange={(value) =>
updatePrompt('enhancement', 'improveSystemPrompt', value)
}
/>
<PromptField
label="Technical Mode"
description="Add implementation details and technical specifications"
defaultValue={DEFAULT_ENHANCEMENT_PROMPTS.technicalSystemPrompt}
customValue={promptCustomization?.enhancement?.technicalSystemPrompt}
onCustomValueChange={(value) =>
updatePrompt('enhancement', 'technicalSystemPrompt', value)
}
/>
<PromptField
label="Simplify Mode"
description="Make verbose descriptions concise and focused"
defaultValue={DEFAULT_ENHANCEMENT_PROMPTS.simplifySystemPrompt}
customValue={promptCustomization?.enhancement?.simplifySystemPrompt}
onCustomValueChange={(value) =>
updatePrompt('enhancement', 'simplifySystemPrompt', value)
}
/>
<PromptField
label="Acceptance Criteria Mode"
description="Add testable acceptance criteria to descriptions"
defaultValue={DEFAULT_ENHANCEMENT_PROMPTS.acceptanceSystemPrompt}
customValue={promptCustomization?.enhancement?.acceptanceSystemPrompt}
onCustomValueChange={(value) =>
updatePrompt('enhancement', 'acceptanceSystemPrompt', value)
}
/>
</div>
</TabsContent>
</Tabs>
</div>
</div>
);
}

View File

@@ -231,6 +231,7 @@ export async function syncSettingsToServer(): Promise<boolean> {
mcpServers: state.mcpServers,
mcpAutoApproveTools: state.mcpAutoApproveTools,
mcpUnrestrictedTools: state.mcpUnrestrictedTools,
promptCustomization: state.promptCustomization,
projects: state.projects,
trashedProjects: state.trashedProjects,
projectHistory: state.projectHistory,

View File

@@ -11,6 +11,7 @@ import type {
FeatureStatusWithPipeline,
PipelineConfig,
PipelineStep,
PromptCustomization,
} from '@automaker/types';
// Re-export ThemeMode for convenience
@@ -492,6 +493,9 @@ export interface AppState {
mcpAutoApproveTools: boolean; // Auto-approve MCP tool calls without permission prompts
mcpUnrestrictedTools: boolean; // Allow unrestricted tools when MCP servers are enabled
// Prompt Customization
promptCustomization: PromptCustomization; // Custom prompts for Auto Mode, Agent, Backlog Plan, Enhancement
// Project Analysis
projectAnalysis: ProjectAnalysis | null;
isAnalyzing: boolean;
@@ -774,6 +778,9 @@ export interface AppActions {
setMcpAutoApproveTools: (enabled: boolean) => Promise<void>;
setMcpUnrestrictedTools: (enabled: boolean) => Promise<void>;
// Prompt Customization actions
setPromptCustomization: (customization: PromptCustomization) => Promise<void>;
// AI Profile actions
addAIProfile: (profile: Omit<AIProfile, 'id'>) => void;
updateAIProfile: (id: string, updates: Partial<AIProfile>) => void;
@@ -972,6 +979,7 @@ const initialState: AppState = {
mcpServers: [], // No MCP servers configured by default
mcpAutoApproveTools: true, // Default to enabled - bypass permission prompts for MCP tools
mcpUnrestrictedTools: true, // Default to enabled - don't filter allowedTools when MCP enabled
promptCustomization: {}, // Empty by default - all prompts use built-in defaults
aiProfiles: DEFAULT_AI_PROFILES,
projectAnalysis: null,
isAnalyzing: false,
@@ -1628,6 +1636,14 @@ export const useAppStore = create<AppState & AppActions>()(
await syncSettingsToServer();
},
// Prompt Customization actions
setPromptCustomization: async (customization) => {
set({ promptCustomization: customization });
// Sync to server settings file
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncSettingsToServer();
},
// AI Profile actions
addAIProfile: (profile) => {
const id = `profile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
@@ -2909,6 +2925,8 @@ export const useAppStore = create<AppState & AppActions>()(
mcpServers: state.mcpServers,
mcpAutoApproveTools: state.mcpAutoApproveTools,
mcpUnrestrictedTools: state.mcpUnrestrictedTools,
// Prompt customization
promptCustomization: state.promptCustomization,
// Profiles and sessions
aiProfiles: state.aiProfiles,
chatSessions: state.chatSessions,