Merge remote-tracking branch 'upstream/v0.10.0rc' into feature/codex-cli

This commit is contained in:
DhanushSantosh
2026-01-11 21:59:04 +05:30
17 changed files with 589 additions and 361 deletions

View File

@@ -10,13 +10,20 @@ import { getErrorMessage, logError } from '../common.js';
export function createUpdateHandler(featureLoader: FeatureLoader) { export function createUpdateHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const { projectPath, featureId, updates, descriptionHistorySource, enhancementMode } = const {
req.body as { projectPath,
featureId,
updates,
descriptionHistorySource,
enhancementMode,
preEnhancementDescription,
} = req.body as {
projectPath: string; projectPath: string;
featureId: string; featureId: string;
updates: Partial<Feature>; updates: Partial<Feature>;
descriptionHistorySource?: 'enhance' | 'edit'; descriptionHistorySource?: 'enhance' | 'edit';
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer'; enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer';
preEnhancementDescription?: string;
}; };
if (!projectPath || !featureId || !updates) { if (!projectPath || !featureId || !updates) {
@@ -32,7 +39,8 @@ export function createUpdateHandler(featureLoader: FeatureLoader) {
featureId, featureId,
updates, updates,
descriptionHistorySource, descriptionHistorySource,
enhancementMode enhancementMode,
preEnhancementDescription
); );
res.json({ success: true, feature: updated }); res.json({ success: true, feature: updated });
} catch (error) { } catch (error) {

View File

@@ -308,13 +308,15 @@ export class FeatureLoader {
* @param updates - Partial feature updates * @param updates - Partial feature updates
* @param descriptionHistorySource - Source of description change ('enhance' or 'edit') * @param descriptionHistorySource - Source of description change ('enhance' or 'edit')
* @param enhancementMode - Enhancement mode if source is 'enhance' * @param enhancementMode - Enhancement mode if source is 'enhance'
* @param preEnhancementDescription - Description before enhancement (for restoring original)
*/ */
async update( async update(
projectPath: string, projectPath: string,
featureId: string, featureId: string,
updates: Partial<Feature>, updates: Partial<Feature>,
descriptionHistorySource?: 'enhance' | 'edit', descriptionHistorySource?: 'enhance' | 'edit',
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer',
preEnhancementDescription?: string
): Promise<Feature> { ): Promise<Feature> {
const feature = await this.get(projectPath, featureId); const feature = await this.get(projectPath, featureId);
if (!feature) { if (!feature) {
@@ -338,9 +340,31 @@ export class FeatureLoader {
updates.description !== feature.description && updates.description !== feature.description &&
updates.description.trim() 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 = { const historyEntry: DescriptionHistoryEntry = {
description: updates.description, description: updates.description,
timestamp: new Date().toISOString(), timestamp,
source: descriptionHistorySource || 'edit', source: descriptionHistorySource || 'edit',
...(descriptionHistorySource === 'enhance' && enhancementMode ? { enhancementMode } : {}), ...(descriptionHistorySource === 'enhance' && enhancementMode ? { enhancementMode } : {}),
}; };

View File

@@ -161,12 +161,14 @@ export function BoardView() {
followUpPrompt, followUpPrompt,
followUpImagePaths, followUpImagePaths,
followUpPreviewMap, followUpPreviewMap,
followUpPromptHistory,
setShowFollowUpDialog, setShowFollowUpDialog,
setFollowUpFeature, setFollowUpFeature,
setFollowUpPrompt, setFollowUpPrompt,
setFollowUpImagePaths, setFollowUpImagePaths,
setFollowUpPreviewMap, setFollowUpPreviewMap,
handleFollowUpDialogChange, handleFollowUpDialogChange,
addToPromptHistory,
} = useFollowUpState(); } = useFollowUpState();
// Selection mode hook for mass editing // Selection mode hook for mass editing
@@ -1422,6 +1424,8 @@ export function BoardView() {
onPreviewMapChange={setFollowUpPreviewMap} onPreviewMapChange={setFollowUpPreviewMap}
onSend={handleSendFollowUp} onSend={handleSendFollowUp}
isMaximized={isMaximized} isMaximized={isMaximized}
promptHistory={followUpPromptHistory}
onHistoryAdd={addToPromptHistory}
/> />
{/* Backlog Plan Dialog */} {/* Backlog Plan Dialog */}

View File

@@ -21,11 +21,9 @@ import {
FeatureTextFilePath as DescriptionTextFilePath, FeatureTextFilePath as DescriptionTextFilePath,
ImagePreviewMap, ImagePreviewMap,
} from '@/components/ui/description-image-dropzone'; } from '@/components/ui/description-image-dropzone';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { Play, Cpu, FolderKanban } from 'lucide-react';
import { Sparkles, ChevronDown, ChevronRight, Play, Cpu, FolderKanban } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { getElectronAPI } from '@/lib/electron';
import { modelSupportsThinking } from '@/lib/utils'; import { modelSupportsThinking } from '@/lib/utils';
import { import {
useAppStore, useAppStore,
@@ -43,16 +41,12 @@ import {
WorkModeSelector, WorkModeSelector,
PlanningModeSelect, PlanningModeSelect,
AncestorContextSection, AncestorContextSection,
EnhanceWithAI,
EnhancementHistoryButton,
type BaseHistoryEntry,
} from '../shared'; } from '../shared';
import type { WorkMode } from '../shared'; import type { WorkMode } from '../shared';
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector'; import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { import {
getAncestors, getAncestors,
@@ -97,6 +91,13 @@ interface AddFeatureDialogProps {
allFeatures?: Feature[]; allFeatures?: Feature[];
} }
/**
* A single entry in the description history
*/
interface DescriptionHistoryEntry extends BaseHistoryEntry {
description: string;
}
export function AddFeatureDialog({ export function AddFeatureDialog({
open, open,
onOpenChange, onOpenChange,
@@ -139,11 +140,9 @@ export function AddFeatureDialog({
// UI state // UI state
const [previewMap, setPreviewMap] = useState<ImagePreviewMap>(() => new Map()); const [previewMap, setPreviewMap] = useState<ImagePreviewMap>(() => new Map());
const [descriptionError, setDescriptionError] = useState(false); const [descriptionError, setDescriptionError] = useState(false);
const [isEnhancing, setIsEnhancing] = useState(false);
const [enhancementMode, setEnhancementMode] = useState< // Description history state
'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' const [descriptionHistory, setDescriptionHistory] = useState<DescriptionHistoryEntry[]>([]);
>('improve');
const [enhanceOpen, setEnhanceOpen] = useState(false);
// Spawn mode state // Spawn mode state
const [ancestors, setAncestors] = useState<AncestorContext[]>([]); const [ancestors, setAncestors] = useState<AncestorContext[]>([]);
@@ -152,9 +151,6 @@ export function AddFeatureDialog({
// Get defaults from store // Get defaults from store
const { defaultPlanningMode, defaultRequirePlanApproval } = useAppStore(); const { defaultPlanningMode, defaultRequirePlanApproval } = useAppStore();
// Enhancement model override
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
// Track previous open state to detect when dialog opens // Track previous open state to detect when dialog opens
const wasOpenRef = useRef(false); const wasOpenRef = useRef(false);
@@ -171,6 +167,9 @@ export function AddFeatureDialog({
setRequirePlanApproval(defaultRequirePlanApproval); setRequirePlanApproval(defaultRequirePlanApproval);
setModelEntry({ model: 'opus' }); setModelEntry({ model: 'opus' });
// Initialize description history (empty for new feature)
setDescriptionHistory([]);
// Initialize ancestors for spawn mode // Initialize ancestors for spawn mode
if (parentFeature) { if (parentFeature) {
const ancestorList = getAncestors(parentFeature, allFeatures); const ancestorList = getAncestors(parentFeature, allFeatures);
@@ -279,7 +278,7 @@ export function AddFeatureDialog({
setRequirePlanApproval(defaultRequirePlanApproval); setRequirePlanApproval(defaultRequirePlanApproval);
setPreviewMap(new Map()); setPreviewMap(new Map());
setDescriptionError(false); setDescriptionError(false);
setEnhanceOpen(false); setDescriptionHistory([]);
onOpenChange(false); onOpenChange(false);
}; };
@@ -302,33 +301,6 @@ export function AddFeatureDialog({
} }
}; };
const handleEnhanceDescription = async () => {
if (!description.trim() || isEnhancing) return;
setIsEnhancing(true);
try {
const api = getElectronAPI();
const result = await api.enhancePrompt?.enhance(
description,
enhancementMode,
enhancementOverride.effectiveModel,
enhancementOverride.effectiveModelEntry.thinkingLevel
);
if (result?.success && result.enhancedText) {
setDescription(result.enhancedText);
toast.success('Description enhanced!');
} else {
toast.error(result?.error || 'Failed to enhance description');
}
} catch (error) {
logger.error('Enhancement failed:', error);
toast.error('Failed to enhance description');
} finally {
setIsEnhancing(false);
}
};
// Shared card styling // Shared card styling
const cardClass = 'rounded-lg border border-border/50 bg-muted/30 p-4 space-y-3'; const cardClass = 'rounded-lg border border-border/50 bg-muted/30 p-4 space-y-3';
const sectionHeaderClass = 'flex items-center gap-2 text-sm font-medium text-foreground'; const sectionHeaderClass = 'flex items-center gap-2 text-sm font-medium text-foreground';
@@ -380,7 +352,18 @@ export function AddFeatureDialog({
{/* Task Details Section */} {/* Task Details Section */}
<div className={cardClass}> <div className={cardClass}>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="description">Description</Label> <Label htmlFor="description">Description</Label>
{/* Version History Button */}
<EnhancementHistoryButton
history={descriptionHistory}
currentValue={description}
onRestore={setDescription}
valueAccessor={(entry) => entry.description}
title="Version History"
restoreMessage="Description restored from history"
/>
</div>
<DescriptionImageDropZone <DescriptionImageDropZone
value={description} value={description}
onChange={(value) => { onChange={(value) => {
@@ -409,76 +392,36 @@ export function AddFeatureDialog({
/> />
</div> </div>
{/* Collapsible Enhancement Section */} {/* Enhancement Section */}
<Collapsible open={enhanceOpen} onOpenChange={setEnhanceOpen}> <EnhanceWithAI
<CollapsibleTrigger asChild> value={description}
<button className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors w-full py-1"> onChange={setDescription}
{enhanceOpen ? ( onHistoryAdd={({ mode, originalText, enhancedText }) => {
<ChevronDown className="w-4 h-4" /> const timestamp = new Date().toISOString();
) : ( setDescriptionHistory((prev) => {
<ChevronRight className="w-4 h-4" /> const newHistory = [...prev];
)} // Add original text first (so user can restore to pre-enhancement state)
<Sparkles className="w-4 h-4" /> // Only add if it's different from the last entry to avoid duplicates
<span>Enhance with AI</span> const lastEntry = prev[prev.length - 1];
</button> if (!lastEntry || lastEntry.description !== originalText) {
</CollapsibleTrigger> newHistory.push({
<CollapsibleContent className="pt-3"> description: originalText,
<div className="flex flex-wrap items-center gap-2 pl-6"> timestamp,
<DropdownMenu> source: prev.length === 0 ? 'initial' : 'edit',
<DropdownMenuTrigger asChild> });
<Button variant="outline" size="sm" className="h-8 text-xs"> }
{enhancementMode === 'improve' && 'Improve Clarity'} // Add enhanced text
{enhancementMode === 'technical' && 'Add Technical Details'} newHistory.push({
{enhancementMode === 'simplify' && 'Simplify'} description: enhancedText,
{enhancementMode === 'acceptance' && 'Add Acceptance Criteria'} timestamp,
{enhancementMode === 'ux-reviewer' && 'User Experience'} source: 'enhance',
<ChevronDown className="w-3 h-3 ml-1" /> enhancementMode: mode,
</Button> });
</DropdownMenuTrigger> return newHistory;
<DropdownMenuContent align="start"> });
<DropdownMenuItem onClick={() => setEnhancementMode('improve')}> }}
Improve Clarity
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('technical')}>
Add Technical Details
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('simplify')}>
Simplify
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('acceptance')}>
Add Acceptance Criteria
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('ux-reviewer')}>
User Experience
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
type="button"
variant="default"
size="sm"
className="h-8 text-xs"
onClick={handleEnhanceDescription}
disabled={!description.trim() || isEnhancing}
loading={isEnhancing}
>
<Sparkles className="w-3 h-3 mr-1" />
Enhance
</Button>
<ModelOverrideTrigger
currentModelEntry={enhancementOverride.effectiveModelEntry}
onModelChange={enhancementOverride.setOverride}
phase="enhancementModel"
isOverridden={enhancementOverride.isOverridden}
size="sm"
variant="icon"
/> />
</div> </div>
</CollapsibleContent>
</Collapsible>
</div>
{/* AI & Execution Section */} {/* AI & Execution Section */}
<div className={cardClass}> <div className={cardClass}>

View File

@@ -21,18 +21,8 @@ import {
FeatureTextFilePath as DescriptionTextFilePath, FeatureTextFilePath as DescriptionTextFilePath,
ImagePreviewMap, ImagePreviewMap,
} from '@/components/ui/description-image-dropzone'; } from '@/components/ui/description-image-dropzone';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { GitBranch, Cpu, FolderKanban } from 'lucide-react';
import {
Sparkles,
ChevronDown,
ChevronRight,
GitBranch,
History,
Cpu,
FolderKanban,
} from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { getElectronAPI } from '@/lib/electron';
import { cn, modelSupportsThinking } from '@/lib/utils'; import { cn, modelSupportsThinking } from '@/lib/utils';
import { Feature, ModelAlias, ThinkingLevel, useAppStore, PlanningMode } from '@/store/app-store'; import { Feature, ModelAlias, ThinkingLevel, useAppStore, PlanningMode } from '@/store/app-store';
import type { ReasoningEffort, PhaseModelEntry, DescriptionHistoryEntry } from '@automaker/types'; import type { ReasoningEffort, PhaseModelEntry, DescriptionHistoryEntry } from '@automaker/types';
@@ -41,17 +31,12 @@ import {
PrioritySelector, PrioritySelector,
WorkModeSelector, WorkModeSelector,
PlanningModeSelect, PlanningModeSelect,
EnhanceWithAI,
EnhancementHistoryButton,
type EnhancementMode,
} from '../shared'; } from '../shared';
import type { WorkMode } from '../shared'; import type { WorkMode } from '../shared';
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector'; import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { DependencyTreeDialog } from './dependency-tree-dialog'; import { DependencyTreeDialog } from './dependency-tree-dialog';
import { isClaudeModel, supportsReasoningEffort } from '@automaker/types'; import { isClaudeModel, supportsReasoningEffort } from '@automaker/types';
@@ -79,7 +64,8 @@ interface EditFeatureDialogProps {
requirePlanApproval: boolean; requirePlanApproval: boolean;
}, },
descriptionHistorySource?: 'enhance' | 'edit', descriptionHistorySource?: 'enhance' | 'edit',
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' enhancementMode?: EnhancementMode,
preEnhancementDescription?: string
) => void; ) => void;
categorySuggestions: string[]; categorySuggestions: string[];
branchSuggestions: string[]; branchSuggestions: string[];
@@ -110,11 +96,6 @@ export function EditFeatureDialog({
const [editFeaturePreviewMap, setEditFeaturePreviewMap] = useState<ImagePreviewMap>( const [editFeaturePreviewMap, setEditFeaturePreviewMap] = useState<ImagePreviewMap>(
() => new Map() () => new Map()
); );
const [isEnhancing, setIsEnhancing] = useState(false);
const [enhancementMode, setEnhancementMode] = useState<
'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer'
>('improve');
const [enhanceOpen, setEnhanceOpen] = useState(false);
const [showDependencyTree, setShowDependencyTree] = useState(false); const [showDependencyTree, setShowDependencyTree] = useState(false);
const [planningMode, setPlanningMode] = useState<PlanningMode>(feature?.planningMode ?? 'skip'); const [planningMode, setPlanningMode] = useState<PlanningMode>(feature?.planningMode ?? 'skip');
const [requirePlanApproval, setRequirePlanApproval] = useState( const [requirePlanApproval, setRequirePlanApproval] = useState(
@@ -133,15 +114,16 @@ export function EditFeatureDialog({
// Track the source of description changes for history // Track the source of description changes for history
const [descriptionChangeSource, setDescriptionChangeSource] = useState< const [descriptionChangeSource, setDescriptionChangeSource] = useState<
{ source: 'enhance'; mode: 'improve' | 'technical' | 'simplify' | 'acceptance' } | 'edit' | null { source: 'enhance'; mode: EnhancementMode } | 'edit' | null
>(null); >(null);
// Track the original description when the dialog opened for comparison // Track the original description when the dialog opened for comparison
const [originalDescription, setOriginalDescription] = useState(feature?.description ?? ''); const [originalDescription, setOriginalDescription] = useState(feature?.description ?? '');
// Track if history dropdown is open // Track the description before enhancement (so it can be restored)
const [showHistory, setShowHistory] = useState(false); const [preEnhancementDescription, setPreEnhancementDescription] = useState<string | null>(null);
// Local history state for real-time display (combines persisted + session history)
// Enhancement model override const [localHistory, setLocalHistory] = useState<DescriptionHistoryEntry[]>(
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' }); feature?.descriptionHistory ?? []
);
useEffect(() => { useEffect(() => {
setEditingFeature(feature); setEditingFeature(feature);
@@ -153,8 +135,8 @@ export function EditFeatureDialog({
// Reset history tracking state // Reset history tracking state
setOriginalDescription(feature.description ?? ''); setOriginalDescription(feature.description ?? '');
setDescriptionChangeSource(null); setDescriptionChangeSource(null);
setShowHistory(false); setPreEnhancementDescription(null);
setEnhanceOpen(false); setLocalHistory(feature.descriptionHistory ?? []);
// Reset model entry // Reset model entry
setModelEntry({ setModelEntry({
model: (feature.model as ModelAlias) || 'opus', model: (feature.model as ModelAlias) || 'opus',
@@ -164,7 +146,8 @@ export function EditFeatureDialog({
} else { } else {
setEditFeaturePreviewMap(new Map()); setEditFeaturePreviewMap(new Map());
setDescriptionChangeSource(null); setDescriptionChangeSource(null);
setShowHistory(false); setPreEnhancementDescription(null);
setLocalHistory([]);
} }
}, [feature]); }, [feature]);
@@ -226,7 +209,13 @@ export function EditFeatureDialog({
} }
} }
onUpdate(editingFeature.id, updates, historySource, historyEnhancementMode); onUpdate(
editingFeature.id,
updates,
historySource,
historyEnhancementMode,
preEnhancementDescription ?? undefined
);
setEditFeaturePreviewMap(new Map()); setEditFeaturePreviewMap(new Map());
onClose(); onClose();
}; };
@@ -237,36 +226,6 @@ export function EditFeatureDialog({
} }
}; };
const handleEnhanceDescription = async () => {
if (!editingFeature?.description.trim() || isEnhancing) return;
setIsEnhancing(true);
try {
const api = getElectronAPI();
const result = await api.enhancePrompt?.enhance(
editingFeature.description,
enhancementMode,
enhancementOverride.effectiveModel, // API accepts string, extract from PhaseModelEntry
enhancementOverride.effectiveModelEntry.thinkingLevel // Pass thinking level
);
if (result?.success && result.enhancedText) {
const enhancedText = result.enhancedText;
setEditingFeature((prev) => (prev ? { ...prev, description: enhancedText } : prev));
// Track that this change was from enhancement
setDescriptionChangeSource({ source: 'enhance', mode: enhancementMode });
toast.success('Description enhanced!');
} else {
toast.error(result?.error || 'Failed to enhance description');
}
} catch (error) {
logger.error('Enhancement failed:', error);
toast.error('Failed to enhance description');
} finally {
setIsEnhancing(false);
}
};
if (!editingFeature) { if (!editingFeature) {
return null; return null;
} }
@@ -304,95 +263,18 @@ export function EditFeatureDialog({
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label htmlFor="edit-description">Description</Label> <Label htmlFor="edit-description">Description</Label>
{/* Version History Button */} {/* Version History Button - uses local history for real-time updates */}
{feature?.descriptionHistory && feature.descriptionHistory.length > 0 && ( <EnhancementHistoryButton
<Popover open={showHistory} onOpenChange={setShowHistory}> history={localHistory}
<PopoverTrigger asChild> currentValue={editingFeature.description}
<Button onRestore={(description) => {
type="button" setEditingFeature((prev) => (prev ? { ...prev, description } : prev));
variant="ghost"
size="sm"
className="h-7 gap-1.5 text-xs text-muted-foreground"
>
<History className="w-3.5 h-3.5" />
History ({feature.descriptionHistory.length})
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end">
<div className="p-3 border-b">
<h4 className="font-medium text-sm">Version History</h4>
<p className="text-xs text-muted-foreground mt-1">
Click a version to restore it
</p>
</div>
<div className="max-h-64 overflow-y-auto p-2 space-y-1">
{[...(feature.descriptionHistory || [])]
.reverse()
.map((entry: DescriptionHistoryEntry, index: number) => {
const isCurrentVersion =
entry.description === editingFeature.description;
const date = new Date(entry.timestamp);
const formattedDate = date.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
const getEnhancementModeLabel = (mode?: string) => {
const labels: Record<string, string> = {
improve: 'Improve Clarity',
technical: 'Add Technical Details',
simplify: 'Simplify',
acceptance: 'Add Acceptance Criteria',
'ux-reviewer': 'User Experience',
};
return labels[mode || 'improve'] || mode || 'improve';
};
const sourceLabel =
entry.source === 'initial'
? 'Original'
: entry.source === 'enhance'
? `Enhanced (${getEnhancementModeLabel(entry.enhancementMode)})`
: 'Edited';
return (
<button
key={`${entry.timestamp}-${index}`}
onClick={() => {
setEditingFeature((prev) =>
prev ? { ...prev, description: entry.description } : prev
);
// Mark as edit since user is restoring from history
setDescriptionChangeSource('edit'); setDescriptionChangeSource('edit');
setShowHistory(false);
toast.success('Description restored from history');
}} }}
className={`w-full text-left p-2 rounded-md hover:bg-muted transition-colors ${ valueAccessor={(entry) => entry.description}
isCurrentVersion ? 'bg-muted/50 border border-primary/20' : '' title="Version History"
}`} restoreMessage="Description restored from history"
> />
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-medium">{sourceLabel}</span>
<span className="text-xs text-muted-foreground">
{formattedDate}
</span>
</div>
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
{entry.description.slice(0, 100)}
{entry.description.length > 100 ? '...' : ''}
</p>
{isCurrentVersion && (
<span className="text-xs text-primary font-medium mt-1 block">
Current version
</span>
)}
</button>
);
})}
</div>
</PopoverContent>
</Popover>
)}
</div> </div>
<DescriptionImageDropZone <DescriptionImageDropZone
value={editingFeature.description} value={editingFeature.description}
@@ -443,76 +325,41 @@ export function EditFeatureDialog({
/> />
</div> </div>
{/* Collapsible Enhancement Section */} {/* Enhancement Section */}
<Collapsible open={enhanceOpen} onOpenChange={setEnhanceOpen}> <EnhanceWithAI
<CollapsibleTrigger asChild> value={editingFeature.description}
<button className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors w-full py-1"> onChange={(enhanced) =>
{enhanceOpen ? ( setEditingFeature((prev) => (prev ? { ...prev, description: enhanced } : prev))
<ChevronDown className="w-4 h-4" /> }
) : ( onHistoryAdd={({ mode, originalText, enhancedText }) => {
<ChevronRight className="w-4 h-4" /> setDescriptionChangeSource({ source: 'enhance', mode });
)} setPreEnhancementDescription(originalText);
<Sparkles className="w-4 h-4" />
<span>Enhance with AI</span>
</button>
</CollapsibleTrigger>
<CollapsibleContent className="pt-3">
<div className="flex flex-wrap items-center gap-2 pl-6">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-8 text-xs">
{enhancementMode === 'improve' && 'Improve Clarity'}
{enhancementMode === 'technical' && 'Add Technical Details'}
{enhancementMode === 'simplify' && 'Simplify'}
{enhancementMode === 'acceptance' && 'Add Acceptance Criteria'}
{enhancementMode === 'ux-reviewer' && 'User Experience'}
<ChevronDown className="w-3 h-3 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => setEnhancementMode('improve')}>
Improve Clarity
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('technical')}>
Add Technical Details
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('simplify')}>
Simplify
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('acceptance')}>
Add Acceptance Criteria
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEnhancementMode('ux-reviewer')}>
User Experience
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button // Update local history for real-time display
type="button" const timestamp = new Date().toISOString();
variant="default" setLocalHistory((prev) => {
size="sm" const newHistory = [...prev];
className="h-8 text-xs" // Add original text first (so user can restore to pre-enhancement state)
onClick={handleEnhanceDescription} const lastEntry = prev[prev.length - 1];
disabled={!editingFeature.description.trim() || isEnhancing} if (!lastEntry || lastEntry.description !== originalText) {
loading={isEnhancing} newHistory.push({
> description: originalText,
<Sparkles className="w-3 h-3 mr-1" /> timestamp,
Enhance source: prev.length === 0 ? 'initial' : 'edit',
</Button> });
}
<ModelOverrideTrigger // Add enhanced text
currentModelEntry={enhancementOverride.effectiveModelEntry} newHistory.push({
onModelChange={enhancementOverride.setOverride} description: enhancedText,
phase="enhancementModel" timestamp,
isOverridden={enhancementOverride.isOverridden} source: 'enhance',
size="sm" enhancementMode: mode,
variant="icon" });
return newHistory;
});
}}
/> />
</div> </div>
</CollapsibleContent>
</Collapsible>
</div>
{/* AI & Execution Section */} {/* AI & Execution Section */}
<div className={cardClass}> <div className={cardClass}>

View File

@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { createLogger } from '@automaker/utils/logger';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -17,6 +18,21 @@ import {
} from '@/components/ui/description-image-dropzone'; } from '@/components/ui/description-image-dropzone';
import { MessageSquare } from 'lucide-react'; import { MessageSquare } from 'lucide-react';
import { Feature } from '@/store/app-store'; import { Feature } from '@/store/app-store';
import {
EnhanceWithAI,
EnhancementHistoryButton,
type EnhancementMode,
type BaseHistoryEntry,
} from '../shared';
const logger = createLogger('FollowUpDialog');
/**
* A single entry in the follow-up prompt history
*/
export interface FollowUpHistoryEntry extends BaseHistoryEntry {
prompt: string;
}
interface FollowUpDialogProps { interface FollowUpDialogProps {
open: boolean; open: boolean;
@@ -30,6 +46,10 @@ interface FollowUpDialogProps {
onPreviewMapChange: (map: ImagePreviewMap) => void; onPreviewMapChange: (map: ImagePreviewMap) => void;
onSend: () => void; onSend: () => void;
isMaximized: boolean; isMaximized: boolean;
/** History of prompt versions for restoration */
promptHistory?: FollowUpHistoryEntry[];
/** Callback to add a new entry to prompt history */
onHistoryAdd?: (entry: FollowUpHistoryEntry) => void;
} }
export function FollowUpDialog({ export function FollowUpDialog({
@@ -44,9 +64,11 @@ export function FollowUpDialog({
onPreviewMapChange, onPreviewMapChange,
onSend, onSend,
isMaximized, isMaximized,
promptHistory = [],
onHistoryAdd,
}: FollowUpDialogProps) { }: FollowUpDialogProps) {
const handleClose = (open: boolean) => { const handleClose = (openState: boolean) => {
if (!open) { if (!openState) {
onOpenChange(false); onOpenChange(false);
} }
}; };
@@ -77,7 +99,18 @@ export function FollowUpDialog({
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4 overflow-y-auto flex-1 min-h-0"> <div className="space-y-4 py-4 overflow-y-auto flex-1 min-h-0">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="follow-up-prompt">Instructions</Label> <Label htmlFor="follow-up-prompt">Instructions</Label>
{/* Version History Button */}
<EnhancementHistoryButton
history={promptHistory}
currentValue={prompt}
onRestore={onPromptChange}
valueAccessor={(entry) => entry.prompt}
title="Prompt History"
restoreMessage="Prompt restored from history"
/>
</div>
<DescriptionImageDropZone <DescriptionImageDropZone
value={prompt} value={prompt}
onChange={onPromptChange} onChange={onPromptChange}
@@ -88,6 +121,33 @@ export function FollowUpDialog({
onPreviewMapChange={onPreviewMapChange} onPreviewMapChange={onPreviewMapChange}
/> />
</div> </div>
{/* Enhancement Section */}
<EnhanceWithAI
value={prompt}
onChange={onPromptChange}
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,
source: 'enhance',
enhancementMode: mode,
});
}}
/>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
The agent will continue from where it left off, using the existing context. You can The agent will continue from where it left off, using the existing context. You can
attach screenshots to help explain the issue. attach screenshots to help explain the issue.

View File

@@ -5,6 +5,6 @@ export { CompletedFeaturesModal } from './completed-features-modal';
export { ArchiveAllVerifiedDialog } from './archive-all-verified-dialog'; export { ArchiveAllVerifiedDialog } from './archive-all-verified-dialog';
export { DeleteCompletedFeatureDialog } from './delete-completed-feature-dialog'; export { DeleteCompletedFeatureDialog } from './delete-completed-feature-dialog';
export { EditFeatureDialog } from './edit-feature-dialog'; export { EditFeatureDialog } from './edit-feature-dialog';
export { FollowUpDialog } from './follow-up-dialog'; export { FollowUpDialog, type FollowUpHistoryEntry } from './follow-up-dialog';
export { PlanApprovalDialog } from './plan-approval-dialog'; export { PlanApprovalDialog } from './plan-approval-dialog';
export { MassEditDialog } from './mass-edit-dialog'; export { MassEditDialog } from './mass-edit-dialog';

View File

@@ -30,7 +30,8 @@ interface UseBoardActionsProps {
featureId: string, featureId: string,
updates: Partial<Feature>, updates: Partial<Feature>,
descriptionHistorySource?: 'enhance' | 'edit', descriptionHistorySource?: 'enhance' | 'edit',
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer',
preEnhancementDescription?: string
) => Promise<void>; ) => Promise<void>;
persistFeatureDelete: (featureId: string) => Promise<void>; persistFeatureDelete: (featureId: string) => Promise<void>;
saveCategory: (category: string) => Promise<void>; saveCategory: (category: string) => Promise<void>;
@@ -251,7 +252,8 @@ export function useBoardActions({
workMode?: 'current' | 'auto' | 'custom'; workMode?: 'current' | 'auto' | 'custom';
}, },
descriptionHistorySource?: 'enhance' | 'edit', descriptionHistorySource?: 'enhance' | 'edit',
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer',
preEnhancementDescription?: string
) => { ) => {
const workMode = updates.workMode || 'current'; const workMode = updates.workMode || 'current';
@@ -308,7 +310,13 @@ export function useBoardActions({
}; };
updateFeature(featureId, finalUpdates); updateFeature(featureId, finalUpdates);
persistFeatureUpdate(featureId, finalUpdates, descriptionHistorySource, enhancementMode); persistFeatureUpdate(
featureId,
finalUpdates,
descriptionHistorySource,
enhancementMode,
preEnhancementDescription
);
if (updates.category) { if (updates.category) {
saveCategory(updates.category); saveCategory(updates.category);
} }

View File

@@ -19,7 +19,8 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
featureId: string, featureId: string,
updates: Partial<Feature>, updates: Partial<Feature>,
descriptionHistorySource?: 'enhance' | 'edit', descriptionHistorySource?: 'enhance' | 'edit',
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer',
preEnhancementDescription?: string
) => { ) => {
if (!currentProject) return; if (!currentProject) return;
@@ -35,7 +36,8 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
featureId, featureId,
updates, updates,
descriptionHistorySource, descriptionHistorySource,
enhancementMode enhancementMode,
preEnhancementDescription
); );
if (result.success && result.feature) { if (result.success && result.feature) {
updateFeature(result.feature.id, result.feature); updateFeature(result.feature.id, result.feature);

View File

@@ -4,13 +4,18 @@ import {
FeatureImagePath as DescriptionImagePath, FeatureImagePath as DescriptionImagePath,
ImagePreviewMap, ImagePreviewMap,
} from '@/components/ui/description-image-dropzone'; } from '@/components/ui/description-image-dropzone';
import type { FollowUpHistoryEntry } from '../dialogs/follow-up-dialog';
/**
* Custom hook for managing follow-up dialog state including prompt history
*/
export function useFollowUpState() { export function useFollowUpState() {
const [showFollowUpDialog, setShowFollowUpDialog] = useState(false); const [showFollowUpDialog, setShowFollowUpDialog] = useState(false);
const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null); const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null);
const [followUpPrompt, setFollowUpPrompt] = useState(''); const [followUpPrompt, setFollowUpPrompt] = useState('');
const [followUpImagePaths, setFollowUpImagePaths] = useState<DescriptionImagePath[]>([]); const [followUpImagePaths, setFollowUpImagePaths] = useState<DescriptionImagePath[]>([]);
const [followUpPreviewMap, setFollowUpPreviewMap] = useState<ImagePreviewMap>(() => new Map()); const [followUpPreviewMap, setFollowUpPreviewMap] = useState<ImagePreviewMap>(() => new Map());
const [followUpPromptHistory, setFollowUpPromptHistory] = useState<FollowUpHistoryEntry[]>([]);
const resetFollowUpState = useCallback(() => { const resetFollowUpState = useCallback(() => {
setShowFollowUpDialog(false); setShowFollowUpDialog(false);
@@ -18,6 +23,7 @@ export function useFollowUpState() {
setFollowUpPrompt(''); setFollowUpPrompt('');
setFollowUpImagePaths([]); setFollowUpImagePaths([]);
setFollowUpPreviewMap(new Map()); setFollowUpPreviewMap(new Map());
setFollowUpPromptHistory([]);
}, []); }, []);
const handleFollowUpDialogChange = useCallback( const handleFollowUpDialogChange = useCallback(
@@ -31,6 +37,13 @@ export function useFollowUpState() {
[resetFollowUpState] [resetFollowUpState]
); );
/**
* Adds a new entry to the prompt history
*/
const addToPromptHistory = useCallback((entry: FollowUpHistoryEntry) => {
setFollowUpPromptHistory((prev) => [...prev, entry]);
}, []);
return { return {
// State // State
showFollowUpDialog, showFollowUpDialog,
@@ -38,14 +51,17 @@ export function useFollowUpState() {
followUpPrompt, followUpPrompt,
followUpImagePaths, followUpImagePaths,
followUpPreviewMap, followUpPreviewMap,
followUpPromptHistory,
// Setters // Setters
setShowFollowUpDialog, setShowFollowUpDialog,
setFollowUpFeature, setFollowUpFeature,
setFollowUpPrompt, setFollowUpPrompt,
setFollowUpImagePaths, setFollowUpImagePaths,
setFollowUpPreviewMap, setFollowUpPreviewMap,
setFollowUpPromptHistory,
// Helpers // Helpers
resetFollowUpState, resetFollowUpState,
handleFollowUpDialogChange, handleFollowUpDialogChange,
addToPromptHistory,
}; };
} }

View File

@@ -0,0 +1,152 @@
import { useState } from 'react';
import { createLogger } from '@automaker/utils/logger';
import { Button } from '@/components/ui/button';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Sparkles, ChevronDown, ChevronRight } from 'lucide-react';
import { toast } from 'sonner';
import { getElectronAPI } from '@/lib/electron';
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
import { EnhancementMode, ENHANCEMENT_MODE_LABELS } from './enhancement-constants';
const logger = createLogger('EnhanceWithAI');
interface EnhanceWithAIProps {
/** Current text value to enhance */
value: string;
/** Callback when text is enhanced */
onChange: (enhancedText: string) => void;
/** Optional callback to track enhancement in history */
onHistoryAdd?: (entry: {
mode: EnhancementMode;
originalText: string;
enhancedText: string;
}) => void;
/** Disable the enhancement feature */
disabled?: boolean;
/** Additional CSS classes */
className?: string;
}
/**
* Reusable "Enhance with AI" component
*
* Provides AI-powered text enhancement with multiple modes:
* - Improve Clarity
* - Add Technical Details
* - Simplify
* - Add Acceptance Criteria
* - User Experience
*
* Used in Add Feature, Edit Feature, and Follow-Up dialogs.
*/
export function EnhanceWithAI({
value,
onChange,
onHistoryAdd,
disabled = false,
className,
}: EnhanceWithAIProps) {
const [isEnhancing, setIsEnhancing] = useState(false);
const [enhancementMode, setEnhancementMode] = useState<EnhancementMode>('improve');
const [enhanceOpen, setEnhanceOpen] = useState(false);
// Enhancement model override
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
const handleEnhance = async () => {
if (!value.trim() || isEnhancing || disabled) return;
setIsEnhancing(true);
try {
const api = getElectronAPI();
const result = await api.enhancePrompt?.enhance(
value,
enhancementMode,
enhancementOverride.effectiveModel,
enhancementOverride.effectiveModelEntry.thinkingLevel
);
if (result?.success && result.enhancedText) {
const originalText = value;
const enhancedText = result.enhancedText;
onChange(enhancedText);
// Track in history if callback provided (includes original for restoration)
onHistoryAdd?.({ mode: enhancementMode, originalText, enhancedText });
toast.success('Enhanced successfully!');
} else {
toast.error(result?.error || 'Failed to enhance');
}
} catch (error) {
logger.error('Enhancement failed:', error);
toast.error('Failed to enhance');
} finally {
setIsEnhancing(false);
}
};
return (
<Collapsible open={enhanceOpen} onOpenChange={setEnhanceOpen} className={className}>
<CollapsibleTrigger asChild>
<button
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors w-full py-1"
disabled={disabled}
>
{enhanceOpen ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
<Sparkles className="w-4 h-4" />
<span>Enhance with AI</span>
</button>
</CollapsibleTrigger>
<CollapsibleContent className="pt-3">
<div className="flex flex-wrap items-center gap-2 pl-6">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-8 text-xs" disabled={disabled}>
{ENHANCEMENT_MODE_LABELS[enhancementMode]}
<ChevronDown className="w-3 h-3 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{(Object.entries(ENHANCEMENT_MODE_LABELS) as [EnhancementMode, string][]).map(
([mode, label]) => (
<DropdownMenuItem key={mode} onClick={() => setEnhancementMode(mode)}>
{label}
</DropdownMenuItem>
)
)}
</DropdownMenuContent>
</DropdownMenu>
<Button
type="button"
variant="default"
size="sm"
className="h-8 text-xs"
onClick={handleEnhance}
disabled={!value.trim() || isEnhancing || disabled}
loading={isEnhancing}
>
<Sparkles className="w-3 h-3 mr-1" />
Enhance
</Button>
<ModelOverrideTrigger
currentModelEntry={enhancementOverride.effectiveModelEntry}
onModelChange={enhancementOverride.setOverride}
phase="enhancementModel"
isOverridden={enhancementOverride.isOverridden}
size="sm"
variant="icon"
/>
</div>
</CollapsibleContent>
</Collapsible>
);
}

View File

@@ -0,0 +1,20 @@
/** Enhancement mode options for AI-powered prompt improvement */
export type EnhancementMode = 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer';
/** Labels for enhancement modes displayed in the UI */
export const ENHANCEMENT_MODE_LABELS: Record<EnhancementMode, string> = {
improve: 'Improve Clarity',
technical: 'Add Technical Details',
simplify: 'Simplify',
acceptance: 'Add Acceptance Criteria',
'ux-reviewer': 'User Experience',
};
/** Descriptions for enhancement modes (for tooltips/accessibility) */
export const ENHANCEMENT_MODE_DESCRIPTIONS: Record<EnhancementMode, string> = {
improve: 'Make the prompt clearer and more concise',
technical: 'Add implementation details and specifications',
simplify: 'Reduce complexity while keeping the core intent',
acceptance: 'Add specific acceptance criteria and test cases',
'ux-reviewer': 'Add user experience considerations and flows',
};

View File

@@ -0,0 +1,136 @@
import { useState, useMemo } from 'react';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { History } from 'lucide-react';
import { toast } from 'sonner';
import { EnhancementMode, ENHANCEMENT_MODE_LABELS } from './enhancement-constants';
/**
* Base interface for history entries
*/
export interface BaseHistoryEntry {
timestamp: string;
source: 'initial' | 'enhance' | 'edit';
enhancementMode?: EnhancementMode;
}
interface EnhancementHistoryButtonProps<T extends BaseHistoryEntry> {
/** Array of history entries */
history: T[];
/** Current value to compare against for highlighting */
currentValue: string;
/** Callback when a history entry is restored */
onRestore: (value: string) => void;
/** Function to extract the text value from a history entry */
valueAccessor: (entry: T) => string;
/** Title for the history popover (e.g., "Version History", "Prompt History") */
title?: string;
/** Message shown when restoring an entry */
restoreMessage?: string;
}
/**
* Reusable history button component for enhancement-related history
*
* Displays a popover with a list of historical versions that can be restored.
* Used in edit-feature-dialog and follow-up-dialog for description/prompt history.
*/
export function EnhancementHistoryButton<T extends BaseHistoryEntry>({
history,
currentValue,
onRestore,
valueAccessor,
title = 'Version History',
restoreMessage = 'Restored from history',
}: EnhancementHistoryButtonProps<T>) {
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;
}
const getSourceLabel = (entry: T): string => {
if (entry.source === 'initial') {
return 'Original';
}
if (entry.source === 'enhance') {
const mode = entry.enhancementMode ?? 'improve';
const label = ENHANCEMENT_MODE_LABELS[mode as EnhancementMode] ?? mode;
return `Enhanced (${label})`;
}
return 'Edited';
};
const formatDate = (timestamp: string): string => {
const date = new Date(timestamp);
return date.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
return (
<Popover open={showHistory} onOpenChange={setShowHistory}>
<PopoverTrigger asChild>
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 gap-1.5 text-xs text-muted-foreground"
>
<History className="w-3.5 h-3.5" />
History ({history.length})
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end">
<div className="p-3 border-b">
<h4 className="font-medium text-sm">{title}</h4>
<p className="text-xs text-muted-foreground mt-1">Click a version to restore it</p>
</div>
<div className="max-h-64 overflow-y-auto p-2 space-y-1">
{reversedHistory.map((entry, index) => {
const value = valueAccessor(entry);
const isCurrentVersion = value === currentValue;
const sourceLabel = getSourceLabel(entry);
const formattedDate = formatDate(entry.timestamp);
return (
<button
key={`${entry.timestamp}-${index}`}
onClick={() => {
onRestore(value);
setShowHistory(false);
toast.success(restoreMessage);
}}
className={`w-full text-left p-2 rounded-md hover:bg-muted transition-colors ${
isCurrentVersion ? 'bg-muted/50 border border-primary/20' : ''
}`}
>
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-medium">{sourceLabel}</span>
<span className="text-xs text-muted-foreground">{formattedDate}</span>
</div>
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
{value.slice(0, 100)}
{value.length > 100 ? '...' : ''}
</p>
{isCurrentVersion && (
<span className="text-xs text-primary font-medium mt-1 block">
Current version
</span>
)}
</button>
);
})}
</div>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,3 @@
export * from './enhancement-constants';
export * from './enhance-with-ai';
export * from './enhancement-history-button';

View File

@@ -10,3 +10,4 @@ export * from './planning-mode-selector';
export * from './planning-mode-select'; export * from './planning-mode-select';
export * from './ancestor-context-section'; export * from './ancestor-context-section';
export * from './work-mode-selector'; export * from './work-mode-selector';
export * from './enhancement';

View File

@@ -462,7 +462,8 @@ export interface FeaturesAPI {
featureId: string, featureId: string,
updates: Partial<Feature>, updates: Partial<Feature>,
descriptionHistorySource?: 'enhance' | 'edit', 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 }>; ) => Promise<{ success: boolean; feature?: Feature; error?: string }>;
delete: (projectPath: string, featureId: string) => Promise<{ success: boolean; error?: string }>; delete: (projectPath: string, featureId: string) => Promise<{ success: boolean; error?: string }>;
getAgentOutput: ( getAgentOutput: (
@@ -612,7 +613,8 @@ export interface ElectronAPI {
enhance: ( enhance: (
originalText: string, originalText: string,
enhancementMode: string, enhancementMode: string,
model?: string model?: string,
thinkingLevel?: string
) => Promise<{ ) => Promise<{
success: boolean; success: boolean;
enhancedText?: string; enhancedText?: string;

View File

@@ -1480,7 +1480,8 @@ export class HttpApiClient implements ElectronAPI {
featureId: string, featureId: string,
updates: Partial<Feature>, updates: Partial<Feature>,
descriptionHistorySource?: 'enhance' | 'edit', descriptionHistorySource?: 'enhance' | 'edit',
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer' enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer',
preEnhancementDescription?: string
) => ) =>
this.post('/api/features/update', { this.post('/api/features/update', {
projectPath, projectPath,
@@ -1488,6 +1489,7 @@ export class HttpApiClient implements ElectronAPI {
updates, updates,
descriptionHistorySource, descriptionHistorySource,
enhancementMode, enhancementMode,
preEnhancementDescription,
}), }),
delete: (projectPath: string, featureId: string) => delete: (projectPath: string, featureId: string) =>
this.post('/api/features/delete', { projectPath, featureId }), this.post('/api/features/delete', { projectPath, featureId }),