diff --git a/apps/server/src/routes/features/routes/update.ts b/apps/server/src/routes/features/routes/update.ts index 9cb8f25e..1a89cda3 100644 --- a/apps/server/src/routes/features/routes/update.ts +++ b/apps/server/src/routes/features/routes/update.ts @@ -10,14 +10,21 @@ import { getErrorMessage, logError } from '../common.js'; export function createUpdateHandler(featureLoader: FeatureLoader) { return async (req: Request, res: Response): Promise => { try { - const { projectPath, featureId, updates, descriptionHistorySource, enhancementMode } = - req.body as { - projectPath: string; - featureId: string; - updates: Partial; - descriptionHistorySource?: 'enhance' | 'edit'; - enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer'; - }; + const { + projectPath, + featureId, + updates, + descriptionHistorySource, + enhancementMode, + preEnhancementDescription, + } = req.body as { + projectPath: string; + featureId: string; + updates: Partial; + descriptionHistorySource?: 'enhance' | 'edit'; + enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer'; + preEnhancementDescription?: string; + }; if (!projectPath || !featureId || !updates) { res.status(400).json({ @@ -32,7 +39,8 @@ export function createUpdateHandler(featureLoader: FeatureLoader) { featureId, updates, descriptionHistorySource, - enhancementMode + enhancementMode, + preEnhancementDescription ); res.json({ success: true, feature: updated }); } catch (error) { diff --git a/apps/server/src/services/feature-loader.ts b/apps/server/src/services/feature-loader.ts index 62570b6b..409abd2a 100644 --- a/apps/server/src/services/feature-loader.ts +++ b/apps/server/src/services/feature-loader.ts @@ -308,13 +308,15 @@ export class FeatureLoader { * @param updates - Partial feature updates * @param descriptionHistorySource - Source of description change ('enhance' or 'edit') * @param enhancementMode - Enhancement mode if source is 'enhance' + * @param preEnhancementDescription - Description before enhancement (for restoring original) */ async update( projectPath: string, featureId: string, updates: Partial, descriptionHistorySource?: 'enhance' | 'edit', - enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' + enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer', + preEnhancementDescription?: string ): Promise { const feature = await this.get(projectPath, featureId); if (!feature) { @@ -338,9 +340,31 @@ export class FeatureLoader { updates.description !== feature.description && updates.description.trim() ) { + const timestamp = new Date().toISOString(); + + // If this is an enhancement and we have the pre-enhancement description, + // add the original text to history first (so user can restore to it) + if ( + descriptionHistorySource === 'enhance' && + preEnhancementDescription && + preEnhancementDescription.trim() + ) { + // Check if this pre-enhancement text is different from the last history entry + const lastEntry = updatedHistory[updatedHistory.length - 1]; + if (!lastEntry || lastEntry.description !== preEnhancementDescription) { + const preEnhanceEntry: DescriptionHistoryEntry = { + description: preEnhancementDescription, + timestamp, + source: updatedHistory.length === 0 ? 'initial' : 'edit', + }; + updatedHistory = [...updatedHistory, preEnhanceEntry]; + } + } + + // Add the new/enhanced description to history const historyEntry: DescriptionHistoryEntry = { description: updates.description, - timestamp: new Date().toISOString(), + timestamp, source: descriptionHistorySource || 'edit', ...(descriptionHistorySource === 'enhance' && enhancementMode ? { enhancementMode } : {}), }; diff --git a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx index 126fe688..797b64b9 100644 --- a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx @@ -396,16 +396,29 @@ export function AddFeatureDialog({ { - setDescriptionHistory((prev) => [ - ...prev, - { + onHistoryAdd={({ mode, originalText, enhancedText }) => { + const timestamp = new Date().toISOString(); + setDescriptionHistory((prev) => { + const newHistory = [...prev]; + // Add original text first (so user can restore to pre-enhancement state) + // Only add if it's different from the last entry to avoid duplicates + const lastEntry = prev[prev.length - 1]; + if (!lastEntry || lastEntry.description !== originalText) { + newHistory.push({ + description: originalText, + timestamp, + source: prev.length === 0 ? 'initial' : 'edit', + }); + } + // Add enhanced text + newHistory.push({ description: enhancedText, - timestamp: new Date().toISOString(), + timestamp, source: 'enhance', enhancementMode: mode, - }, - ]); + }); + return newHistory; + }); }} /> diff --git a/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx index 92d633dc..9912201d 100644 --- a/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx @@ -64,7 +64,8 @@ interface EditFeatureDialogProps { requirePlanApproval: boolean; }, descriptionHistorySource?: 'enhance' | 'edit', - enhancementMode?: EnhancementMode + enhancementMode?: EnhancementMode, + preEnhancementDescription?: string ) => void; categorySuggestions: string[]; branchSuggestions: string[]; @@ -117,6 +118,12 @@ export function EditFeatureDialog({ >(null); // Track the original description when the dialog opened for comparison const [originalDescription, setOriginalDescription] = useState(feature?.description ?? ''); + // Track the description before enhancement (so it can be restored) + const [preEnhancementDescription, setPreEnhancementDescription] = useState(null); + // Local history state for real-time display (combines persisted + session history) + const [localHistory, setLocalHistory] = useState( + feature?.descriptionHistory ?? [] + ); useEffect(() => { setEditingFeature(feature); @@ -128,6 +135,8 @@ export function EditFeatureDialog({ // Reset history tracking state setOriginalDescription(feature.description ?? ''); setDescriptionChangeSource(null); + setPreEnhancementDescription(null); + setLocalHistory(feature.descriptionHistory ?? []); // Reset model entry setModelEntry({ model: (feature.model as ModelAlias) || 'opus', @@ -137,6 +146,8 @@ export function EditFeatureDialog({ } else { setEditFeaturePreviewMap(new Map()); setDescriptionChangeSource(null); + setPreEnhancementDescription(null); + setLocalHistory([]); } }, [feature]); @@ -198,7 +209,13 @@ export function EditFeatureDialog({ } } - onUpdate(editingFeature.id, updates, historySource, historyEnhancementMode); + onUpdate( + editingFeature.id, + updates, + historySource, + historyEnhancementMode, + preEnhancementDescription ?? undefined + ); setEditFeaturePreviewMap(new Map()); onClose(); }; @@ -246,20 +263,18 @@ export function EditFeatureDialog({
- {/* Version History Button */} - {feature?.descriptionHistory && feature.descriptionHistory.length > 0 && ( - { - setEditingFeature((prev) => (prev ? { ...prev, description } : prev)); - setDescriptionChangeSource('edit'); - }} - valueAccessor={(entry) => entry.description} - title="Version History" - restoreMessage="Description restored from history" - /> - )} + {/* Version History Button - uses local history for real-time updates */} + { + setEditingFeature((prev) => (prev ? { ...prev, description } : prev)); + setDescriptionChangeSource('edit'); + }} + valueAccessor={(entry) => entry.description} + title="Version History" + restoreMessage="Description restored from history" + />
setEditingFeature((prev) => (prev ? { ...prev, description: enhanced } : prev)) } - onHistoryAdd={({ mode }) => setDescriptionChangeSource({ source: 'enhance', mode })} + onHistoryAdd={({ mode, originalText, enhancedText }) => { + setDescriptionChangeSource({ source: 'enhance', mode }); + setPreEnhancementDescription(originalText); + + // Update local history for real-time display + const timestamp = new Date().toISOString(); + setLocalHistory((prev) => { + const newHistory = [...prev]; + // Add original text first (so user can restore to pre-enhancement state) + const lastEntry = prev[prev.length - 1]; + if (!lastEntry || lastEntry.description !== originalText) { + newHistory.push({ + description: originalText, + timestamp, + source: prev.length === 0 ? 'initial' : 'edit', + }); + } + // Add enhanced text + newHistory.push({ + description: enhancedText, + timestamp, + source: 'enhance', + enhancementMode: mode, + }); + return newHistory; + }); + }} />
diff --git a/apps/ui/src/components/views/board-view/dialogs/follow-up-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/follow-up-dialog.tsx index 4457b5d0..6df1eea0 100644 --- a/apps/ui/src/components/views/board-view/dialogs/follow-up-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/follow-up-dialog.tsx @@ -126,10 +126,22 @@ export function FollowUpDialog({ { + onHistoryAdd={({ mode, originalText, enhancedText }) => { + const timestamp = new Date().toISOString(); + // Add original text first (so user can restore to pre-enhancement state) + // Only add if it's different from the last history entry + const lastEntry = promptHistory[promptHistory.length - 1]; + if (!lastEntry || lastEntry.prompt !== originalText) { + onHistoryAdd?.({ + prompt: originalText, + timestamp, + source: promptHistory.length === 0 ? 'initial' : 'edit', + }); + } + // Add enhanced text onHistoryAdd?.({ prompt: enhancedText, - timestamp: new Date().toISOString(), + timestamp, source: 'enhance', enhancementMode: mode, }); diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts b/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts index 8efacd8b..e25d963e 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts @@ -30,7 +30,8 @@ interface UseBoardActionsProps { featureId: string, updates: Partial, descriptionHistorySource?: 'enhance' | 'edit', - enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' + enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer', + preEnhancementDescription?: string ) => Promise; persistFeatureDelete: (featureId: string) => Promise; saveCategory: (category: string) => Promise; @@ -251,7 +252,8 @@ export function useBoardActions({ workMode?: 'current' | 'auto' | 'custom'; }, descriptionHistorySource?: 'enhance' | 'edit', - enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' + enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer', + preEnhancementDescription?: string ) => { const workMode = updates.workMode || 'current'; @@ -308,7 +310,13 @@ export function useBoardActions({ }; updateFeature(featureId, finalUpdates); - persistFeatureUpdate(featureId, finalUpdates, descriptionHistorySource, enhancementMode); + persistFeatureUpdate( + featureId, + finalUpdates, + descriptionHistorySource, + enhancementMode, + preEnhancementDescription + ); if (updates.category) { saveCategory(updates.category); } diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts b/apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts index df281a56..3c860251 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts @@ -19,7 +19,8 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps featureId: string, updates: Partial, descriptionHistorySource?: 'enhance' | 'edit', - enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' + enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer', + preEnhancementDescription?: string ) => { if (!currentProject) return; @@ -35,7 +36,8 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps featureId, updates, descriptionHistorySource, - enhancementMode + enhancementMode, + preEnhancementDescription ); if (result.success && result.feature) { updateFeature(result.feature.id, result.feature); diff --git a/apps/ui/src/components/views/board-view/shared/enhancement/enhance-with-ai.tsx b/apps/ui/src/components/views/board-view/shared/enhancement/enhance-with-ai.tsx index 530b43cd..63b9dedc 100644 --- a/apps/ui/src/components/views/board-view/shared/enhancement/enhance-with-ai.tsx +++ b/apps/ui/src/components/views/board-view/shared/enhancement/enhance-with-ai.tsx @@ -22,7 +22,11 @@ interface EnhanceWithAIProps { /** Callback when text is enhanced */ onChange: (enhancedText: string) => void; /** Optional callback to track enhancement in history */ - onHistoryAdd?: (entry: { mode: EnhancementMode; enhancedText: string }) => void; + onHistoryAdd?: (entry: { + mode: EnhancementMode; + originalText: string; + enhancedText: string; + }) => void; /** Disable the enhancement feature */ disabled?: boolean; /** Additional CSS classes */ @@ -69,11 +73,12 @@ export function EnhanceWithAI({ ); if (result?.success && result.enhancedText) { + const originalText = value; const enhancedText = result.enhancedText; onChange(enhancedText); - // Track in history if callback provided - onHistoryAdd?.({ mode: enhancementMode, enhancedText }); + // Track in history if callback provided (includes original for restoration) + onHistoryAdd?.({ mode: enhancementMode, originalText, enhancedText }); toast.success('Enhanced successfully!'); } else { diff --git a/apps/ui/src/components/views/board-view/shared/enhancement/enhancement-history-button.tsx b/apps/ui/src/components/views/board-view/shared/enhancement/enhancement-history-button.tsx index 77dac898..bf31fde7 100644 --- a/apps/ui/src/components/views/board-view/shared/enhancement/enhancement-history-button.tsx +++ b/apps/ui/src/components/views/board-view/shared/enhancement/enhancement-history-button.tsx @@ -45,6 +45,11 @@ export function EnhancementHistoryButton({ }: EnhancementHistoryButtonProps) { const [showHistory, setShowHistory] = useState(false); + // Memoize reversed history to avoid creating new array on every render + // NOTE: This hook MUST be called before any early returns to follow Rules of Hooks + const reversedHistory = useMemo(() => [...history].reverse(), [history]); + + // Early return AFTER all hooks are called if (history.length === 0) { return null; } @@ -71,9 +76,6 @@ export function EnhancementHistoryButton({ }); }; - // Memoize reversed history to avoid creating new array on every render - const reversedHistory = useMemo(() => [...history].reverse(), [history]); - return ( diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index e06ed639..3abe43fa 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -462,7 +462,8 @@ export interface FeaturesAPI { featureId: string, updates: Partial, descriptionHistorySource?: 'enhance' | 'edit', - enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' + enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer', + preEnhancementDescription?: string ) => Promise<{ success: boolean; feature?: Feature; error?: string }>; delete: (projectPath: string, featureId: string) => Promise<{ success: boolean; error?: string }>; getAgentOutput: ( diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index 7d442836..c64c427e 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -1459,7 +1459,8 @@ export class HttpApiClient implements ElectronAPI { featureId: string, updates: Partial, descriptionHistorySource?: 'enhance' | 'edit', - enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' + enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer', + preEnhancementDescription?: string ) => this.post('/api/features/update', { projectPath, @@ -1467,6 +1468,7 @@ export class HttpApiClient implements ElectronAPI { updates, descriptionHistorySource, enhancementMode, + preEnhancementDescription, }), delete: (projectPath: string, featureId: string) => this.post('/api/features/delete', { projectPath, featureId }),