feat(backup): add backup.json for feature tracking and status updates

- Introduced a new `backup.json` file to track feature statuses, descriptions, and summaries for better project management.
- Updated `.automaker/feature_list.json` to reflect verified statuses for several features, ensuring accurate representation of progress.
- Enhanced `memory.md` with details on drag-and-drop functionality for features in `waiting_approval` status.
- Improved auto mode service to allow running tasks to complete when auto mode is stopped, enhancing user experience.
This commit is contained in:
Cody Seibert
2025-12-10 14:29:05 -05:00
parent d83eb86f22
commit c502fbc57a
26 changed files with 2497 additions and 298 deletions

View File

@@ -641,11 +641,12 @@ export function AgentView() {
{/* Input */}
{currentSessionId && (
<div className="border-t p-4 space-y-3">
<div className="border-t border-border p-4 space-y-3 bg-background">
{/* Image Drop Zone (when visible) */}
{showImageDropZone && (
<ImageDropZone
onImagesSelected={handleImagesSelected}
images={selectedImages}
maxFiles={5}
className="mb-3"
disabled={isProcessing || !isConnected}
@@ -657,7 +658,7 @@ export function AgentView() {
className={cn(
"flex gap-2 transition-all duration-200 rounded-lg",
isDragOver &&
"bg-blue-50 dark:bg-blue-950/20 ring-2 ring-blue-400 ring-offset-2"
"bg-primary/10 ring-2 ring-primary ring-offset-2 ring-offset-background"
)}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
@@ -679,20 +680,21 @@ export function AgentView() {
disabled={isProcessing || !isConnected}
data-testid="agent-input"
className={cn(
"bg-input border-border",
selectedImages.length > 0 &&
"border-blue-200 bg-blue-50/50 dark:bg-blue-950/20",
"border-primary/50 bg-primary/5",
isDragOver &&
"border-blue-400 bg-blue-50/50 dark:bg-blue-950/20"
"border-primary bg-primary/10"
)}
/>
{selectedImages.length > 0 && !isDragOver && (
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-blue-600 bg-blue-100 dark:bg-blue-900 px-2 py-1 rounded">
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-primary-foreground bg-primary px-2 py-1 rounded">
{selectedImages.length} image
{selectedImages.length > 1 ? "s" : ""}
</div>
)}
{isDragOver && (
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-blue-600 bg-blue-100 dark:bg-blue-900 px-2 py-1 rounded flex items-center gap-1">
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-primary-foreground bg-primary px-2 py-1 rounded flex items-center gap-1">
<Paperclip className="w-3 h-3" />
Drop here
</div>
@@ -707,8 +709,8 @@ export function AgentView() {
disabled={isProcessing || !isConnected}
className={cn(
showImageDropZone &&
"bg-blue-100 text-blue-600 dark:bg-blue-900/50 dark:text-blue-400",
selectedImages.length > 0 && "border-blue-400"
"bg-primary/20 text-primary border-primary",
selectedImages.length > 0 && "border-primary"
)}
title="Attach images"
>

View File

@@ -34,6 +34,7 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
@@ -79,9 +80,20 @@ import {
Sparkles,
UserCircle,
Lightbulb,
Search,
X,
Minimize2,
Square,
Maximize2,
} from "lucide-react";
import { toast } from "sonner";
import { Slider } from "@/components/ui/slider";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Checkbox } from "@/components/ui/checkbox";
import { useAutoMode } from "@/hooks/use-auto-mode";
import {
@@ -188,6 +200,8 @@ export function BoardView() {
useWorktrees,
showProfilesOnly,
aiProfiles,
kanbanCardDetailLevel,
setKanbanCardDetailLevel,
} = useAppStore();
const [activeFeature, setActiveFeature] = useState<Feature | null>(null);
const [editingFeature, setEditingFeature] = useState<Feature | null>(null);
@@ -234,6 +248,10 @@ export function BoardView() {
import("@/lib/electron").FeatureSuggestion[]
>([]);
const [isGeneratingSuggestions, setIsGeneratingSuggestions] = useState(false);
// Search filter for Kanban cards
const [searchQuery, setSearchQuery] = useState("");
// Validation state for add feature form
const [descriptionError, setDescriptionError] = useState(false);
// Make current project available globally for modal
useEffect(() => {
@@ -290,6 +308,9 @@ export function BoardView() {
// Ref to hold the start next callback (to avoid dependency issues)
const startNextFeaturesRef = useRef<() => void>(() => {});
// Ref for search input to enable keyboard shortcut focus
const searchInputRef = useRef<HTMLInputElement>(null);
// Keyboard shortcuts for this view
const boardShortcuts: KeyboardShortcut[] = useMemo(() => {
const shortcuts: KeyboardShortcut[] = [
@@ -303,6 +324,11 @@ export function BoardView() {
action: () => startNextFeaturesRef.current(),
description: "Start next features from backlog",
},
{
key: "/",
action: () => searchInputRef.current?.focus(),
description: "Focus search input",
},
];
// Add shortcuts for in-progress cards (1-9 and 0 for 10th)
@@ -660,9 +686,13 @@ export function BoardView() {
// Determine if dragging is allowed based on status and skipTests
// - Backlog items can always be dragged
// - waiting_approval items can always be dragged (to allow manual verification via drag)
// - skipTests (non-TDD) items can be dragged between in_progress and verified
// - Non-skipTests (TDD) items that are in progress or verified cannot be dragged
if (draggedFeature.status !== "backlog") {
if (
draggedFeature.status !== "backlog" &&
draggedFeature.status !== "waiting_approval"
) {
// Only allow dragging in_progress/verified if it's a skipTests feature and not currently running
if (!draggedFeature.skipTests || isRunningTask) {
console.log(
@@ -720,6 +750,28 @@ export function BoardView() {
} else {
moveFeature(featureId, targetStatus);
}
} else if (draggedFeature.status === "waiting_approval") {
// waiting_approval features can be dragged to verified for manual verification
// NOTE: This check must come BEFORE skipTests check because waiting_approval
// features often have skipTests=true, and we want status-based handling first
if (targetStatus === "verified") {
moveFeature(featureId, "verified");
toast.success("Feature verified", {
description: `Manually verified: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
});
} else if (targetStatus === "backlog") {
// Allow moving waiting_approval cards back to backlog
moveFeature(featureId, "backlog");
toast.info("Feature moved to backlog", {
description: `Moved to Backlog: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
});
}
} else if (draggedFeature.skipTests) {
// skipTests feature being moved between in_progress and verified
if (
@@ -763,6 +815,11 @@ export function BoardView() {
};
const handleAddFeature = () => {
// Validate description is required
if (!newFeature.description.trim()) {
setDescriptionError(true);
return;
}
const category = newFeature.category || "Uncategorized";
const selectedModel = newFeature.model;
const normalizedThinking = modelSupportsThinking(selectedModel)
@@ -1288,7 +1345,17 @@ export function BoardView() {
verified: [],
};
features.forEach((f) => {
// Filter features by search query (case-insensitive)
const normalizedQuery = searchQuery.toLowerCase().trim();
const filteredFeatures = normalizedQuery
? features.filter(
(f) =>
f.description.toLowerCase().includes(normalizedQuery) ||
f.category.toLowerCase().includes(normalizedQuery)
)
: features;
filteredFeatures.forEach((f) => {
// If feature has a running agent, always show it in "in_progress"
const isRunning = runningAutoTasks.includes(f.id);
if (isRunning) {
@@ -1300,7 +1367,7 @@ export function BoardView() {
});
return map;
}, [features, runningAutoTasks]);
}, [features, runningAutoTasks, searchQuery]);
const getColumnFeatures = useCallback(
(columnId: ColumnId) => {
@@ -1556,27 +1623,123 @@ export function BoardView() {
</>
)}
<Button
<HotkeyButton
size="sm"
onClick={() => setShowAddDialog(true)}
hotkey={ACTION_SHORTCUTS.addFeature}
hotkeyActive={false}
data-testid="add-feature-button"
>
<Plus className="w-4 h-4 mr-2" />
Add Feature
<span
className="ml-3 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/20 border border-primary-foreground/30 text-primary-foreground inline-flex items-center justify-center"
data-testid="shortcut-add-feature"
>
{ACTION_SHORTCUTS.addFeature}
</span>
</Button>
</HotkeyButton>
</div>
</div>
{/* Main Content Area */}
<div className="flex-1 flex overflow-hidden">
<div className="flex-1 flex flex-col overflow-hidden">
{/* Search Bar Row */}
<div className="px-4 pt-4 pb-2 flex items-center justify-between">
<div className="relative max-w-md flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
<Input
ref={searchInputRef}
type="text"
placeholder="Search features by keyword..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-12 border-border"
data-testid="kanban-search-input"
/>
{searchQuery ? (
<button
onClick={() => setSearchQuery("")}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-sm hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
data-testid="kanban-search-clear"
aria-label="Clear search"
>
<X className="w-4 h-4" />
</button>
) : (
<span
className="absolute right-2 top-1/2 -translate-y-1/2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70"
data-testid="kanban-search-hotkey"
>
/
</span>
)}
</div>
{/* Kanban Card Detail Level Toggle */}
{isMounted && (
<TooltipProvider>
<div
className="flex items-center rounded-lg bg-secondary border border-border ml-4"
data-testid="kanban-detail-toggle"
>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => setKanbanCardDetailLevel("minimal")}
className={cn(
"p-2 rounded-l-lg transition-colors",
kanbanCardDetailLevel === "minimal"
? "bg-brand-500/20 text-brand-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
)}
data-testid="kanban-toggle-minimal"
>
<Minimize2 className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Minimal - Title & category only</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => setKanbanCardDetailLevel("standard")}
className={cn(
"p-2 transition-colors",
kanbanCardDetailLevel === "standard"
? "bg-brand-500/20 text-brand-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
)}
data-testid="kanban-toggle-standard"
>
<Square className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Standard - Steps & progress</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => setKanbanCardDetailLevel("detailed")}
className={cn(
"p-2 rounded-r-lg transition-colors",
kanbanCardDetailLevel === "detailed"
? "bg-brand-500/20 text-brand-500"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
)}
data-testid="kanban-toggle-detailed"
>
<Maximize2 className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Detailed - Model, tools & tasks</p>
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
)}
</div>
{/* Kanban Columns */}
<div className="flex-1 overflow-x-auto p-4">
<div className="flex-1 overflow-x-auto px-4 pb-4">
<DndContext
sensors={sensors}
collisionDetection={collisionDetectionStrategy}
@@ -1626,19 +1789,18 @@ export function BoardView() {
)}
</Button>
{columnFeatures.length > 0 && (
<Button
<HotkeyButton
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
onClick={handleStartNextFeatures}
hotkey={ACTION_SHORTCUTS.startNext}
hotkeyActive={false}
data-testid="start-next-button"
>
<FastForward className="w-3 h-3 mr-1" />
Start Next
<span className="ml-1 px-1 py-0.5 text-[9px] font-mono rounded bg-accent border border-border-glass">
{ACTION_SHORTCUTS.startNext}
</span>
</Button>
</HotkeyButton>
)}
</div>
) : undefined
@@ -1707,25 +1869,16 @@ export function BoardView() {
{/* Add Feature Dialog */}
<Dialog open={showAddDialog} onOpenChange={(open) => {
setShowAddDialog(open);
// Clear preview map and reset advanced options when dialog closes
// Clear preview map, validation error, and reset advanced options when dialog closes
if (!open) {
setNewFeaturePreviewMap(new Map());
setShowAdvancedOptions(false);
setDescriptionError(false);
}
}}>
<DialogContent
compact={!isMaximized}
data-testid="add-feature-dialog"
onKeyDown={(e) => {
if (
(e.metaKey || e.ctrlKey) &&
e.key === "Enter" &&
newFeature.description
) {
e.preventDefault();
handleAddFeature();
}
}}
>
<DialogHeader>
<DialogTitle>Add New Feature</DialogTitle>
@@ -1755,9 +1908,12 @@ export function BoardView() {
<Label htmlFor="description">Description</Label>
<DescriptionImageDropZone
value={newFeature.description}
onChange={(value) =>
setNewFeature({ ...newFeature, description: value })
}
onChange={(value) => {
setNewFeature({ ...newFeature, description: value });
if (value.trim()) {
setDescriptionError(false);
}
}}
images={newFeature.imagePaths}
onImagesChange={(images) =>
setNewFeature({ ...newFeature, imagePaths: images })
@@ -1766,6 +1922,7 @@ export function BoardView() {
previewMap={newFeaturePreviewMap}
onPreviewMapChange={setNewFeaturePreviewMap}
autoFocus
error={descriptionError}
/>
</div>
<div className="space-y-2">
@@ -2057,20 +2214,14 @@ export function BoardView() {
<Button variant="ghost" onClick={() => setShowAddDialog(false)}>
Cancel
</Button>
<Button
<HotkeyButton
onClick={handleAddFeature}
disabled={!newFeature.description}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showAddDialog}
data-testid="confirm-add-feature"
>
Add Feature
<span
className="ml-3 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/10 border border-primary-foreground/20 inline-flex items-center gap-1.5"
data-testid="shortcut-confirm-add-feature"
>
<span className="leading-none flex items-center justify-center"></span>
<span className="leading-none flex items-center justify-center"></span>
</span>
</Button>
</HotkeyButton>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -2414,12 +2565,14 @@ export function BoardView() {
<Button variant="ghost" onClick={() => setEditingFeature(null)}>
Cancel
</Button>
<Button
<HotkeyButton
onClick={handleUpdateFeature}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={!!editingFeature}
data-testid="confirm-edit-feature"
>
Save Changes
</Button>
</HotkeyButton>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -2584,17 +2737,16 @@ export function BoardView() {
>
Cancel
</Button>
<Button
<HotkeyButton
onClick={handleSendFollowUp}
disabled={!followUpPrompt.trim()}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showFollowUpDialog}
data-testid="confirm-follow-up"
>
<MessageSquare className="w-4 h-4 mr-2" />
Send Follow-Up
<span className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/10 border border-primary-foreground/20">
</span>
</Button>
</HotkeyButton>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -4,6 +4,7 @@ import { useEffect, useState, useCallback, useMemo } from "react";
import { useAppStore } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Card } from "@/components/ui/card";
import {
Plus,
@@ -363,20 +364,16 @@ export function ContextView() {
</div>
</div>
<div className="flex gap-2">
<Button
<HotkeyButton
size="sm"
onClick={() => setIsAddDialogOpen(true)}
hotkey={ACTION_SHORTCUTS.addContextFile}
hotkeyActive={false}
data-testid="add-context-file"
>
<Plus className="w-4 h-4 mr-2" />
Add File
<span
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-secondary border border-border"
data-testid="shortcut-add-context-file"
>
{ACTION_SHORTCUTS.addContextFile}
</span>
</Button>
</HotkeyButton>
</div>
</div>
@@ -650,16 +647,18 @@ export function ContextView() {
>
Cancel
</Button>
<Button
<HotkeyButton
onClick={handleAddFile}
disabled={
!newFileName.trim() ||
(newFileType === "image" && !uploadedImageData)
}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={isAddDialogOpen}
data-testid="confirm-add-file"
>
Add File
</Button>
</HotkeyButton>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -10,6 +10,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import {
@@ -426,9 +427,11 @@ export function FeatureSuggestionsDialog({
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<Button
<HotkeyButton
onClick={handleImport}
disabled={selectedIds.size === 0 || isImporting}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={open && hasSuggestions}
>
{isImporting ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
@@ -437,7 +440,7 @@ export function FeatureSuggestionsDialog({
)}
Import {selectedIds.size} Feature
{selectedIds.size !== 1 ? "s" : ""}
</Button>
</HotkeyButton>
</div>
</div>
)}

View File

@@ -188,9 +188,12 @@ export const KanbanCard = memo(function KanbanCard({
// Dragging logic:
// - Backlog items can always be dragged
// - skipTests items can be dragged even when in_progress or verified (unless currently running)
// - waiting_approval items can always be dragged (to allow manual verification via drag)
// - Non-skipTests (TDD) items in progress or verified cannot be dragged
const isDraggable =
feature.status === "backlog" || (feature.skipTests && !isCurrentAutoTask);
feature.status === "backlog" ||
feature.status === "waiting_approval" ||
(feature.skipTests && !isCurrentAutoTask);
const {
attributes,
listeners,
@@ -336,7 +339,7 @@ export const KanbanCard = memo(function KanbanCard({
<Edit className="w-3 h-3 mr-2" />
Edit
</DropdownMenuItem>
{onViewOutput && (
{onViewOutput && feature.status !== "backlog" && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
@@ -737,25 +740,6 @@ export const KanbanCard = memo(function KanbanCard({
)}
</>
)}
{!isCurrentAutoTask && feature.status === "backlog" && (
<>
{onViewOutput && (
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
onViewOutput();
}}
data-testid={`view-logs-backlog-${feature.id}`}
>
<FileText className="w-3 h-3 mr-1" />
Logs
</Button>
)}
</>
)}
</div>
</CardContent>

View File

@@ -3,6 +3,7 @@
import { useState, useMemo, useCallback, useEffect } from "react";
import { useAppStore, AIProfile, AgentModel, ThinkingLevel, ModelProvider } from "@/store/app-store";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
@@ -236,11 +237,13 @@ function ProfileForm({
onSave,
onCancel,
isEditing,
hotkeyActive,
}: {
profile: Partial<AIProfile>;
onSave: (profile: Omit<AIProfile, "id">) => void;
onCancel: () => void;
isEditing: boolean;
hotkeyActive: boolean;
}) {
const [formData, setFormData] = useState({
name: profile.name || "",
@@ -429,9 +432,14 @@ function ProfileForm({
<Button variant="ghost" onClick={onCancel}>
Cancel
</Button>
<Button onClick={handleSubmit} data-testid="save-profile-button">
<HotkeyButton
onClick={handleSubmit}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={hotkeyActive}
data-testid="save-profile-button"
>
{isEditing ? "Save Changes" : "Create Profile"}
</Button>
</HotkeyButton>
</DialogFooter>
</div>
);
@@ -545,13 +553,15 @@ export function ProfilesView() {
</p>
</div>
</div>
<Button onClick={() => setShowAddDialog(true)} data-testid="add-profile-button" className="relative">
<HotkeyButton
onClick={() => setShowAddDialog(true)}
hotkey={ACTION_SHORTCUTS.addProfile}
hotkeyActive={false}
data-testid="add-profile-button"
>
<Plus className="w-4 h-4 mr-2" />
New Profile
<span className="hidden lg:flex items-center justify-center ml-2 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/20 border border-primary-foreground/30 text-primary-foreground">
{ACTION_SHORTCUTS.addProfile}
</span>
</Button>
</HotkeyButton>
</div>
</div>
</div>
@@ -662,6 +672,7 @@ export function ProfilesView() {
onSave={handleAddProfile}
onCancel={() => setShowAddDialog(false)}
isEditing={false}
hotkeyActive={showAddDialog}
/>
</DialogContent>
</Dialog>
@@ -682,6 +693,7 @@ export function ProfilesView() {
onSave={handleUpdateProfile}
onCancel={() => setEditingProfile(null)}
isEditing={true}
hotkeyActive={!!editingProfile}
/>
)}
</DialogContent>

View File

@@ -68,6 +68,7 @@ export function SettingsView() {
setCurrentView,
theme,
setTheme,
setProjectTheme,
kanbanCardDetailLevel,
setKanbanCardDetailLevel,
defaultSkipTests,
@@ -79,6 +80,18 @@ export function SettingsView() {
currentProject,
moveProjectToTrash,
} = useAppStore();
// Compute the effective theme for the current project
const effectiveTheme = currentProject?.theme || theme;
// Handler to set theme - saves to project if one is selected, otherwise to global
const handleSetTheme = (newTheme: typeof theme) => {
if (currentProject) {
setProjectTheme(currentProject.id, newTheme);
} else {
setTheme(newTheme);
}
};
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
const [googleKey, setGoogleKey] = useState(apiKeys.google);
const [openaiKey, setOpenaiKey] = useState(apiKeys.openai);
@@ -171,13 +184,28 @@ export function SettingsView() {
if (!container) return;
const handleScroll = () => {
const sections = NAV_ITEMS.map((item) => ({
id: item.id,
element: document.getElementById(item.id),
})).filter((s) => s.element);
const sections = NAV_ITEMS.filter(
(item) => item.id !== "danger" || currentProject
)
.map((item) => ({
id: item.id,
element: document.getElementById(item.id),
}))
.filter((s) => s.element);
const containerRect = container.getBoundingClientRect();
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
// Check if scrolled to bottom (within a small threshold)
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 50;
if (isAtBottom && sections.length > 0) {
// If at bottom, highlight the last visible section
setActiveSection(sections[sections.length - 1].id);
return;
}
for (let i = sections.length - 1; i >= 0; i--) {
const section = sections[i];
@@ -194,7 +222,7 @@ export function SettingsView() {
container.addEventListener("scroll", handleScroll);
return () => container.removeEventListener("scroll", handleScroll);
}, []);
}, [currentProject]);
const scrollToSection = useCallback((sectionId: string) => {
const element = document.getElementById(sectionId);
@@ -407,7 +435,7 @@ export function SettingsView() {
{/* Scrollable Content */}
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto p-8">
<div className="max-w-4xl mx-auto space-y-6">
<div className="max-w-4xl mx-auto space-y-6 pb-96">
{/* API Keys Section */}
<div
id="api-keys"
@@ -1012,13 +1040,20 @@ export function SettingsView() {
</div>
<div className="p-6 space-y-4">
<div className="space-y-3">
<Label className="text-foreground">Theme</Label>
<Label className="text-foreground">
Theme{" "}
{currentProject
? `(for ${currentProject.name})`
: "(Global)"}
</Label>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Button
variant={theme === "dark" ? "secondary" : "outline"}
onClick={() => setTheme("dark")}
variant={
effectiveTheme === "dark" ? "secondary" : "outline"
}
onClick={() => handleSetTheme("dark")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "dark"
effectiveTheme === "dark"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
@@ -1028,10 +1063,12 @@ export function SettingsView() {
<span className="font-medium text-sm">Dark</span>
</Button>
<Button
variant={theme === "light" ? "secondary" : "outline"}
onClick={() => setTheme("light")}
variant={
effectiveTheme === "light" ? "secondary" : "outline"
}
onClick={() => handleSetTheme("light")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "light"
effectiveTheme === "light"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
@@ -1041,10 +1078,12 @@ export function SettingsView() {
<span className="font-medium text-sm">Light</span>
</Button>
<Button
variant={theme === "retro" ? "secondary" : "outline"}
onClick={() => setTheme("retro")}
variant={
effectiveTheme === "retro" ? "secondary" : "outline"
}
onClick={() => handleSetTheme("retro")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "retro"
effectiveTheme === "retro"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
@@ -1054,10 +1093,12 @@ export function SettingsView() {
<span className="font-medium text-sm">Retro</span>
</Button>
<Button
variant={theme === "dracula" ? "secondary" : "outline"}
onClick={() => setTheme("dracula")}
variant={
effectiveTheme === "dracula" ? "secondary" : "outline"
}
onClick={() => handleSetTheme("dracula")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "dracula"
effectiveTheme === "dracula"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
@@ -1067,10 +1108,12 @@ export function SettingsView() {
<span className="font-medium text-sm">Dracula</span>
</Button>
<Button
variant={theme === "nord" ? "secondary" : "outline"}
onClick={() => setTheme("nord")}
variant={
effectiveTheme === "nord" ? "secondary" : "outline"
}
onClick={() => handleSetTheme("nord")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "nord"
effectiveTheme === "nord"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
@@ -1080,10 +1123,12 @@ export function SettingsView() {
<span className="font-medium text-sm">Nord</span>
</Button>
<Button
variant={theme === "monokai" ? "secondary" : "outline"}
onClick={() => setTheme("monokai")}
variant={
effectiveTheme === "monokai" ? "secondary" : "outline"
}
onClick={() => handleSetTheme("monokai")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "monokai"
effectiveTheme === "monokai"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
@@ -1093,10 +1138,14 @@ export function SettingsView() {
<span className="font-medium text-sm">Monokai</span>
</Button>
<Button
variant={theme === "tokyonight" ? "secondary" : "outline"}
onClick={() => setTheme("tokyonight")}
variant={
effectiveTheme === "tokyonight"
? "secondary"
: "outline"
}
onClick={() => handleSetTheme("tokyonight")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "tokyonight"
effectiveTheme === "tokyonight"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
@@ -1106,10 +1155,12 @@ export function SettingsView() {
<span className="font-medium text-sm">Tokyo Night</span>
</Button>
<Button
variant={theme === "solarized" ? "secondary" : "outline"}
onClick={() => setTheme("solarized")}
variant={
effectiveTheme === "solarized" ? "secondary" : "outline"
}
onClick={() => handleSetTheme("solarized")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "solarized"
effectiveTheme === "solarized"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
@@ -1119,10 +1170,12 @@ export function SettingsView() {
<span className="font-medium text-sm">Solarized</span>
</Button>
<Button
variant={theme === "gruvbox" ? "secondary" : "outline"}
onClick={() => setTheme("gruvbox")}
variant={
effectiveTheme === "gruvbox" ? "secondary" : "outline"
}
onClick={() => handleSetTheme("gruvbox")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "gruvbox"
effectiveTheme === "gruvbox"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
@@ -1132,10 +1185,14 @@ export function SettingsView() {
<span className="font-medium text-sm">Gruvbox</span>
</Button>
<Button
variant={theme === "catppuccin" ? "secondary" : "outline"}
onClick={() => setTheme("catppuccin")}
variant={
effectiveTheme === "catppuccin"
? "secondary"
: "outline"
}
onClick={() => handleSetTheme("catppuccin")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "catppuccin"
effectiveTheme === "catppuccin"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
@@ -1145,10 +1202,12 @@ export function SettingsView() {
<span className="font-medium text-sm">Catppuccin</span>
</Button>
<Button
variant={theme === "onedark" ? "secondary" : "outline"}
onClick={() => setTheme("onedark")}
variant={
effectiveTheme === "onedark" ? "secondary" : "outline"
}
onClick={() => handleSetTheme("onedark")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "onedark"
effectiveTheme === "onedark"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
@@ -1158,10 +1217,12 @@ export function SettingsView() {
<span className="font-medium text-sm">One Dark</span>
</Button>
<Button
variant={theme === "synthwave" ? "secondary" : "outline"}
onClick={() => setTheme("synthwave")}
variant={
effectiveTheme === "synthwave" ? "secondary" : "outline"
}
onClick={() => handleSetTheme("synthwave")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "synthwave"
effectiveTheme === "synthwave"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
@@ -1307,10 +1368,11 @@ export function SettingsView() {
Show profiles only by default
</Label>
<p className="text-xs text-muted-foreground">
When enabled, the Add Feature dialog will show only AI profiles
and hide advanced model tweaking options (Claude SDK, thinking levels,
and OpenAI Codex CLI). This creates a cleaner, less overwhelming UI.
You can always disable this to access advanced settings.
When enabled, the Add Feature dialog will show only AI
profiles and hide advanced model tweaking options
(Claude SDK, thinking levels, and OpenAI Codex CLI).
This creates a cleaner, less overwhelming UI. You can
always disable this to access advanced settings.
</p>
</div>
</div>

View File

@@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from "react";
import { useAppStore } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Card } from "@/components/ui/card";
import {
Dialog,
@@ -15,6 +16,7 @@ import {
} from "@/components/ui/dialog";
import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2 } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor";
import type { SpecRegenerationEvent } from "@/types/electron";
export function SpecView() {
@@ -299,13 +301,15 @@ export function SpecView() {
>
Cancel
</Button>
<Button
<HotkeyButton
onClick={handleCreateSpec}
disabled={!projectOverview.trim()}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showCreateDialog}
>
<Sparkles className="w-4 h-4 mr-2" />
Generate Spec
</Button>
</HotkeyButton>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -359,12 +363,10 @@ export function SpecView() {
{/* Editor */}
<div className="flex-1 p-4 overflow-hidden">
<Card className="h-full overflow-hidden">
<textarea
className="w-full h-full p-4 font-mono text-sm bg-transparent resize-none focus:outline-none"
<XmlSyntaxEditor
value={appSpec}
onChange={(e) => handleChange(e.target.value)}
onChange={handleChange}
placeholder="Write your app specification here..."
spellCheck={false}
data-testid="spec-editor"
/>
</Card>
@@ -409,9 +411,11 @@ export function SpecView() {
>
Cancel
</Button>
<Button
<HotkeyButton
onClick={handleRegenerate}
disabled={!projectDefinition.trim() || isRegenerating}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showRegenerateDialog}
>
{isRegenerating ? (
<>
@@ -424,7 +428,7 @@ export function SpecView() {
Regenerate Spec
</>
)}
</Button>
</HotkeyButton>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -2,6 +2,7 @@
import { useState, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
@@ -512,14 +513,16 @@ export function WelcomeView() {
>
Cancel
</Button>
<Button
<HotkeyButton
onClick={handleCreateProject}
disabled={!newProjectName || !newProjectPath || isCreating}
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0"
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showNewProjectDialog}
data-testid="confirm-create-project"
>
{isCreating ? "Creating..." : "Create Project"}
</Button>
</HotkeyButton>
</DialogFooter>
</DialogContent>
</Dialog>