Merge branch 'v0.13.0rc' into feat/react-query

Merged latest changes from v0.13.0rc into feat/react-query while preserving
React Query migration. Key merge decisions:

- Kept React Query hooks for data fetching (useRunningAgents, useStopFeature, etc.)
- Added backlog plan handling to running-agents-view stop functionality
- Imported both SkeletonPulse and Spinner for CLI status components
- Used Spinner for refresh buttons across all settings sections
- Preserved isBacklogPlan check in agent-output-modal TaskProgressPanel
- Added handleOpenInIntegratedTerminal to worktree actions while keeping React Query mutations
This commit is contained in:
Shirone
2026-01-19 13:28:43 +01:00
387 changed files with 28102 additions and 6881 deletions

View File

@@ -4,10 +4,12 @@
*/
import { useState, useMemo, useEffect, useCallback } from 'react';
import { Loader2, AlertCircle, Plus, X, Sparkles, Lightbulb } from 'lucide-react';
import { AlertCircle, Plus, X, Sparkles, Lightbulb, Trash2 } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { useIdeationStore, type GenerationJob } from '@/store/ideation-store';
import { useAppStore } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron';
@@ -15,9 +17,37 @@ import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import type { AnalysisSuggestion } from '@automaker/types';
// Helper for consistent pluralization of "idea/ideas"
const pluralizeIdea = (count: number) => `idea${count !== 1 ? 's' : ''}`;
// Helper to map priority to Badge variant
const getPriorityVariant = (
priority: string
):
| 'default'
| 'secondary'
| 'destructive'
| 'outline'
| 'success'
| 'warning'
| 'error'
| 'info' => {
switch (priority.toLowerCase()) {
case 'high':
return 'error';
case 'medium':
return 'warning';
case 'low':
return 'info';
default:
return 'secondary';
}
};
interface IdeationDashboardProps {
onGenerateIdeas: () => void;
onAcceptAllReady?: (isReady: boolean, count: number, handler: () => Promise<void>) => void;
onDiscardAllReady?: (isReady: boolean, count: number, handler: () => void) => void;
}
function SuggestionCard({
@@ -34,39 +64,53 @@ function SuggestionCard({
isAdding: boolean;
}) {
return (
<Card className="transition-all hover:border-primary/50">
<CardContent className="p-4">
<Card className="group transition-all hover:border-primary/50 hover:shadow-sm">
<CardContent className="p-5">
<div className="flex items-start gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-start gap-2 mb-1">
<h4 className="font-medium shrink-0">{suggestion.title}</h4>
<div className="flex-1 min-w-0 space-y-3">
<div className="flex flex-col gap-2">
<div className="flex items-start justify-between gap-4">
<h4 className="font-semibold text-base leading-tight">{suggestion.title}</h4>
</div>
<div className="flex items-center gap-2 flex-wrap">
<Badge variant="outline" className="text-xs whitespace-nowrap">
<Badge
variant={getPriorityVariant(suggestion.priority)}
className="text-xs font-medium capitalize"
>
{suggestion.priority}
</Badge>
<Badge variant="secondary" className="text-xs whitespace-nowrap">
<Badge
variant="secondary"
className="text-xs text-muted-foreground bg-secondary/40"
>
{job.prompt.title}
</Badge>
</div>
</div>
<p className="text-sm text-muted-foreground">{suggestion.description}</p>
<p className="text-sm text-muted-foreground leading-relaxed">
{suggestion.description}
</p>
{suggestion.rationale && (
<p className="text-xs text-muted-foreground mt-2 italic">{suggestion.rationale}</p>
<div className="relative pl-3 border-l-2 border-primary/20 mt-3 py-1">
<p className="text-xs text-muted-foreground/80 italic">{suggestion.rationale}</p>
</div>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="flex flex-col gap-2 shrink-0 pt-1">
<Button
size="sm"
variant="ghost"
onClick={onRemove}
onClick={onAccept}
disabled={isAdding}
className="text-muted-foreground hover:text-destructive"
className={cn(
'w-full gap-1.5 shadow-none transition-all',
isAdding ? 'opacity-80' : 'hover:ring-2 hover:ring-primary/20'
)}
>
<X className="w-4 h-4" />
</Button>
<Button size="sm" onClick={onAccept} disabled={isAdding} className="gap-1">
{isAdding ? (
<Loader2 className="w-4 h-4 animate-spin" />
<Spinner size="sm" />
) : (
<>
<Plus className="w-4 h-4" />
@@ -74,6 +118,15 @@ function SuggestionCard({
</>
)}
</Button>
<Button
size="sm"
variant="ghost"
onClick={onRemove}
disabled={isAdding}
className="w-full text-muted-foreground hover:text-destructive hover:bg-destructive/10 h-8"
>
Dismiss
</Button>
</div>
</div>
</CardContent>
@@ -86,19 +139,29 @@ function GeneratingCard({ job }: { job: GenerationJob }) {
const isError = job.status === 'error';
return (
<Card className={cn('transition-all', isError ? 'border-red-500/50' : 'border-blue-500/50')}>
<CardContent className="p-4">
<Card
className={cn(
'transition-all',
isError ? 'border-destructive/50' : 'border-blue-500/30 bg-blue-50/5 dark:bg-blue-900/5'
)}
>
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{isError ? (
<AlertCircle className="w-5 h-5 text-red-500" />
) : (
<Loader2 className="w-5 h-5 text-blue-500 animate-spin" />
)}
<div className="flex items-center gap-4">
<div
className={cn(
'w-10 h-10 rounded-full flex items-center justify-center shrink-0',
isError ? 'bg-destructive/10 text-destructive' : 'bg-blue-500/10 text-blue-500'
)}
>
{isError ? <AlertCircle className="w-5 h-5" /> : <Spinner size="md" />}
</div>
<div>
<p className="font-medium">{job.prompt.title}</p>
<p className="text-sm text-muted-foreground">
{isError ? job.error || 'Failed to generate' : 'Generating ideas...'}
{isError
? job.error || 'Failed to generate'
: 'Analyzing codebase and generating ideas...'}
</p>
</div>
</div>
@@ -130,7 +193,7 @@ function TagFilter({
if (tags.length === 0) return null;
return (
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2 py-2">
{tags.map((tag) => {
const isSelected = selectedTags.has(tag);
const count = tagCounts[tag] || 0;
@@ -139,28 +202,31 @@ function TagFilter({
key={tag}
onClick={() => onToggleTag(tag)}
className={cn(
'px-3 py-1.5 text-sm rounded-full border transition-all flex items-center gap-1.5',
'px-3.5 py-1.5 text-sm rounded-full border shadow-sm transition-all flex items-center gap-2',
isSelected
? 'bg-primary text-primary-foreground border-primary'
: 'bg-secondary/50 text-muted-foreground border-border hover:border-primary/50 hover:text-foreground'
? 'bg-primary text-primary-foreground border-primary ring-2 ring-primary/20'
: 'bg-card text-muted-foreground border-border hover:border-primary/50 hover:text-foreground hover:bg-accent/50'
)}
>
{tag}
<span className="font-medium">{tag}</span>
<span
className={cn(
'text-xs',
isSelected ? 'text-primary-foreground/70' : 'text-muted-foreground/70'
'text-xs py-0.5 px-1.5 rounded-full',
isSelected
? 'bg-primary-foreground/20 text-primary-foreground'
: 'bg-muted text-muted-foreground'
)}
>
({count})
{count}
</span>
</button>
);
})}
{selectedTags.size > 0 && <div className="h-8 w-px bg-border mx-1" />}
{selectedTags.size > 0 && (
<button
onClick={() => selectedTags.forEach((tag) => onToggleTag(tag))}
className="px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
className="px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors font-medium"
>
Clear filters
</button>
@@ -169,13 +235,18 @@ function TagFilter({
);
}
export function IdeationDashboard({ onGenerateIdeas, onAcceptAllReady }: IdeationDashboardProps) {
export function IdeationDashboard({
onGenerateIdeas,
onAcceptAllReady,
onDiscardAllReady,
}: IdeationDashboardProps) {
const currentProject = useAppStore((s) => s.currentProject);
const generationJobs = useIdeationStore((s) => s.generationJobs);
const removeSuggestionFromJob = useIdeationStore((s) => s.removeSuggestionFromJob);
const [addingId, setAddingId] = useState<string | null>(null);
const [isAcceptingAll, setIsAcceptingAll] = useState(false);
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
const [showDiscardConfirm, setShowDiscardConfirm] = useState(false);
// Get jobs for current project only (memoized to prevent unnecessary re-renders)
const projectJobs = useMemo(
@@ -304,23 +375,40 @@ export function IdeationDashboard({ onGenerateIdeas, onAcceptAllReady }: Ideatio
setIsAcceptingAll(false);
if (successCount > 0 && failCount === 0) {
toast.success(`Added ${successCount} idea${successCount > 1 ? 's' : ''} to board`);
toast.success(`Added ${successCount} ${pluralizeIdea(successCount)} to board`);
} else if (successCount > 0 && failCount > 0) {
toast.warning(
`Added ${successCount} idea${successCount > 1 ? 's' : ''}, ${failCount} failed`
);
toast.warning(`Added ${successCount} ${pluralizeIdea(successCount)}, ${failCount} failed`);
} else {
toast.error('Failed to add ideas to board');
}
}, [currentProject?.path, filteredSuggestions, removeSuggestionFromJob]);
// Show discard confirmation dialog
const handleDiscardAll = useCallback(() => {
setShowDiscardConfirm(true);
}, []);
// Actually discard all filtered suggestions
const confirmDiscardAll = useCallback(() => {
const count = filteredSuggestions.length;
for (const { suggestion, job } of filteredSuggestions) {
removeSuggestionFromJob(job.id, suggestion.id);
}
toast.info(`Discarded ${count} ${pluralizeIdea(count)}`);
}, [filteredSuggestions, removeSuggestionFromJob]);
// Common readiness state for bulk operations
const bulkActionsReady = filteredSuggestions.length > 0 && !isAcceptingAll && !addingId;
// Notify parent about accept all readiness
useEffect(() => {
if (onAcceptAllReady) {
const isReady = filteredSuggestions.length > 0 && !isAcceptingAll && !addingId;
onAcceptAllReady(isReady, filteredSuggestions.length, handleAcceptAll);
}
}, [filteredSuggestions.length, isAcceptingAll, addingId, handleAcceptAll, onAcceptAllReady]);
onAcceptAllReady?.(bulkActionsReady, filteredSuggestions.length, handleAcceptAll);
}, [bulkActionsReady, filteredSuggestions.length, handleAcceptAll, onAcceptAllReady]);
// Notify parent about discard all readiness
useEffect(() => {
onDiscardAllReady?.(bulkActionsReady, filteredSuggestions.length, handleDiscardAll);
}, [bulkActionsReady, filteredSuggestions.length, handleDiscardAll, onDiscardAllReady]);
const isEmpty = allSuggestions.length === 0 && activeJobs.length === 0;
@@ -331,10 +419,10 @@ export function IdeationDashboard({ onGenerateIdeas, onAcceptAllReady }: Ideatio
{(generatingCount > 0 || allSuggestions.length > 0) && (
<p className="text-sm text-muted-foreground">
{generatingCount > 0
? `Generating ${generatingCount} idea${generatingCount > 1 ? 's' : ''}...`
? `Generating ${generatingCount} ${pluralizeIdea(generatingCount)}...`
: selectedTags.size > 0
? `Showing ${filteredSuggestions.length} of ${allSuggestions.length} ideas`
: `${allSuggestions.length} idea${allSuggestions.length > 1 ? 's' : ''} ready for review`}
? `Showing ${filteredSuggestions.length} of ${allSuggestions.length} ${pluralizeIdea(allSuggestions.length)}`
: `${allSuggestions.length} ${pluralizeIdea(allSuggestions.length)} ready for review`}
</p>
)}
@@ -419,6 +507,19 @@ export function IdeationDashboard({ onGenerateIdeas, onAcceptAllReady }: Ideatio
</Card>
)}
</div>
{/* Discard All Confirmation Dialog */}
<ConfirmDialog
open={showDiscardConfirm}
onOpenChange={setShowDiscardConfirm}
onConfirm={confirmDiscardAll}
title="Discard All Ideas"
description={`Are you sure you want to discard ${filteredSuggestions.length} ${pluralizeIdea(filteredSuggestions.length)}? This cannot be undone.`}
icon={Trash2}
iconClassName="text-destructive"
confirmText="Discard"
confirmVariant="destructive"
/>
</div>
);
}

View File

@@ -13,8 +13,8 @@ import {
Gauge,
Accessibility,
BarChart3,
Loader2,
} from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { Card, CardContent } from '@/components/ui/card';
import { useGuidedPrompts } from '@/hooks/use-guided-prompts';
import type { IdeaCategory } from '@automaker/types';
@@ -53,7 +53,7 @@ export function PromptCategoryGrid({ onSelect, onBack }: PromptCategoryGridProps
{isLoading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
<Spinner size="lg" />
<span className="ml-2 text-muted-foreground">Loading categories...</span>
</div>
)}
@@ -69,17 +69,19 @@ export function PromptCategoryGrid({ onSelect, onBack }: PromptCategoryGridProps
return (
<Card
key={category.id}
className="cursor-pointer transition-all hover:border-primary hover:shadow-md"
className="group cursor-pointer transition-all duration-300 hover:border-primary hover:shadow-lg hover:-translate-y-1"
onClick={() => onSelect(category.id)}
>
<CardContent className="p-6">
<div className="flex flex-col items-center text-center gap-3">
<div className="p-4 rounded-full bg-primary/10">
<Icon className="w-8 h-8 text-primary" />
<div className="flex flex-col items-center text-center gap-4">
<div className="p-4 rounded-2xl bg-primary/10 text-primary group-hover:bg-primary/20 group-hover:scale-110 transition-all duration-300">
<Icon className="w-8 h-8" />
</div>
<div>
<h3 className="font-semibold text-lg">{category.name}</h3>
<p className="text-muted-foreground text-sm mt-1">{category.description}</p>
<div className="space-y-2">
<h3 className="font-semibold text-lg leading-tight group-hover:text-primary transition-colors">
{category.name}
</h3>
<p className="text-muted-foreground text-sm">{category.description}</p>
</div>
</div>
</CardContent>

View File

@@ -3,7 +3,8 @@
*/
import { useState, useMemo } from 'react';
import { ArrowLeft, Lightbulb, Loader2, CheckCircle2 } from 'lucide-react';
import { ArrowLeft, Lightbulb, CheckCircle2 } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { Card, CardContent } from '@/components/ui/card';
import { useGuidedPrompts } from '@/hooks/use-guided-prompts';
import { useIdeationStore } from '@/store/ideation-store';
@@ -113,7 +114,7 @@ export function PromptList({ category, onBack }: PromptListProps) {
<div className="space-y-3">
{isLoadingPrompts && (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
<Spinner size="lg" />
<span className="ml-2 text-muted-foreground">Loading prompts...</span>
</div>
)}
@@ -133,43 +134,51 @@ export function PromptList({ category, onBack }: PromptListProps) {
return (
<Card
key={prompt.id}
className={`transition-all ${
className={`group transition-all duration-300 ${
isDisabled
? 'opacity-60 cursor-not-allowed'
: 'cursor-pointer hover:border-primary hover:shadow-md'
} ${isLoading || isGenerating ? 'border-blue-500 ring-1 ring-blue-500' : ''} ${
isStarted && !isGenerating ? 'border-green-500/50' : ''
? 'opacity-60 cursor-not-allowed bg-muted/50'
: 'cursor-pointer hover:border-primary hover:shadow-md hover:-translate-x-1'
} ${isLoading || isGenerating ? 'border-blue-500/50 ring-1 ring-blue-500/20 bg-blue-50/10' : ''} ${
isStarted && !isGenerating ? 'border-green-500/50 bg-green-50/10' : ''
}`}
onClick={() => !isDisabled && handleSelectPrompt(prompt)}
>
<CardContent className="p-5">
<div className="flex items-start gap-4">
<div className="flex items-start gap-5">
<div
className={`p-2 rounded-lg mt-0.5 ${
className={`p-3 rounded-xl shrink-0 transition-all duration-300 ${
isLoading || isGenerating
? 'bg-blue-500/10'
? 'bg-blue-500/10 text-blue-500'
: isStarted
? 'bg-green-500/10'
: 'bg-primary/10'
? 'bg-green-500/10 text-green-500'
: 'bg-primary/10 text-primary group-hover:bg-primary/20 group-hover:scale-110'
}`}
>
{isLoading || isGenerating ? (
<Loader2 className="w-4 h-4 text-blue-500 animate-spin" />
<Spinner size="md" />
) : isStarted ? (
<CheckCircle2 className="w-4 h-4 text-green-500" />
<CheckCircle2 className="w-5 h-5" />
) : (
<Lightbulb className="w-4 h-4 text-primary" />
<Lightbulb className="w-5 h-5" />
)}
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold">{prompt.title}</h3>
<p className="text-muted-foreground text-sm mt-1">{prompt.description}</p>
<div className="flex-1 min-w-0 space-y-1">
<div className="flex items-center justify-between gap-2">
<h3 className="font-semibold text-lg group-hover:text-primary transition-colors">
{prompt.title}
</h3>
{isStarted && !isGenerating && (
<span className="text-xs font-medium text-green-600 bg-green-100 dark:bg-green-900/30 dark:text-green-400 px-2 py-0.5 rounded-full">
Generated
</span>
)}
</div>
<p className="text-muted-foreground text-sm leading-relaxed">
{prompt.description}
</p>
{(isLoading || isGenerating) && (
<p className="text-blue-500 text-sm mt-2">Generating in dashboard...</p>
)}
{isStarted && !isGenerating && (
<p className="text-green-500 text-sm mt-2">
Already generated - check dashboard
<p className="text-blue-500 text-sm font-medium animate-pulse pt-1">
Generating ideas...
</p>
)}
</div>

View File

@@ -11,7 +11,8 @@ import { PromptList } from './components/prompt-list';
import { IdeationDashboard } from './components/ideation-dashboard';
import { useGuidedPrompts } from '@/hooks/use-guided-prompts';
import { Button } from '@/components/ui/button';
import { ArrowLeft, ChevronRight, Lightbulb, CheckCheck, Loader2 } from 'lucide-react';
import { ArrowLeft, ChevronRight, Lightbulb, CheckCheck, Trash2 } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import type { IdeaCategory } from '@automaker/types';
import type { IdeationMode } from '@/store/ideation-store';
@@ -71,6 +72,9 @@ function IdeationHeader({
acceptAllCount,
onAcceptAll,
isAcceptingAll,
discardAllReady,
discardAllCount,
onDiscardAll,
}: {
currentMode: IdeationMode;
selectedCategory: IdeaCategory | null;
@@ -81,6 +85,9 @@ function IdeationHeader({
acceptAllCount: number;
onAcceptAll: () => void;
isAcceptingAll: boolean;
discardAllReady: boolean;
discardAllCount: number;
onDiscardAll: () => void;
}) {
const { getCategoryById } = useGuidedPrompts();
const showBackButton = currentMode === 'prompts';
@@ -128,6 +135,17 @@ function IdeationHeader({
</div>
<div className="flex gap-2 items-center">
{currentMode === 'dashboard' && discardAllReady && (
<Button
onClick={onDiscardAll}
variant="outline"
className="gap-2 text-destructive hover:text-destructive"
disabled={isAcceptingAll}
>
<Trash2 className="w-4 h-4" />
Discard All ({discardAllCount})
</Button>
)}
{currentMode === 'dashboard' && acceptAllReady && (
<Button
onClick={onAcceptAll}
@@ -135,11 +153,7 @@ function IdeationHeader({
className="gap-2"
disabled={isAcceptingAll}
>
{isAcceptingAll ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<CheckCheck className="w-4 h-4" />
)}
{isAcceptingAll ? <Spinner size="sm" /> : <CheckCheck className="w-4 h-4" />}
Accept All ({acceptAllCount})
</Button>
)}
@@ -162,6 +176,11 @@ export function IdeationView() {
const [acceptAllHandler, setAcceptAllHandler] = useState<(() => Promise<void>) | null>(null);
const [isAcceptingAll, setIsAcceptingAll] = useState(false);
// Discard all state
const [discardAllReady, setDiscardAllReady] = useState(false);
const [discardAllCount, setDiscardAllCount] = useState(0);
const [discardAllHandler, setDiscardAllHandler] = useState<(() => void) | null>(null);
const handleAcceptAllReady = useCallback(
(isReady: boolean, count: number, handler: () => Promise<void>) => {
setAcceptAllReady(isReady);
@@ -182,6 +201,21 @@ export function IdeationView() {
}
}, [acceptAllHandler]);
const handleDiscardAllReady = useCallback(
(isReady: boolean, count: number, handler: () => void) => {
setDiscardAllReady(isReady);
setDiscardAllCount(count);
setDiscardAllHandler(() => handler);
},
[]
);
const handleDiscardAll = useCallback(() => {
if (discardAllHandler) {
discardAllHandler();
}
}, [discardAllHandler]);
const handleNavigate = useCallback(
(mode: IdeationMode, category?: IdeaCategory | null) => {
setMode(mode);
@@ -245,6 +279,9 @@ export function IdeationView() {
acceptAllCount={acceptAllCount}
onAcceptAll={handleAcceptAll}
isAcceptingAll={isAcceptingAll}
discardAllReady={discardAllReady}
discardAllCount={discardAllCount}
onDiscardAll={handleDiscardAll}
/>
{/* Dashboard - main view */}
@@ -252,6 +289,7 @@ export function IdeationView() {
<IdeationDashboard
onGenerateIdeas={handleGenerateIdeas}
onAcceptAllReady={handleAcceptAllReady}
onDiscardAllReady={handleDiscardAllReady}
/>
)}