mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
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:
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user