mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
Merge branch 'main' into feature/worktrees
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dir \"C:\\Users\\Ben\\Desktop\\appdev\\git\\automaker\\apps\\app\\public\")",
|
||||
"Bash(find:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
66
.gitignore
vendored
66
.gitignore
vendored
@@ -6,11 +6,75 @@ node_modules/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
.next/
|
||||
.turbo/
|
||||
|
||||
# Automaker
|
||||
.automaker/images/
|
||||
.automaker/
|
||||
/.automaker/*
|
||||
/.automaker/
|
||||
|
||||
/logs
|
||||
.worktrees/
|
||||
|
||||
/logs
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# OS-specific files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# IDE/Editor configs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
|
||||
# Editor backup/temp files
|
||||
*~
|
||||
*.bak
|
||||
*.backup
|
||||
*.orig
|
||||
*.swp
|
||||
*.swo
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Local settings (user-specific)
|
||||
*.local.json
|
||||
|
||||
# Application state/backup
|
||||
backup.json
|
||||
|
||||
# Test artifacts
|
||||
test-results/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
*.lcov
|
||||
playwright-report/
|
||||
blob-report/
|
||||
|
||||
# Environment files (keep .example)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
!.env.example
|
||||
!.env.local.example
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Misc
|
||||
*.pem
|
||||
|
||||
BIN
apps/.DS_Store
vendored
BIN
apps/.DS_Store
vendored
Binary file not shown.
@@ -1,30 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const port = process.env.TEST_PORT || 3007;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: "html",
|
||||
timeout: 10000,
|
||||
use: {
|
||||
baseURL: `http://localhost:${port}`,
|
||||
trace: "on-first-retry",
|
||||
screenshot: "only-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: `npx next dev -p ${port}`,
|
||||
url: `http://localhost:${port}`,
|
||||
reuseExistingServer: true,
|
||||
timeout: 60000,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Trash2 } from "lucide-react";
|
||||
|
||||
interface DeleteAllArchivedSessionsDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
archivedCount: number;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export function DeleteAllArchivedSessionsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
archivedCount,
|
||||
onConfirm,
|
||||
}: DeleteAllArchivedSessionsDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent data-testid="delete-all-archived-sessions-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete All Archived Sessions</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete all archived sessions? This action
|
||||
cannot be undone.
|
||||
{archivedCount > 0 && (
|
||||
<span className="block mt-2 text-yellow-500">
|
||||
{archivedCount} session(s) will be deleted.
|
||||
</span>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={onConfirm}
|
||||
data-testid="confirm-delete-all-archived-sessions"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete All
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import type { SessionListItem } from "@/types/electron";
|
||||
import { useKeyboardShortcutsConfig } from "@/hooks/use-keyboard-shortcuts";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { DeleteSessionDialog } from "@/components/delete-session-dialog";
|
||||
import { DeleteAllArchivedSessionsDialog } from "@/components/delete-all-archived-sessions-dialog";
|
||||
|
||||
// Random session name generator
|
||||
const adjectives = [
|
||||
@@ -116,6 +117,7 @@ export function SessionManager({
|
||||
);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [sessionToDelete, setSessionToDelete] = useState<SessionListItem | null>(null);
|
||||
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] = useState(false);
|
||||
|
||||
// Check running state for all sessions
|
||||
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
|
||||
@@ -314,6 +316,20 @@ export function SessionManager({
|
||||
setSessionToDelete(null);
|
||||
};
|
||||
|
||||
// Delete all archived sessions
|
||||
const handleDeleteAllArchivedSessions = async () => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.sessions) return;
|
||||
|
||||
// Delete each archived session
|
||||
for (const session of archivedSessions) {
|
||||
await api.sessions.delete(session.id);
|
||||
}
|
||||
|
||||
await loadSessions();
|
||||
setIsDeleteAllArchivedDialogOpen(false);
|
||||
};
|
||||
|
||||
const activeSessions = sessions.filter((s) => !s.isArchived);
|
||||
const archivedSessions = sessions.filter((s) => s.isArchived);
|
||||
const displayedSessions =
|
||||
@@ -402,6 +418,22 @@ export function SessionManager({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete All Archived button - shown at the top of archived sessions */}
|
||||
{activeTab === "archived" && archivedSessions.length > 0 && (
|
||||
<div className="pb-2 border-b mb-2">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => setIsDeleteAllArchivedDialogOpen(true)}
|
||||
data-testid="delete-all-archived-sessions-button"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete All Archived Sessions
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Session list */}
|
||||
{displayedSessions.map((session) => (
|
||||
<div
|
||||
@@ -574,6 +606,14 @@ export function SessionManager({
|
||||
session={sessionToDelete}
|
||||
onConfirm={confirmDeleteSession}
|
||||
/>
|
||||
|
||||
{/* Delete All Archived Sessions Confirmation Dialog */}
|
||||
<DeleteAllArchivedSessionsDialog
|
||||
open={isDeleteAllArchivedDialogOpen}
|
||||
onOpenChange={setIsDeleteAllArchivedDialogOpen}
|
||||
archivedCount={archivedSessions.length}
|
||||
onConfirm={handleDeleteAllArchivedSessions}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,11 +39,33 @@ export function CategoryAutocomplete({
|
||||
"data-testid": testId,
|
||||
}: CategoryAutocompleteProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [triggerWidth, setTriggerWidth] = React.useState<number>(0);
|
||||
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Update trigger width when component mounts or value changes
|
||||
React.useEffect(() => {
|
||||
if (triggerRef.current) {
|
||||
const updateWidth = () => {
|
||||
setTriggerWidth(triggerRef.current?.offsetWidth || 0);
|
||||
};
|
||||
|
||||
updateWidth();
|
||||
|
||||
// Listen for resize events to handle responsive behavior
|
||||
const resizeObserver = new ResizeObserver(updateWidth);
|
||||
resizeObserver.observe(triggerRef.current);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
@@ -57,7 +79,12 @@ export function CategoryAutocomplete({
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
style={{
|
||||
width: Math.max(triggerWidth, 200),
|
||||
}}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search category..." className="h-9" />
|
||||
<CommandList>
|
||||
|
||||
@@ -620,6 +620,41 @@ export function GitDiffPanel({
|
||||
onToggle={() => toggleFile(fileDiff.filePath)}
|
||||
/>
|
||||
))}
|
||||
{/* Fallback for files that have no diff content (shouldn't happen after fix, but safety net) */}
|
||||
{files.length > 0 && parsedDiffs.length === 0 && (
|
||||
<div className="space-y-2">
|
||||
{files.map((file) => (
|
||||
<div
|
||||
key={file.path}
|
||||
className="border border-border rounded-lg overflow-hidden"
|
||||
>
|
||||
<div className="w-full px-3 py-2 flex items-center gap-2 text-left bg-card">
|
||||
{getFileIcon(file.status)}
|
||||
<span className="flex-1 text-sm font-mono truncate text-foreground">
|
||||
{file.path}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs px-1.5 py-0.5 rounded border font-medium",
|
||||
getStatusBadgeColor(file.status)
|
||||
)}
|
||||
>
|
||||
{getStatusDisplayName(file.status)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="px-4 py-3 text-sm text-muted-foreground bg-background border-t border-border">
|
||||
{file.status === "?" ? (
|
||||
<span>New file - content preview not available</span>
|
||||
) : file.status === "D" ? (
|
||||
<span>File deleted</span>
|
||||
) : (
|
||||
<span>Diff content not available</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -541,6 +541,7 @@ export function BoardView() {
|
||||
isMaximized={isMaximized}
|
||||
showProfilesOnly={showProfilesOnly}
|
||||
aiProfiles={aiProfiles}
|
||||
allFeatures={hookFeatures}
|
||||
/>
|
||||
|
||||
{/* Agent Output Modal */}
|
||||
|
||||
@@ -82,8 +82,8 @@ function formatThinkingLevel(level: ThinkingLevel | undefined): string {
|
||||
const labels: Record<ThinkingLevel, string> = {
|
||||
none: "",
|
||||
low: "Low",
|
||||
medium: "Med",
|
||||
high: "High",
|
||||
medium: "Med", //
|
||||
high: "High", //
|
||||
ultrathink: "Ultra",
|
||||
};
|
||||
return labels[level];
|
||||
@@ -323,6 +323,49 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Priority badge */}
|
||||
{feature.priority && (
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute px-2 py-1 text-sm font-bold rounded-md flex items-center justify-center z-10",
|
||||
"top-2 left-2 min-w-[36px]",
|
||||
feature.priority === 1 &&
|
||||
"bg-red-500/20 text-red-500 border-2 border-red-500/50",
|
||||
feature.priority === 2 &&
|
||||
"bg-yellow-500/20 text-yellow-500 border-2 border-yellow-500/50",
|
||||
feature.priority === 3 &&
|
||||
"bg-blue-500/20 text-blue-500 border-2 border-blue-500/50"
|
||||
)}
|
||||
data-testid={`priority-badge-${feature.id}`}
|
||||
>
|
||||
P{feature.priority}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="text-xs">
|
||||
<p>
|
||||
{feature.priority === 1
|
||||
? "High Priority"
|
||||
: feature.priority === 2
|
||||
? "Medium Priority"
|
||||
: "Low Priority"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* Category text next to priority badge */}
|
||||
{feature.priority && (
|
||||
<div className="absolute top-2 left-[54px] right-12 z-10 flex items-center h-[32px]">
|
||||
<span className="text-[11px] text-muted-foreground/70 font-medium truncate">
|
||||
{feature.category}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Skip Tests (Manual) indicator badge */}
|
||||
{feature.skipTests && !feature.error && (
|
||||
<TooltipProvider delayDuration={200}>
|
||||
@@ -331,7 +374,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<div
|
||||
className={cn(
|
||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10",
|
||||
"top-2 left-2",
|
||||
feature.priority ? "top-11 left-2" : "top-2 left-2",
|
||||
"bg-[var(--status-warning-bg)] border border-[var(--status-warning)]/40 text-[var(--status-warning)]"
|
||||
)}
|
||||
data-testid={`skip-tests-badge-${feature.id}`}
|
||||
@@ -354,7 +397,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<div
|
||||
className={cn(
|
||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10",
|
||||
"top-2 left-2",
|
||||
feature.priority ? "top-11 left-2" : "top-2 left-2",
|
||||
"bg-[var(--status-error-bg)] border border-[var(--status-error)]/40 text-[var(--status-error)]"
|
||||
)}
|
||||
data-testid={`error-badge-${feature.id}`}
|
||||
@@ -374,7 +417,11 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<div
|
||||
className={cn(
|
||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10",
|
||||
feature.skipTests ? "top-8 left-2" : "top-2 left-2",
|
||||
feature.priority
|
||||
? "top-11 left-2"
|
||||
: feature.skipTests
|
||||
? "top-8 left-2"
|
||||
: "top-2 left-2",
|
||||
"bg-[var(--status-success-bg)] border border-[var(--status-success)]/40 text-[var(--status-success)]",
|
||||
"animate-pulse"
|
||||
)}
|
||||
@@ -394,7 +441,9 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
className={cn(
|
||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded-md flex items-center gap-1 z-10 cursor-default",
|
||||
"bg-[var(--status-info-bg)] border border-[var(--status-info)]/40 text-[var(--status-info)]",
|
||||
feature.error || feature.skipTests || isJustFinished
|
||||
feature.priority
|
||||
? "top-11 left-2"
|
||||
: feature.error || feature.skipTests || isJustFinished
|
||||
? "top-8 left-2"
|
||||
: "top-2 left-2"
|
||||
)}
|
||||
@@ -415,7 +464,10 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<CardHeader
|
||||
className={cn(
|
||||
"p-3 pb-2 block",
|
||||
(feature.skipTests || feature.error || isJustFinished) && "pt-10",
|
||||
feature.priority && "pt-12",
|
||||
!feature.priority &&
|
||||
(feature.skipTests || feature.error || isJustFinished) &&
|
||||
"pt-10",
|
||||
hasWorktree &&
|
||||
(feature.skipTests || feature.error || isJustFinished) &&
|
||||
"pt-14"
|
||||
@@ -606,9 +658,11 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{!feature.priority && (
|
||||
<CardDescription className="text-[11px] mt-1.5 truncate text-muted-foreground/70">
|
||||
{feature.category}
|
||||
</CardDescription>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -901,7 +955,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
) : null}
|
||||
{onViewOutput && !feature.skipTests && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-7 text-[11px] px-2"
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -20,7 +20,15 @@ import {
|
||||
FeatureImagePath as DescriptionImagePath,
|
||||
ImagePreviewMap,
|
||||
} from "@/components/ui/description-image-dropzone";
|
||||
import { MessageSquare, Settings2, FlaskConical } from "lucide-react";
|
||||
import {
|
||||
MessageSquare,
|
||||
Settings2,
|
||||
FlaskConical,
|
||||
Sparkles,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { modelSupportsThinking } from "@/lib/utils";
|
||||
import {
|
||||
useAppStore,
|
||||
@@ -34,7 +42,14 @@ import {
|
||||
ThinkingLevelSelector,
|
||||
ProfileQuickSelect,
|
||||
TestingTabContent,
|
||||
PrioritySelector,
|
||||
} from "../shared";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
interface AddFeatureDialogProps {
|
||||
open: boolean;
|
||||
@@ -49,6 +64,7 @@ interface AddFeatureDialogProps {
|
||||
model: AgentModel;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
branchName: string;
|
||||
priority: number;
|
||||
}) => void;
|
||||
categorySuggestions: string[];
|
||||
branchSuggestions: string[];
|
||||
@@ -79,11 +95,19 @@ export function AddFeatureDialog({
|
||||
model: "opus" as AgentModel,
|
||||
thinkingLevel: "none" as ThinkingLevel,
|
||||
branchName: "main",
|
||||
priority: 2 as number, // Default to medium priority
|
||||
});
|
||||
const [newFeaturePreviewMap, setNewFeaturePreviewMap] =
|
||||
useState<ImagePreviewMap>(() => new Map());
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
const [descriptionError, setDescriptionError] = useState(false);
|
||||
const [isEnhancing, setIsEnhancing] = useState(false);
|
||||
const [enhancementMode, setEnhancementMode] = useState<
|
||||
"improve" | "technical" | "simplify" | "acceptance"
|
||||
>("improve");
|
||||
|
||||
// Get enhancement model from store
|
||||
const { enhancementModel } = useAppStore();
|
||||
|
||||
// Sync skipTests default when dialog opens
|
||||
useEffect(() => {
|
||||
@@ -117,6 +141,7 @@ export function AddFeatureDialog({
|
||||
model: selectedModel,
|
||||
thinkingLevel: normalizedThinking,
|
||||
branchName: newFeature.branchName,
|
||||
priority: newFeature.priority,
|
||||
});
|
||||
|
||||
// Reset form
|
||||
@@ -128,6 +153,7 @@ export function AddFeatureDialog({
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: "opus",
|
||||
priority: 2,
|
||||
thinkingLevel: "none",
|
||||
branchName: "main",
|
||||
});
|
||||
@@ -146,6 +172,33 @@ export function AddFeatureDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnhanceDescription = async () => {
|
||||
if (!newFeature.description.trim() || isEnhancing) return;
|
||||
|
||||
setIsEnhancing(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.enhancePrompt?.enhance(
|
||||
newFeature.description,
|
||||
enhancementMode,
|
||||
enhancementModel
|
||||
);
|
||||
|
||||
if (result?.success && result.enhancedText) {
|
||||
const enhancedText = result.enhancedText;
|
||||
setNewFeature((prev) => ({ ...prev, description: enhancedText }));
|
||||
toast.success("Description enhanced!");
|
||||
} else {
|
||||
toast.error(result?.error || "Failed to enhance description");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Enhancement failed:", error);
|
||||
toast.error("Failed to enhance description");
|
||||
} finally {
|
||||
setIsEnhancing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelSelect = (model: AgentModel) => {
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
@@ -156,7 +209,10 @@ export function AddFeatureDialog({
|
||||
});
|
||||
};
|
||||
|
||||
const handleProfileSelect = (model: AgentModel, thinkingLevel: ThinkingLevel) => {
|
||||
const handleProfileSelect = (
|
||||
model: AgentModel,
|
||||
thinkingLevel: ThinkingLevel
|
||||
) => {
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model,
|
||||
@@ -210,7 +266,10 @@ export function AddFeatureDialog({
|
||||
</TabsList>
|
||||
|
||||
{/* Prompt Tab */}
|
||||
<TabsContent value="prompt" className="space-y-4 overflow-y-auto">
|
||||
<TabsContent
|
||||
value="prompt"
|
||||
className="space-y-4 overflow-y-auto cursor-default"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<DescriptionImageDropZone
|
||||
@@ -232,6 +291,58 @@ export function AddFeatureDialog({
|
||||
error={descriptionError}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-fit items-center gap-3 select-none cursor-default">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-[200px] justify-between"
|
||||
>
|
||||
{enhancementMode === "improve" && "Improve Clarity"}
|
||||
{enhancementMode === "technical" && "Add Technical Details"}
|
||||
{enhancementMode === "simplify" && "Simplify"}
|
||||
{enhancementMode === "acceptance" &&
|
||||
"Add Acceptance Criteria"}
|
||||
<ChevronDown className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnhancementMode("improve")}
|
||||
>
|
||||
Improve Clarity
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnhancementMode("technical")}
|
||||
>
|
||||
Add Technical Details
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnhancementMode("simplify")}
|
||||
>
|
||||
Simplify
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnhancementMode("acceptance")}
|
||||
>
|
||||
Add Acceptance Criteria
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleEnhanceDescription}
|
||||
disabled={!newFeature.description.trim() || isEnhancing}
|
||||
loading={isEnhancing}
|
||||
>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Enhance with AI
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category (optional)</Label>
|
||||
<CategoryAutocomplete
|
||||
@@ -256,13 +367,26 @@ export function AddFeatureDialog({
|
||||
data-testid="feature-branch-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Work will be done in this branch. A worktree will be created if needed.
|
||||
Work will be done in this branch. A worktree will be created if
|
||||
needed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Priority Selector */}
|
||||
<PrioritySelector
|
||||
selectedPriority={newFeature.priority}
|
||||
onPrioritySelect={(priority) =>
|
||||
setNewFeature({ ...newFeature, priority })
|
||||
}
|
||||
testIdPrefix="priority"
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Model Tab */}
|
||||
<TabsContent value="model" className="space-y-4 overflow-y-auto">
|
||||
<TabsContent
|
||||
value="model"
|
||||
className="space-y-4 overflow-y-auto cursor-default"
|
||||
>
|
||||
{/* Show Advanced Options Toggle */}
|
||||
{showProfilesOnly && (
|
||||
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg border border-border">
|
||||
@@ -325,16 +449,17 @@ export function AddFeatureDialog({
|
||||
</TabsContent>
|
||||
|
||||
{/* Testing Tab */}
|
||||
<TabsContent value="testing" className="space-y-4 overflow-y-auto">
|
||||
<TabsContent
|
||||
value="testing"
|
||||
className="space-y-4 overflow-y-auto cursor-default"
|
||||
>
|
||||
<TestingTabContent
|
||||
skipTests={newFeature.skipTests}
|
||||
onSkipTestsChange={(skipTests) =>
|
||||
setNewFeature({ ...newFeature, skipTests })
|
||||
}
|
||||
steps={newFeature.steps}
|
||||
onStepsChange={(steps) =>
|
||||
setNewFeature({ ...newFeature, steps })
|
||||
}
|
||||
onStepsChange={(steps) => setNewFeature({ ...newFeature, steps })}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Feature } from "@/store/app-store";
|
||||
import { AlertCircle, CheckCircle2, Circle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface DependencyTreeDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
feature: Feature | null;
|
||||
allFeatures: Feature[];
|
||||
}
|
||||
|
||||
export function DependencyTreeDialog({
|
||||
open,
|
||||
onClose,
|
||||
feature,
|
||||
allFeatures,
|
||||
}: DependencyTreeDialogProps) {
|
||||
const [dependencyTree, setDependencyTree] = useState<{
|
||||
dependencies: Feature[];
|
||||
dependents: Feature[];
|
||||
}>({ dependencies: [], dependents: [] });
|
||||
|
||||
useEffect(() => {
|
||||
if (!feature) return;
|
||||
|
||||
// Find features this depends on
|
||||
const dependencies = (feature.dependencies || [])
|
||||
.map((depId) => allFeatures.find((f) => f.id === depId))
|
||||
.filter((f): f is Feature => f !== undefined);
|
||||
|
||||
// Find features that depend on this one
|
||||
const dependents = allFeatures.filter((f) =>
|
||||
f.dependencies?.includes(feature.id)
|
||||
);
|
||||
|
||||
setDependencyTree({ dependencies, dependents });
|
||||
}, [feature, allFeatures]);
|
||||
|
||||
if (!feature) return null;
|
||||
|
||||
const getStatusIcon = (status: Feature["status"]) => {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
case "verified":
|
||||
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
|
||||
case "in_progress":
|
||||
case "waiting_approval":
|
||||
return <Circle className="w-4 h-4 text-blue-500 fill-blue-500/20" />;
|
||||
default:
|
||||
return <Circle className="w-4 h-4 text-muted-foreground/50" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityBadge = (priority?: number) => {
|
||||
if (!priority) return null;
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs px-1.5 py-0.5 rounded font-medium",
|
||||
priority === 1 && "bg-red-500/20 text-red-500",
|
||||
priority === 2 && "bg-yellow-500/20 text-yellow-500",
|
||||
priority === 3 && "bg-blue-500/20 text-blue-500"
|
||||
)}
|
||||
>
|
||||
P{priority}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Dependency Tree</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 mt-4">
|
||||
{/* Current Feature */}
|
||||
<div className="border-2 border-primary rounded-lg p-4 bg-primary/5">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
{getStatusIcon(feature.status)}
|
||||
<h3 className="font-semibold text-sm">Current Feature</h3>
|
||||
{getPriorityBadge(feature.priority)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{feature.description}</p>
|
||||
<p className="text-xs text-muted-foreground/70 mt-2">
|
||||
Category: {feature.category}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Dependencies (what this feature needs) */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<h3 className="font-semibold text-sm">
|
||||
Dependencies ({dependencyTree.dependencies.length})
|
||||
</h3>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
This feature requires:
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{dependencyTree.dependencies.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground/70 italic border border-dashed rounded-lg p-4 text-center">
|
||||
No dependencies - this feature can be started independently
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{dependencyTree.dependencies.map((dep) => (
|
||||
<div
|
||||
key={dep.id}
|
||||
className={cn(
|
||||
"border rounded-lg p-3 transition-colors",
|
||||
dep.status === "completed" || dep.status === "verified"
|
||||
? "bg-green-500/5 border-green-500/20"
|
||||
: "bg-muted/30 border-border"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
{getStatusIcon(dep.status)}
|
||||
<span className="text-sm font-medium flex-1">
|
||||
{dep.description.slice(0, 100)}
|
||||
{dep.description.length > 100 && "..."}
|
||||
</span>
|
||||
{getPriorityBadge(dep.priority)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 ml-7">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{dep.category}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs px-2 py-0.5 rounded-full",
|
||||
dep.status === "completed" || dep.status === "verified"
|
||||
? "bg-green-500/20 text-green-600"
|
||||
: dep.status === "in_progress"
|
||||
? "bg-blue-500/20 text-blue-600"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{dep.status.replace(/_/g, " ")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dependents (what depends on this feature) */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<h3 className="font-semibold text-sm">
|
||||
Dependents ({dependencyTree.dependents.length})
|
||||
</h3>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Features blocked by this:
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{dependencyTree.dependents.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground/70 italic border border-dashed rounded-lg p-4 text-center">
|
||||
No dependents - no other features are waiting on this one
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{dependencyTree.dependents.map((dependent) => (
|
||||
<div
|
||||
key={dependent.id}
|
||||
className="border rounded-lg p-3 bg-muted/30"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
{getStatusIcon(dependent.status)}
|
||||
<span className="text-sm font-medium flex-1">
|
||||
{dependent.description.slice(0, 100)}
|
||||
{dependent.description.length > 100 && "..."}
|
||||
</span>
|
||||
{getPriorityBadge(dependent.priority)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 ml-7">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{dependent.category}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs px-2 py-0.5 rounded-full",
|
||||
dependent.status === "completed" ||
|
||||
dependent.status === "verified"
|
||||
? "bg-green-500/20 text-green-600"
|
||||
: dependent.status === "in_progress"
|
||||
? "bg-blue-500/20 text-blue-600"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{dependent.status.replace(/_/g, " ")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Warning for incomplete dependencies */}
|
||||
{dependencyTree.dependencies.some(
|
||||
(d) => d.status !== "completed" && d.status !== "verified"
|
||||
) && (
|
||||
<div className="flex items-start gap-3 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||
<AlertCircle className="w-5 h-5 text-yellow-600 shrink-0 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-yellow-700 dark:text-yellow-500">
|
||||
Incomplete Dependencies
|
||||
</p>
|
||||
<p className="text-yellow-600 dark:text-yellow-400 mt-1">
|
||||
This feature has dependencies that aren't completed yet.
|
||||
Consider completing them first for a smoother implementation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -20,20 +20,38 @@ import {
|
||||
FeatureImagePath as DescriptionImagePath,
|
||||
ImagePreviewMap,
|
||||
} from "@/components/ui/description-image-dropzone";
|
||||
import { MessageSquare, Settings2, FlaskConical } from "lucide-react";
|
||||
import {
|
||||
MessageSquare,
|
||||
Settings2,
|
||||
FlaskConical,
|
||||
Sparkles,
|
||||
ChevronDown,
|
||||
GitBranch,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { modelSupportsThinking } from "@/lib/utils";
|
||||
import {
|
||||
Feature,
|
||||
AgentModel,
|
||||
ThinkingLevel,
|
||||
AIProfile,
|
||||
useAppStore,
|
||||
} from "@/store/app-store";
|
||||
import {
|
||||
ModelSelector,
|
||||
ThinkingLevelSelector,
|
||||
ProfileQuickSelect,
|
||||
TestingTabContent,
|
||||
PrioritySelector,
|
||||
} from "../shared";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { DependencyTreeDialog } from "./dependency-tree-dialog";
|
||||
|
||||
interface EditFeatureDialogProps {
|
||||
feature: Feature | null;
|
||||
@@ -49,6 +67,7 @@ interface EditFeatureDialogProps {
|
||||
thinkingLevel: ThinkingLevel;
|
||||
imagePaths: DescriptionImagePath[];
|
||||
branchName: string;
|
||||
priority: number;
|
||||
}
|
||||
) => void;
|
||||
categorySuggestions: string[];
|
||||
@@ -56,6 +75,7 @@ interface EditFeatureDialogProps {
|
||||
isMaximized: boolean;
|
||||
showProfilesOnly: boolean;
|
||||
aiProfiles: AIProfile[];
|
||||
allFeatures: Feature[];
|
||||
}
|
||||
|
||||
export function EditFeatureDialog({
|
||||
@@ -67,11 +87,20 @@ export function EditFeatureDialog({
|
||||
isMaximized,
|
||||
showProfilesOnly,
|
||||
aiProfiles,
|
||||
allFeatures,
|
||||
}: EditFeatureDialogProps) {
|
||||
const [editingFeature, setEditingFeature] = useState<Feature | null>(feature);
|
||||
const [editFeaturePreviewMap, setEditFeaturePreviewMap] =
|
||||
useState<ImagePreviewMap>(() => new Map());
|
||||
const [showEditAdvancedOptions, setShowEditAdvancedOptions] = useState(false);
|
||||
const [isEnhancing, setIsEnhancing] = useState(false);
|
||||
const [enhancementMode, setEnhancementMode] = useState<
|
||||
"improve" | "technical" | "simplify" | "acceptance"
|
||||
>("improve");
|
||||
const [showDependencyTree, setShowDependencyTree] = useState(false);
|
||||
|
||||
// Get enhancement model from store
|
||||
const { enhancementModel } = useAppStore();
|
||||
|
||||
useEffect(() => {
|
||||
setEditingFeature(feature);
|
||||
@@ -85,8 +114,10 @@ export function EditFeatureDialog({
|
||||
if (!editingFeature) return;
|
||||
|
||||
const selectedModel = (editingFeature.model ?? "opus") as AgentModel;
|
||||
const normalizedThinking: ThinkingLevel = modelSupportsThinking(selectedModel)
|
||||
? (editingFeature.thinkingLevel ?? "none")
|
||||
const normalizedThinking: ThinkingLevel = modelSupportsThinking(
|
||||
selectedModel
|
||||
)
|
||||
? editingFeature.thinkingLevel ?? "none"
|
||||
: "none";
|
||||
|
||||
const updates = {
|
||||
@@ -98,6 +129,7 @@ export function EditFeatureDialog({
|
||||
thinkingLevel: normalizedThinking,
|
||||
imagePaths: editingFeature.imagePaths ?? [],
|
||||
branchName: editingFeature.branchName ?? "main",
|
||||
priority: editingFeature.priority ?? 2,
|
||||
};
|
||||
|
||||
onUpdate(editingFeature.id, updates);
|
||||
@@ -123,7 +155,10 @@ export function EditFeatureDialog({
|
||||
});
|
||||
};
|
||||
|
||||
const handleProfileSelect = (model: AgentModel, thinkingLevel: ThinkingLevel) => {
|
||||
const handleProfileSelect = (
|
||||
model: AgentModel,
|
||||
thinkingLevel: ThinkingLevel
|
||||
) => {
|
||||
if (!editingFeature) return;
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
@@ -132,6 +167,35 @@ export function EditFeatureDialog({
|
||||
});
|
||||
};
|
||||
|
||||
const handleEnhanceDescription = async () => {
|
||||
if (!editingFeature?.description.trim() || isEnhancing) return;
|
||||
|
||||
setIsEnhancing(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.enhancePrompt?.enhance(
|
||||
editingFeature.description,
|
||||
enhancementMode,
|
||||
enhancementModel
|
||||
);
|
||||
|
||||
if (result?.success && result.enhancedText) {
|
||||
const enhancedText = result.enhancedText;
|
||||
setEditingFeature((prev) =>
|
||||
prev ? { ...prev, description: enhancedText } : prev
|
||||
);
|
||||
toast.success("Description enhanced!");
|
||||
} else {
|
||||
toast.error(result?.error || "Failed to enhance description");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Enhancement failed:", error);
|
||||
toast.error("Failed to enhance description");
|
||||
} finally {
|
||||
setIsEnhancing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const editModelAllowsThinking = modelSupportsThinking(editingFeature?.model);
|
||||
|
||||
if (!editingFeature) {
|
||||
@@ -180,7 +244,10 @@ export function EditFeatureDialog({
|
||||
</TabsList>
|
||||
|
||||
{/* Prompt Tab */}
|
||||
<TabsContent value="prompt" className="space-y-4 overflow-y-auto">
|
||||
<TabsContent
|
||||
value="prompt"
|
||||
className="space-y-4 overflow-y-auto cursor-default"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-description">Description</Label>
|
||||
<DescriptionImageDropZone
|
||||
@@ -204,6 +271,58 @@ export function EditFeatureDialog({
|
||||
data-testid="edit-feature-description"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-fit items-center gap-3 select-none cursor-default">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-[180px] justify-between"
|
||||
>
|
||||
{enhancementMode === "improve" && "Improve Clarity"}
|
||||
{enhancementMode === "technical" && "Add Technical Details"}
|
||||
{enhancementMode === "simplify" && "Simplify"}
|
||||
{enhancementMode === "acceptance" &&
|
||||
"Add Acceptance Criteria"}
|
||||
<ChevronDown className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnhancementMode("improve")}
|
||||
>
|
||||
Improve Clarity
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnhancementMode("technical")}
|
||||
>
|
||||
Add Technical Details
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnhancementMode("simplify")}
|
||||
>
|
||||
Simplify
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEnhancementMode("acceptance")}
|
||||
>
|
||||
Add Acceptance Criteria
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleEnhanceDescription}
|
||||
disabled={!editingFeature.description.trim() || isEnhancing}
|
||||
loading={isEnhancing}
|
||||
>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Enhance with AI
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-category">Category (optional)</Label>
|
||||
<CategoryAutocomplete
|
||||
@@ -241,14 +360,30 @@ export function EditFeatureDialog({
|
||||
)}
|
||||
{editingFeature.status === "backlog" && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Work will be done in this branch. A worktree will be created if needed.
|
||||
Work will be done in this branch. A worktree will be created
|
||||
if needed.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Priority Selector */}
|
||||
<PrioritySelector
|
||||
selectedPriority={editingFeature.priority ?? 2}
|
||||
onPrioritySelect={(priority) =>
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
priority,
|
||||
})
|
||||
}
|
||||
testIdPrefix="edit-priority"
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Model Tab */}
|
||||
<TabsContent value="model" className="space-y-4 overflow-y-auto">
|
||||
<TabsContent
|
||||
value="model"
|
||||
className="space-y-4 overflow-y-auto cursor-default"
|
||||
>
|
||||
{/* Show Advanced Options Toggle */}
|
||||
{showProfilesOnly && (
|
||||
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg border border-border">
|
||||
@@ -314,7 +449,10 @@ export function EditFeatureDialog({
|
||||
</TabsContent>
|
||||
|
||||
{/* Testing Tab */}
|
||||
<TabsContent value="testing" className="space-y-4 overflow-y-auto">
|
||||
<TabsContent
|
||||
value="testing"
|
||||
className="space-y-4 overflow-y-auto cursor-default"
|
||||
>
|
||||
<TestingTabContent
|
||||
skipTests={editingFeature.skipTests ?? false}
|
||||
onSkipTestsChange={(skipTests) =>
|
||||
@@ -328,7 +466,16 @@ export function EditFeatureDialog({
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<DialogFooter>
|
||||
<DialogFooter className="sm:!justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDependencyTree(true)}
|
||||
className="gap-2 h-10"
|
||||
>
|
||||
<GitBranch className="w-4 h-4" />
|
||||
View Dependency Tree
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -340,8 +487,16 @@ export function EditFeatureDialog({
|
||||
>
|
||||
Save Changes
|
||||
</HotkeyButton>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
<DependencyTreeDialog
|
||||
open={showDependencyTree}
|
||||
onClose={() => setShowDependencyTree(false)}
|
||||
feature={editingFeature}
|
||||
allFeatures={allFeatures}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -239,6 +239,7 @@ export function FeatureSuggestionsDialog({
|
||||
steps: s.steps,
|
||||
status: "backlog" as const,
|
||||
skipTests: true, // As specified, testing mode true
|
||||
priority: s.priority, // Preserve priority from suggestion
|
||||
}));
|
||||
|
||||
// Create each new feature using the features API
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { useCallback } from "react";
|
||||
import { Feature, FeatureImage, AgentModel, ThinkingLevel, useAppStore } from "@/store/app-store";
|
||||
import {
|
||||
Feature,
|
||||
FeatureImage,
|
||||
AgentModel,
|
||||
ThinkingLevel,
|
||||
useAppStore,
|
||||
} from "@/store/app-store";
|
||||
import { FeatureImagePath as DescriptionImagePath } from "@/components/ui/description-image-dropzone";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { toast } from "sonner";
|
||||
@@ -12,7 +18,10 @@ interface UseBoardActionsProps {
|
||||
runningAutoTasks: string[];
|
||||
loadFeatures: () => Promise<void>;
|
||||
persistFeatureCreate: (feature: Feature) => Promise<void>;
|
||||
persistFeatureUpdate: (featureId: string, updates: Partial<Feature>) => Promise<void>;
|
||||
persistFeatureUpdate: (
|
||||
featureId: string,
|
||||
updates: Partial<Feature>
|
||||
) => Promise<void>;
|
||||
persistFeatureDelete: (featureId: string) => Promise<void>;
|
||||
saveCategory: (category: string) => Promise<void>;
|
||||
setEditingFeature: (feature: Feature | null) => void;
|
||||
@@ -57,7 +66,13 @@ export function useBoardActions({
|
||||
projectPath,
|
||||
onWorktreeCreated,
|
||||
}: UseBoardActionsProps) {
|
||||
const { addFeature, updateFeature, removeFeature, moveFeature, useWorktrees } = useAppStore();
|
||||
const {
|
||||
addFeature,
|
||||
updateFeature,
|
||||
removeFeature,
|
||||
moveFeature,
|
||||
useWorktrees,
|
||||
} = useAppStore();
|
||||
const autoMode = useAutoMode();
|
||||
|
||||
/**
|
||||
@@ -98,9 +113,13 @@ export function useBoardActions({
|
||||
}
|
||||
return result.worktree.path;
|
||||
} else {
|
||||
console.error("[BoardActions] Failed to create worktree:", result.error);
|
||||
console.error(
|
||||
"[BoardActions] Failed to create worktree:",
|
||||
result.error
|
||||
);
|
||||
toast.error("Failed to create worktree", {
|
||||
description: result.error || "Could not create worktree for this branch.",
|
||||
description:
|
||||
result.error || "Could not create worktree for this branch.",
|
||||
});
|
||||
return projectPath; // Fall back to project path
|
||||
}
|
||||
@@ -126,6 +145,7 @@ export function useBoardActions({
|
||||
model: AgentModel;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
branchName: string;
|
||||
priority: number;
|
||||
}) => {
|
||||
const newFeatureData = {
|
||||
...featureData,
|
||||
@@ -150,6 +170,7 @@ export function useBoardActions({
|
||||
thinkingLevel: ThinkingLevel;
|
||||
imagePaths: DescriptionImagePath[];
|
||||
branchName: string;
|
||||
priority: number;
|
||||
}
|
||||
) => {
|
||||
updateFeature(featureId, updates);
|
||||
@@ -173,7 +194,9 @@ export function useBoardActions({
|
||||
try {
|
||||
await autoMode.stopFeature(featureId);
|
||||
toast.success("Agent stopped", {
|
||||
description: `Stopped and deleted: ${truncateDescription(feature.description)}`,
|
||||
description: `Stopped and deleted: ${truncateDescription(
|
||||
feature.description
|
||||
)}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Board] Error stopping feature before delete:", error);
|
||||
@@ -191,11 +214,17 @@ export function useBoardActions({
|
||||
await api.deleteFile(imagePathObj.path);
|
||||
console.log(`[Board] Deleted image: ${imagePathObj.path}`);
|
||||
} catch (error) {
|
||||
console.error(`[Board] Failed to delete image ${imagePathObj.path}:`, error);
|
||||
console.error(
|
||||
`[Board] Failed to delete image ${imagePathObj.path}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Board] Error deleting images for feature ${featureId}:`, error);
|
||||
console.error(
|
||||
`[Board] Error deleting images for feature ${featureId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +257,10 @@ export function useBoardActions({
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log("[Board] Feature run started successfully in worktree:", featureWorktreePath || "main");
|
||||
console.log(
|
||||
"[Board] Feature run started successfully in worktree:",
|
||||
featureWorktreePath || "main"
|
||||
);
|
||||
} else {
|
||||
console.error("[Board] Failed to run feature:", result.error);
|
||||
await loadFeatures();
|
||||
@@ -276,7 +308,10 @@ export function useBoardActions({
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.autoMode.verifyFeature(currentProject.path, feature.id);
|
||||
const result = await api.autoMode.verifyFeature(
|
||||
currentProject.path,
|
||||
feature.id
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log("[Board] Feature verification started successfully");
|
||||
@@ -303,7 +338,10 @@ export function useBoardActions({
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.autoMode.resumeFeature(currentProject.path, feature.id);
|
||||
const result = await api.autoMode.resumeFeature(
|
||||
currentProject.path,
|
||||
feature.id
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log("[Board] Feature resume started successfully");
|
||||
@@ -327,7 +365,9 @@ export function useBoardActions({
|
||||
justFinishedAt: undefined,
|
||||
});
|
||||
toast.success("Feature verified", {
|
||||
description: `Marked as verified: ${truncateDescription(feature.description)}`,
|
||||
description: `Marked as verified: ${truncateDescription(
|
||||
feature.description
|
||||
)}`,
|
||||
});
|
||||
},
|
||||
[moveFeature, persistFeatureUpdate]
|
||||
@@ -342,7 +382,9 @@ export function useBoardActions({
|
||||
updateFeature(feature.id, updates);
|
||||
persistFeatureUpdate(feature.id, updates);
|
||||
toast.info("Feature moved back", {
|
||||
description: `Moved back to In Progress: ${truncateDescription(feature.description)}`,
|
||||
description: `Moved back to In Progress: ${truncateDescription(
|
||||
feature.description
|
||||
)}`,
|
||||
});
|
||||
},
|
||||
[updateFeature, persistFeatureUpdate]
|
||||
@@ -355,7 +397,12 @@ export function useBoardActions({
|
||||
setFollowUpImagePaths([]);
|
||||
setShowFollowUpDialog(true);
|
||||
},
|
||||
[setFollowUpFeature, setFollowUpPrompt, setFollowUpImagePaths, setShowFollowUpDialog]
|
||||
[
|
||||
setFollowUpFeature,
|
||||
setFollowUpPrompt,
|
||||
setFollowUpImagePaths,
|
||||
setShowFollowUpDialog,
|
||||
]
|
||||
);
|
||||
|
||||
const handleSendFollowUp = useCallback(async () => {
|
||||
@@ -389,18 +436,27 @@ export function useBoardActions({
|
||||
setFollowUpPreviewMap(new Map());
|
||||
|
||||
toast.success("Follow-up started", {
|
||||
description: `Continuing work on: ${truncateDescription(featureDescription)}`,
|
||||
description: `Continuing work on: ${truncateDescription(
|
||||
featureDescription
|
||||
)}`,
|
||||
});
|
||||
|
||||
const imagePaths = followUpImagePaths.map((img) => img.path);
|
||||
// Use the feature's worktreePath to ensure work happens in the correct branch
|
||||
const featureWorktreePath = followUpFeature.worktreePath;
|
||||
api.autoMode
|
||||
.followUpFeature(currentProject.path, followUpFeature.id, followUpPrompt, imagePaths, featureWorktreePath)
|
||||
.followUpFeature(
|
||||
currentProject.path,
|
||||
followUpFeature.id,
|
||||
followUpPrompt,
|
||||
imagePaths,
|
||||
featureWorktreePath
|
||||
)
|
||||
.catch((error) => {
|
||||
console.error("[Board] Error sending follow-up:", error);
|
||||
toast.error("Failed to send follow-up", {
|
||||
description: error instanceof Error ? error.message : "An error occurred",
|
||||
description:
|
||||
error instanceof Error ? error.message : "An error occurred",
|
||||
});
|
||||
loadFeatures();
|
||||
});
|
||||
@@ -428,19 +484,26 @@ export function useBoardActions({
|
||||
if (!api?.autoMode?.commitFeature) {
|
||||
console.error("Commit feature API not available");
|
||||
toast.error("Commit not available", {
|
||||
description: "This feature is not available in the current version.",
|
||||
description:
|
||||
"This feature is not available in the current version.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass the feature's worktreePath to ensure commits happen in the correct worktree
|
||||
const result = await api.autoMode.commitFeature(currentProject.path, feature.id, feature.worktreePath);
|
||||
const result = await api.autoMode.commitFeature(
|
||||
currentProject.path,
|
||||
feature.id,
|
||||
feature.worktreePath
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
moveFeature(feature.id, "verified");
|
||||
persistFeatureUpdate(feature.id, { status: "verified" });
|
||||
toast.success("Feature committed", {
|
||||
description: `Committed and verified: ${truncateDescription(feature.description)}`,
|
||||
description: `Committed and verified: ${truncateDescription(
|
||||
feature.description
|
||||
)}`,
|
||||
});
|
||||
// Refresh worktree selector to update commit counts
|
||||
onWorktreeCreated?.();
|
||||
@@ -454,12 +517,19 @@ export function useBoardActions({
|
||||
} catch (error) {
|
||||
console.error("[Board] Error committing feature:", error);
|
||||
toast.error("Failed to commit feature", {
|
||||
description: error instanceof Error ? error.message : "An error occurred",
|
||||
description:
|
||||
error instanceof Error ? error.message : "An error occurred",
|
||||
});
|
||||
await loadFeatures();
|
||||
}
|
||||
},
|
||||
[currentProject, moveFeature, persistFeatureUpdate, loadFeatures, onWorktreeCreated]
|
||||
[
|
||||
currentProject,
|
||||
moveFeature,
|
||||
persistFeatureUpdate,
|
||||
loadFeatures,
|
||||
onWorktreeCreated,
|
||||
]
|
||||
);
|
||||
|
||||
const handleRevertFeature = useCallback(
|
||||
@@ -471,17 +541,23 @@ export function useBoardActions({
|
||||
if (!api?.worktree?.revertFeature) {
|
||||
console.error("Worktree API not available");
|
||||
toast.error("Revert not available", {
|
||||
description: "This feature is not available in the current version.",
|
||||
description:
|
||||
"This feature is not available in the current version.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.worktree.revertFeature(currentProject.path, feature.id);
|
||||
const result = await api.worktree.revertFeature(
|
||||
currentProject.path,
|
||||
feature.id
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
await loadFeatures();
|
||||
toast.success("Feature reverted", {
|
||||
description: `All changes discarded. Moved back to backlog: ${truncateDescription(feature.description)}`,
|
||||
description: `All changes discarded. Moved back to backlog: ${truncateDescription(
|
||||
feature.description
|
||||
)}`,
|
||||
});
|
||||
} else {
|
||||
console.error("[Board] Failed to revert feature:", result.error);
|
||||
@@ -492,7 +568,8 @@ export function useBoardActions({
|
||||
} catch (error) {
|
||||
console.error("[Board] Error reverting feature:", error);
|
||||
toast.error("Failed to revert feature", {
|
||||
description: error instanceof Error ? error.message : "An error occurred",
|
||||
description:
|
||||
error instanceof Error ? error.message : "An error occurred",
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -508,17 +585,23 @@ export function useBoardActions({
|
||||
if (!api?.worktree?.mergeFeature) {
|
||||
console.error("Worktree API not available");
|
||||
toast.error("Merge not available", {
|
||||
description: "This feature is not available in the current version.",
|
||||
description:
|
||||
"This feature is not available in the current version.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.worktree.mergeFeature(currentProject.path, feature.id);
|
||||
const result = await api.worktree.mergeFeature(
|
||||
currentProject.path,
|
||||
feature.id
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
await loadFeatures();
|
||||
toast.success("Feature merged", {
|
||||
description: `Changes merged to main branch: ${truncateDescription(feature.description)}`,
|
||||
description: `Changes merged to main branch: ${truncateDescription(
|
||||
feature.description
|
||||
)}`,
|
||||
});
|
||||
} else {
|
||||
console.error("[Board] Failed to merge feature:", result.error);
|
||||
@@ -529,7 +612,8 @@ export function useBoardActions({
|
||||
} catch (error) {
|
||||
console.error("[Board] Error merging feature:", error);
|
||||
toast.error("Failed to merge feature", {
|
||||
description: error instanceof Error ? error.message : "An error occurred",
|
||||
description:
|
||||
error instanceof Error ? error.message : "An error occurred",
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -560,7 +644,9 @@ export function useBoardActions({
|
||||
persistFeatureUpdate(feature.id, updates);
|
||||
|
||||
toast.success("Feature restored", {
|
||||
description: `Moved back to verified: ${truncateDescription(feature.description)}`,
|
||||
description: `Moved back to verified: ${truncateDescription(
|
||||
feature.description
|
||||
)}`,
|
||||
});
|
||||
},
|
||||
[updateFeature, persistFeatureUpdate]
|
||||
@@ -589,7 +675,12 @@ export function useBoardActions({
|
||||
setOutputFeature(targetFeature);
|
||||
}
|
||||
},
|
||||
[inProgressFeaturesForShortcuts, outputFeature?.id, setShowOutputModal, setOutputFeature]
|
||||
[
|
||||
inProgressFeaturesForShortcuts,
|
||||
outputFeature?.id,
|
||||
setShowOutputModal,
|
||||
setOutputFeature,
|
||||
]
|
||||
);
|
||||
|
||||
const handleForceStopFeature = useCallback(
|
||||
@@ -610,13 +701,18 @@ export function useBoardActions({
|
||||
toast.success("Agent stopped", {
|
||||
description:
|
||||
targetStatus === "waiting_approval"
|
||||
? `Stopped commit - returned to waiting approval: ${truncateDescription(feature.description)}`
|
||||
: `Stopped working on: ${truncateDescription(feature.description)}`,
|
||||
? `Stopped commit - returned to waiting approval: ${truncateDescription(
|
||||
feature.description
|
||||
)}`
|
||||
: `Stopped working on: ${truncateDescription(
|
||||
feature.description
|
||||
)}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Board] Error stopping feature:", error);
|
||||
toast.error("Failed to stop agent", {
|
||||
description: error instanceof Error ? error.message : "An error occurred",
|
||||
description:
|
||||
error instanceof Error ? error.message : "An error occurred",
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -656,9 +752,19 @@ export function useBoardActions({
|
||||
onWorktreeCreated?.();
|
||||
// Start the implementation
|
||||
// Pass feature with worktreePath so handleRunFeature uses the correct path
|
||||
await handleStartImplementation({ ...feature, worktreePath: worktreePath || undefined });
|
||||
await handleStartImplementation({
|
||||
...feature,
|
||||
worktreePath: worktreePath || undefined,
|
||||
});
|
||||
}
|
||||
}, [features, runningAutoTasks, handleStartImplementation, getOrCreateWorktreeForFeature, persistFeatureUpdate, onWorktreeCreated]);
|
||||
}, [
|
||||
features,
|
||||
runningAutoTasks,
|
||||
handleStartImplementation,
|
||||
getOrCreateWorktreeForFeature,
|
||||
persistFeatureUpdate,
|
||||
onWorktreeCreated,
|
||||
]);
|
||||
|
||||
const handleDeleteAllVerified = useCallback(async () => {
|
||||
const verifiedFeatures = features.filter((f) => f.status === "verified");
|
||||
@@ -669,10 +775,7 @@ export function useBoardActions({
|
||||
try {
|
||||
await autoMode.stopFeature(feature.id);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[Board] Error stopping feature before delete:",
|
||||
error
|
||||
);
|
||||
console.error("[Board] Error stopping feature before delete:", error);
|
||||
}
|
||||
}
|
||||
removeFeature(feature.id);
|
||||
@@ -682,7 +785,13 @@ export function useBoardActions({
|
||||
toast.success("All verified features deleted", {
|
||||
description: `Deleted ${verifiedFeatures.length} feature(s).`,
|
||||
});
|
||||
}, [features, runningAutoTasks, autoMode, removeFeature, persistFeatureDelete]);
|
||||
}, [
|
||||
features,
|
||||
runningAutoTasks,
|
||||
autoMode,
|
||||
removeFeature,
|
||||
persistFeatureDelete,
|
||||
]);
|
||||
|
||||
return {
|
||||
handleAddFeature,
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from "./model-selector";
|
||||
export * from "./thinking-level-selector";
|
||||
export * from "./profile-quick-select";
|
||||
export * from "./testing-tab-content";
|
||||
export * from "./priority-selector";
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PrioritySelectorProps {
|
||||
selectedPriority: number;
|
||||
onPrioritySelect: (priority: number) => void;
|
||||
testIdPrefix?: string;
|
||||
}
|
||||
|
||||
export function PrioritySelector({
|
||||
selectedPriority,
|
||||
onPrioritySelect,
|
||||
testIdPrefix = "priority",
|
||||
}: PrioritySelectorProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label>Priority</Label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPrioritySelect(1)}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors",
|
||||
selectedPriority === 1
|
||||
? "bg-red-500/20 text-red-500 border-2 border-red-500/50"
|
||||
: "bg-muted/50 text-muted-foreground border border-border hover:bg-muted"
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-high-button`}
|
||||
>
|
||||
High
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPrioritySelect(2)}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors",
|
||||
selectedPriority === 2
|
||||
? "bg-yellow-500/20 text-yellow-500 border-2 border-yellow-500/50"
|
||||
: "bg-muted/50 text-muted-foreground border border-border hover:bg-muted"
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-medium-button`}
|
||||
>
|
||||
Medium
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPrioritySelect(3)}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-2 rounded-md text-sm font-medium transition-colors",
|
||||
selectedPriority === 3
|
||||
? "bg-blue-500/20 text-blue-500 border-2 border-blue-500/50"
|
||||
: "bg-muted/50 text-muted-foreground border border-border hover:bg-muted"
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-low-button`}
|
||||
>
|
||||
Low
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,28 +2,18 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Key,
|
||||
Palette,
|
||||
Terminal,
|
||||
FlaskConical,
|
||||
Trash2,
|
||||
Settings2,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
} from "lucide-react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
import { useCliStatus } from "./settings-view/hooks/use-cli-status";
|
||||
import { useScrollTracking } from "@/hooks/use-scroll-tracking";
|
||||
import { useCliStatus, useSettingsView } from "./settings-view/hooks";
|
||||
import { NAV_ITEMS } from "./settings-view/config/navigation";
|
||||
import { SettingsHeader } from "./settings-view/components/settings-header";
|
||||
import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog";
|
||||
import { DeleteProjectDialog } from "./settings-view/components/delete-project-dialog";
|
||||
import { SettingsNavigation } from "./settings-view/components/settings-navigation";
|
||||
import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section";
|
||||
import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status";
|
||||
import { AIEnhancementSection } from "./settings-view/ai-enhancement";
|
||||
import { AppearanceSection } from "./settings-view/appearance/appearance-section";
|
||||
import { AudioSection } from "./settings-view/audio/audio-section";
|
||||
import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts/keyboard-shortcuts-section";
|
||||
import { FeatureDefaultsSection } from "./settings-view/feature-defaults/feature-defaults-section";
|
||||
import { DangerZoneSection } from "./settings-view/danger-zone/danger-zone-section";
|
||||
@@ -33,17 +23,6 @@ import type {
|
||||
} from "./settings-view/shared/types";
|
||||
import type { Project as ElectronProject } from "@/lib/electron";
|
||||
|
||||
// Navigation items for the side panel
|
||||
const NAV_ITEMS = [
|
||||
{ id: "api-keys", label: "API Keys", icon: Key },
|
||||
{ id: "claude", label: "Claude", icon: Terminal },
|
||||
{ id: "appearance", label: "Appearance", icon: Palette },
|
||||
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
|
||||
{ id: "audio", label: "Audio", icon: Volume2 },
|
||||
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
|
||||
{ id: "danger", label: "Danger Zone", icon: Trash2 },
|
||||
];
|
||||
|
||||
export function SettingsView() {
|
||||
const {
|
||||
theme,
|
||||
@@ -91,23 +70,72 @@ export function SettingsView() {
|
||||
};
|
||||
|
||||
// Use CLI status hook
|
||||
const {
|
||||
claudeCliStatus,
|
||||
isCheckingClaudeCli,
|
||||
handleRefreshClaudeCli,
|
||||
} = useCliStatus();
|
||||
const { claudeCliStatus, isCheckingClaudeCli, handleRefreshClaudeCli } =
|
||||
useCliStatus();
|
||||
|
||||
// Use scroll tracking hook
|
||||
const { activeSection, scrollToSection, scrollContainerRef } =
|
||||
useScrollTracking({
|
||||
items: NAV_ITEMS,
|
||||
filterFn: (item) => item.id !== "danger" || !!currentProject,
|
||||
initialSection: "api-keys",
|
||||
});
|
||||
// Use settings view navigation hook
|
||||
const { activeView, navigateTo } = useSettingsView();
|
||||
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false);
|
||||
|
||||
// Render the active section based on current view
|
||||
const renderActiveSection = () => {
|
||||
switch (activeView) {
|
||||
case "claude":
|
||||
return (
|
||||
<ClaudeCliStatus
|
||||
status={claudeCliStatus}
|
||||
isChecking={isCheckingClaudeCli}
|
||||
onRefresh={handleRefreshClaudeCli}
|
||||
/>
|
||||
);
|
||||
case "ai-enhancement":
|
||||
return <AIEnhancementSection />;
|
||||
case "appearance":
|
||||
return (
|
||||
<AppearanceSection
|
||||
effectiveTheme={effectiveTheme}
|
||||
currentProject={settingsProject}
|
||||
onThemeChange={handleSetTheme}
|
||||
/>
|
||||
);
|
||||
case "keyboard":
|
||||
return (
|
||||
<KeyboardShortcutsSection
|
||||
onOpenKeyboardMap={() => setShowKeyboardMapDialog(true)}
|
||||
/>
|
||||
);
|
||||
case "audio":
|
||||
return (
|
||||
<AudioSection
|
||||
muteDoneSound={muteDoneSound}
|
||||
onMuteDoneSoundChange={setMuteDoneSound}
|
||||
/>
|
||||
);
|
||||
case "defaults":
|
||||
return (
|
||||
<FeatureDefaultsSection
|
||||
showProfilesOnly={showProfilesOnly}
|
||||
defaultSkipTests={defaultSkipTests}
|
||||
useWorktrees={useWorktrees}
|
||||
onShowProfilesOnlyChange={setShowProfilesOnly}
|
||||
onDefaultSkipTestsChange={setDefaultSkipTests}
|
||||
onUseWorktreesChange={setUseWorktrees}
|
||||
/>
|
||||
);
|
||||
case "danger":
|
||||
return (
|
||||
<DangerZoneSection
|
||||
project={settingsProject}
|
||||
onDeleteClick={() => setShowDeleteDialog(true)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <ApiKeysSection />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
||||
@@ -118,107 +146,17 @@ export function SettingsView() {
|
||||
|
||||
{/* Content Area with Sidebar */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Sticky Side Navigation */}
|
||||
{/* Side Navigation - No longer scrolls, just switches views */}
|
||||
<SettingsNavigation
|
||||
navItems={NAV_ITEMS}
|
||||
activeSection={activeSection}
|
||||
activeSection={activeView}
|
||||
currentProject={currentProject}
|
||||
onNavigate={scrollToSection}
|
||||
onNavigate={navigateTo}
|
||||
/>
|
||||
|
||||
{/* Scrollable Content */}
|
||||
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto p-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6 pb-96">
|
||||
{/* API Keys Section */}
|
||||
<ApiKeysSection />
|
||||
|
||||
{/* Claude CLI Status Section */}
|
||||
{claudeCliStatus && (
|
||||
<ClaudeCliStatus
|
||||
status={claudeCliStatus}
|
||||
isChecking={isCheckingClaudeCli}
|
||||
onRefresh={handleRefreshClaudeCli}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Appearance Section */}
|
||||
<AppearanceSection
|
||||
effectiveTheme={effectiveTheme}
|
||||
currentProject={settingsProject}
|
||||
onThemeChange={handleSetTheme}
|
||||
/>
|
||||
|
||||
|
||||
{/* Keyboard Shortcuts Section */}
|
||||
<KeyboardShortcutsSection
|
||||
onOpenKeyboardMap={() => setShowKeyboardMapDialog(true)}
|
||||
/>
|
||||
|
||||
{/* Audio Section */}
|
||||
<div
|
||||
id="audio"
|
||||
className="rounded-2xl border border-border/50 bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl shadow-sm shadow-black/5 overflow-hidden scroll-mt-6"
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Volume2 className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||
Audio
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Configure audio and notification settings.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Mute Done Sound Setting */}
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
<Checkbox
|
||||
id="mute-done-sound"
|
||||
checked={muteDoneSound}
|
||||
onCheckedChange={(checked) =>
|
||||
setMuteDoneSound(checked === true)
|
||||
}
|
||||
className="mt-1"
|
||||
data-testid="mute-done-sound-checkbox"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label
|
||||
htmlFor="mute-done-sound"
|
||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||
>
|
||||
<VolumeX className="w-4 h-4 text-brand-500" />
|
||||
Mute notification sound when agents complete
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
When enabled, disables the "ding" sound that
|
||||
plays when an agent completes a feature. The feature
|
||||
will still move to the completed column, but without
|
||||
audio notification.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature Defaults Section */}
|
||||
<FeatureDefaultsSection
|
||||
showProfilesOnly={showProfilesOnly}
|
||||
defaultSkipTests={defaultSkipTests}
|
||||
useWorktrees={useWorktrees}
|
||||
onShowProfilesOnlyChange={setShowProfilesOnly}
|
||||
onDefaultSkipTestsChange={setDefaultSkipTests}
|
||||
onUseWorktreesChange={setUseWorktrees}
|
||||
/>
|
||||
|
||||
{/* Danger Zone Section - Only show when a project is selected */}
|
||||
<DangerZoneSection
|
||||
project={settingsProject}
|
||||
onDeleteClick={() => setShowDeleteDialog(true)}
|
||||
/>
|
||||
</div>
|
||||
{/* Content Panel - Shows only the active section */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
<div className="max-w-4xl mx-auto">{renderActiveSection()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { CLAUDE_MODELS } from "@/components/views/board-view/shared/model-constants";
|
||||
|
||||
export function AIEnhancementSection() {
|
||||
const { enhancementModel, setEnhancementModel } = useAppStore();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Sparkles className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">AI Enhancement</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Choose the model used when enhancing feature descriptions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="space-y-4">
|
||||
<Label className="text-foreground font-medium">
|
||||
Enhancement Model
|
||||
</Label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{CLAUDE_MODELS.map(({ id, label, description, badge }) => {
|
||||
const isActive = enhancementModel === id;
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => setEnhancementModel(id)}
|
||||
className={cn(
|
||||
"group flex flex-col items-start gap-2 px-4 py-4 rounded-xl text-left",
|
||||
"transition-all duration-200 ease-out",
|
||||
isActive
|
||||
? [
|
||||
"bg-gradient-to-br from-brand-500/15 to-brand-600/10",
|
||||
"border-2 border-brand-500/40",
|
||||
"text-foreground",
|
||||
"shadow-md shadow-brand-500/10",
|
||||
]
|
||||
: [
|
||||
"bg-accent/30 hover:bg-accent/50",
|
||||
"border border-border/50 hover:border-border",
|
||||
"text-muted-foreground hover:text-foreground",
|
||||
"hover:shadow-sm",
|
||||
],
|
||||
"hover:scale-[1.02] active:scale-[0.98]"
|
||||
)}
|
||||
data-testid={`enhancement-model-${id}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<span className={cn(
|
||||
"font-medium text-sm",
|
||||
isActive ? "text-foreground" : "group-hover:text-foreground"
|
||||
)}>
|
||||
{label}
|
||||
</span>
|
||||
{badge && (
|
||||
<span className={cn(
|
||||
"ml-auto text-xs px-2 py-0.5 rounded-full",
|
||||
isActive
|
||||
? "bg-brand-500/20 text-brand-500"
|
||||
: "bg-accent text-muted-foreground"
|
||||
)}>
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground/80">
|
||||
{description}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { AIEnhancementSection } from "./ai-enhancement-section";
|
||||
@@ -58,9 +58,8 @@ export function ApiKeysSection() {
|
||||
|
||||
return (
|
||||
<div
|
||||
id="api-keys"
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
||||
"rounded-2xl overflow-hidden",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
|
||||
@@ -18,9 +18,8 @@ export function AppearanceSection({
|
||||
}: AppearanceSectionProps) {
|
||||
return (
|
||||
<div
|
||||
id="appearance"
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
||||
"rounded-2xl overflow-hidden",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Volume2, VolumeX } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AudioSectionProps {
|
||||
muteDoneSound: boolean;
|
||||
onMuteDoneSoundChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function AudioSection({
|
||||
muteDoneSound,
|
||||
onMuteDoneSoundChange,
|
||||
}: AudioSectionProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Volume2 className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||
Audio
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Configure audio and notification settings.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
<Checkbox
|
||||
id="mute-done-sound"
|
||||
checked={muteDoneSound}
|
||||
onCheckedChange={onMuteDoneSoundChange}
|
||||
className="mt-1"
|
||||
data-testid="mute-done-sound-checkbox"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label
|
||||
htmlFor="mute-done-sound"
|
||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||
>
|
||||
<VolumeX className="w-4 h-4 text-brand-500" />
|
||||
Mute notification sound when agents complete
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
When enabled, disables the "ding" sound that plays when
|
||||
an agent completes a feature. The feature will still move to the
|
||||
completed column, but without audio notification.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,9 +23,8 @@ export function ClaudeCliStatus({
|
||||
|
||||
return (
|
||||
<div
|
||||
id="claude"
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
||||
"rounded-2xl overflow-hidden",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Project } from "@/lib/electron";
|
||||
import type { NavigationItem } from "../config/navigation";
|
||||
import type { SettingsViewId } from "../hooks/use-settings-view";
|
||||
|
||||
interface SettingsNavigationProps {
|
||||
navItems: NavigationItem[];
|
||||
activeSection: string;
|
||||
activeSection: SettingsViewId;
|
||||
currentProject: Project | null;
|
||||
onNavigate: (sectionId: string) => void;
|
||||
onNavigate: (sectionId: SettingsViewId) => void;
|
||||
}
|
||||
|
||||
export function SettingsNavigation({
|
||||
|
||||
@@ -3,14 +3,16 @@ import {
|
||||
Key,
|
||||
Terminal,
|
||||
Palette,
|
||||
LayoutGrid,
|
||||
Settings2,
|
||||
Volume2,
|
||||
FlaskConical,
|
||||
Trash2,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import type { SettingsViewId } from "../hooks/use-settings-view";
|
||||
|
||||
export interface NavigationItem {
|
||||
id: string;
|
||||
id: SettingsViewId;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
@@ -19,9 +21,10 @@ export interface NavigationItem {
|
||||
export const NAV_ITEMS: NavigationItem[] = [
|
||||
{ id: "api-keys", label: "API Keys", icon: Key },
|
||||
{ id: "claude", label: "Claude", icon: Terminal },
|
||||
{ id: "ai-enhancement", label: "AI Enhancement", icon: Sparkles },
|
||||
{ id: "appearance", label: "Appearance", icon: Palette },
|
||||
{ id: "kanban", label: "Kanban Display", icon: LayoutGrid },
|
||||
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
|
||||
{ id: "audio", label: "Audio", icon: Volume2 },
|
||||
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
|
||||
{ id: "danger", label: "Danger Zone", icon: Trash2 },
|
||||
];
|
||||
|
||||
@@ -16,9 +16,8 @@ export function DangerZoneSection({
|
||||
|
||||
return (
|
||||
<div
|
||||
id="danger"
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
||||
"rounded-2xl overflow-hidden",
|
||||
"border border-destructive/30",
|
||||
"bg-gradient-to-br from-destructive/5 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-destructive/5"
|
||||
|
||||
@@ -22,9 +22,8 @@ export function FeatureDefaultsSection({
|
||||
}: FeatureDefaultsSectionProps) {
|
||||
return (
|
||||
<div
|
||||
id="defaults"
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
||||
"rounded-2xl overflow-hidden",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useCliStatus } from "./use-cli-status";
|
||||
export { useSettingsView, type SettingsViewId } from "./use-settings-view";
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
export type SettingsViewId =
|
||||
| "api-keys"
|
||||
| "claude"
|
||||
| "ai-enhancement"
|
||||
| "appearance"
|
||||
| "keyboard"
|
||||
| "audio"
|
||||
| "defaults"
|
||||
| "danger";
|
||||
|
||||
interface UseSettingsViewOptions {
|
||||
initialView?: SettingsViewId;
|
||||
}
|
||||
|
||||
export function useSettingsView({
|
||||
initialView = "api-keys",
|
||||
}: UseSettingsViewOptions = {}) {
|
||||
const [activeView, setActiveView] = useState<SettingsViewId>(initialView);
|
||||
|
||||
const navigateTo = useCallback((viewId: SettingsViewId) => {
|
||||
setActiveView(viewId);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
activeView,
|
||||
navigateTo,
|
||||
};
|
||||
}
|
||||
@@ -11,9 +11,8 @@ export function KeyboardShortcutsSection({
|
||||
}: KeyboardShortcutsSectionProps) {
|
||||
return (
|
||||
<div
|
||||
id="keyboard"
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
||||
"rounded-2xl overflow-hidden",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
|
||||
@@ -319,6 +319,13 @@ export interface ElectronAPI {
|
||||
autoMode?: AutoModeAPI;
|
||||
features?: FeaturesAPI;
|
||||
runningAgents?: RunningAgentsAPI;
|
||||
enhancePrompt?: {
|
||||
enhance: (originalText: string, enhancementMode: string, model?: string) => Promise<{
|
||||
success: boolean;
|
||||
enhancedText?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
};
|
||||
setup?: {
|
||||
getClaudeStatus: () => Promise<{
|
||||
success: boolean;
|
||||
|
||||
@@ -58,6 +58,12 @@ type EventType =
|
||||
|
||||
type EventCallback = (payload: unknown) => void;
|
||||
|
||||
interface EnhancePromptResult {
|
||||
success: boolean;
|
||||
enhancedText?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP API Client that implements ElectronAPI interface
|
||||
*/
|
||||
@@ -550,6 +556,20 @@ export class HttpApiClient implements ElectronAPI {
|
||||
},
|
||||
};
|
||||
|
||||
// Enhance Prompt API
|
||||
enhancePrompt = {
|
||||
enhance: (
|
||||
originalText: string,
|
||||
enhancementMode: string,
|
||||
model?: string
|
||||
): Promise<EnhancePromptResult> =>
|
||||
this.post("/api/enhance-prompt", {
|
||||
originalText,
|
||||
enhancementMode,
|
||||
model,
|
||||
}),
|
||||
};
|
||||
|
||||
// Worktree API
|
||||
worktree: WorktreeAPI = {
|
||||
revertFeature: (projectPath: string, featureId: string) =>
|
||||
|
||||
@@ -292,6 +292,7 @@ export interface Feature {
|
||||
thinkingLevel?: ThinkingLevel; // Thinking level for extended thinking (defaults to none)
|
||||
error?: string; // Error message if the agent errored during processing
|
||||
priority?: number; // Priority: 1 = high, 2 = medium, 3 = low
|
||||
dependencies?: string[]; // Array of feature IDs this feature depends on
|
||||
// Worktree info - set when a feature is being worked on in an isolated git worktree
|
||||
worktreePath?: string; // Path to the worktree directory
|
||||
branchName?: string; // Name of the feature branch
|
||||
@@ -422,6 +423,9 @@ export interface AppState {
|
||||
// Audio Settings
|
||||
muteDoneSound: boolean; // When true, mute the notification sound when agents complete (default: false)
|
||||
|
||||
// Enhancement Model Settings
|
||||
enhancementModel: AgentModel; // Model used for feature enhancement (default: sonnet)
|
||||
|
||||
// Project Analysis
|
||||
projectAnalysis: ProjectAnalysis | null;
|
||||
isAnalyzing: boolean;
|
||||
@@ -609,6 +613,9 @@ export interface AppActions {
|
||||
// Audio Settings actions
|
||||
setMuteDoneSound: (muted: boolean) => void;
|
||||
|
||||
// Enhancement Model actions
|
||||
setEnhancementModel: (model: AgentModel) => void;
|
||||
|
||||
// AI Profile actions
|
||||
addAIProfile: (profile: Omit<AIProfile, "id">) => void;
|
||||
updateAIProfile: (id: string, updates: Partial<AIProfile>) => void;
|
||||
@@ -748,6 +755,7 @@ const initialState: AppState = {
|
||||
showProfilesOnly: false, // Default to showing all options (not profiles only)
|
||||
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts
|
||||
muteDoneSound: false, // Default to sound enabled (not muted)
|
||||
enhancementModel: "sonnet", // Default to sonnet for feature enhancement
|
||||
aiProfiles: DEFAULT_AI_PROFILES,
|
||||
projectAnalysis: null,
|
||||
isAnalyzing: false,
|
||||
@@ -1393,6 +1401,9 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
// Audio Settings actions
|
||||
setMuteDoneSound: (muted) => set({ muteDoneSound: muted }),
|
||||
|
||||
// Enhancement Model actions
|
||||
setEnhancementModel: (model) => set({ enhancementModel: model }),
|
||||
|
||||
// AI Profile actions
|
||||
addAIProfile: (profile) => {
|
||||
const id = `profile-${Date.now()}-${Math.random()
|
||||
@@ -2226,6 +2237,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
showProfilesOnly: state.showProfilesOnly,
|
||||
keyboardShortcuts: state.keyboardShortcuts,
|
||||
muteDoneSound: state.muteDoneSound,
|
||||
enhancementModel: state.enhancementModel,
|
||||
// Profiles and sessions
|
||||
aiProfiles: state.aiProfiles,
|
||||
chatSessions: state.chatSessions,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { createAgentRoutes } from "./routes/agent/index.js";
|
||||
import { createSessionsRoutes } from "./routes/sessions/index.js";
|
||||
import { createFeaturesRoutes } from "./routes/features/index.js";
|
||||
import { createAutoModeRoutes } from "./routes/auto-mode/index.js";
|
||||
import { createEnhancePromptRoutes } from "./routes/enhance-prompt/index.js";
|
||||
import { createWorktreeRoutes } from "./routes/worktree/index.js";
|
||||
import { createGitRoutes } from "./routes/git/index.js";
|
||||
import { createSetupRoutes } from "./routes/setup/index.js";
|
||||
@@ -125,6 +126,7 @@ app.use("/api/agent", createAgentRoutes(agentService, events));
|
||||
app.use("/api/sessions", createSessionsRoutes(agentService));
|
||||
app.use("/api/features", createFeaturesRoutes(featureLoader));
|
||||
app.use("/api/auto-mode", createAutoModeRoutes(autoModeService));
|
||||
app.use("/api/enhance-prompt", createEnhancePromptRoutes());
|
||||
app.use("/api/worktree", createWorktreeRoutes());
|
||||
app.use("/api/git", createGitRoutes());
|
||||
app.use("/api/setup", createSetupRoutes());
|
||||
|
||||
456
apps/server/src/lib/enhancement-prompts.ts
Normal file
456
apps/server/src/lib/enhancement-prompts.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
/**
|
||||
* Enhancement Prompts Library - AI-powered text enhancement for task descriptions
|
||||
*
|
||||
* Provides prompt templates and utilities for enhancing user-written task descriptions:
|
||||
* - Improve: Transform vague requests into clear, actionable tasks
|
||||
* - Technical: Add implementation details and technical specifications
|
||||
* - Simplify: Make verbose descriptions concise and focused
|
||||
* - Acceptance: Add testable acceptance criteria
|
||||
*
|
||||
* Uses chain-of-thought prompting with few-shot examples for consistent results.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Available enhancement modes for transforming task descriptions
|
||||
*/
|
||||
export type EnhancementMode = "improve" | "technical" | "simplify" | "acceptance";
|
||||
|
||||
/**
|
||||
* Example input/output pair for few-shot learning
|
||||
*/
|
||||
export interface EnhancementExample {
|
||||
input: string;
|
||||
output: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* System prompt for the "improve" enhancement mode.
|
||||
* Transforms vague or unclear requests into clear, actionable task descriptions.
|
||||
*/
|
||||
export const IMPROVE_SYSTEM_PROMPT = `You are an expert at transforming vague, unclear, or incomplete task descriptions into clear, actionable specifications.
|
||||
|
||||
Your task is to take a user's rough description and improve it by:
|
||||
|
||||
1. ANALYZE the input:
|
||||
- Identify the core intent behind the request
|
||||
- Note any ambiguities or missing details
|
||||
- Determine what success would look like
|
||||
|
||||
2. CLARIFY the scope:
|
||||
- Define clear boundaries for the task
|
||||
- Identify implicit requirements
|
||||
- Add relevant context that may be assumed
|
||||
|
||||
3. STRUCTURE the output:
|
||||
- Write a clear, actionable title
|
||||
- Provide a concise description of what needs to be done
|
||||
- Break down into specific sub-tasks if appropriate
|
||||
|
||||
4. ENHANCE with details:
|
||||
- Add specific, measurable outcomes where possible
|
||||
- Include edge cases to consider
|
||||
- Note any dependencies or prerequisites
|
||||
|
||||
Output ONLY the improved task description. Do not include explanations, markdown formatting, or meta-commentary about your changes.`;
|
||||
|
||||
/**
|
||||
* System prompt for the "technical" enhancement mode.
|
||||
* Adds implementation details and technical specifications.
|
||||
*/
|
||||
export const TECHNICAL_SYSTEM_PROMPT = `You are a senior software engineer skilled at adding technical depth to feature descriptions.
|
||||
|
||||
Your task is to enhance a task description with technical implementation details:
|
||||
|
||||
1. ANALYZE the requirement:
|
||||
- Understand the functional goal
|
||||
- Identify the technical domain (frontend, backend, database, etc.)
|
||||
- Consider the likely tech stack based on context
|
||||
|
||||
2. ADD technical specifications:
|
||||
- Suggest specific technologies, libraries, or patterns
|
||||
- Define API contracts or data structures if relevant
|
||||
- Note performance considerations
|
||||
- Identify security implications
|
||||
|
||||
3. OUTLINE implementation approach:
|
||||
- Break down into technical sub-tasks
|
||||
- Suggest file structure or component organization
|
||||
- Note integration points with existing systems
|
||||
|
||||
4. CONSIDER edge cases:
|
||||
- Error handling requirements
|
||||
- Loading and empty states
|
||||
- Boundary conditions
|
||||
|
||||
Output ONLY the enhanced technical description. Keep it concise but comprehensive. Do not include explanations about your reasoning.`;
|
||||
|
||||
/**
|
||||
* System prompt for the "simplify" enhancement mode.
|
||||
* Makes verbose descriptions concise and focused.
|
||||
*/
|
||||
export const SIMPLIFY_SYSTEM_PROMPT = `You are an expert editor who excels at making verbose text concise without losing meaning.
|
||||
|
||||
Your task is to simplify a task description while preserving essential information:
|
||||
|
||||
1. IDENTIFY the core message:
|
||||
- Extract the primary goal or requirement
|
||||
- Note truly essential details
|
||||
- Separate nice-to-have from must-have information
|
||||
|
||||
2. ELIMINATE redundancy:
|
||||
- Remove repeated information
|
||||
- Cut unnecessary qualifiers and hedging language
|
||||
- Remove filler words and phrases
|
||||
|
||||
3. CONSOLIDATE related points:
|
||||
- Merge overlapping requirements
|
||||
- Group related items together
|
||||
- Use concise language
|
||||
|
||||
4. PRESERVE critical details:
|
||||
- Keep specific technical requirements
|
||||
- Retain important constraints
|
||||
- Maintain actionable specifics
|
||||
|
||||
Output ONLY the simplified description. Aim for 30-50% reduction in length while keeping all essential information. Do not explain your changes.`;
|
||||
|
||||
/**
|
||||
* System prompt for the "acceptance" enhancement mode.
|
||||
* Adds testable acceptance criteria to task descriptions.
|
||||
*/
|
||||
export const ACCEPTANCE_SYSTEM_PROMPT = `You are a QA specialist skilled at defining testable acceptance criteria for software features.
|
||||
|
||||
Your task is to enhance a task description by adding clear acceptance criteria:
|
||||
|
||||
1. UNDERSTAND the feature:
|
||||
- Identify all user-facing behaviors
|
||||
- Note system state changes
|
||||
- Consider different user roles or scenarios
|
||||
|
||||
2. DEFINE acceptance criteria using Given-When-Then format:
|
||||
- Given: The initial context or preconditions
|
||||
- When: The action or trigger
|
||||
- Then: The expected outcome
|
||||
|
||||
3. COVER key scenarios:
|
||||
- Happy path (successful completion)
|
||||
- Edge cases (boundary conditions)
|
||||
- Error scenarios (what should NOT happen)
|
||||
- Performance requirements if relevant
|
||||
|
||||
4. MAKE criteria testable:
|
||||
- Use specific, measurable outcomes
|
||||
- Avoid vague terms like "quickly" or "easily"
|
||||
- Include specific values where applicable
|
||||
|
||||
Output the original description followed by a clear "Acceptance Criteria:" section with numbered, testable criteria. Do not include explanations about your process.`;
|
||||
|
||||
/**
|
||||
* Few-shot examples for the "improve" enhancement mode
|
||||
*/
|
||||
export const IMPROVE_EXAMPLES: EnhancementExample[] = [
|
||||
{
|
||||
input: "add dark mode",
|
||||
output: `Add Dark Mode Theme Support
|
||||
|
||||
Implement a dark mode option that allows users to switch between light and dark themes throughout the application.
|
||||
|
||||
Requirements:
|
||||
- Add a theme toggle switch in the settings area
|
||||
- Store user's theme preference persistently
|
||||
- Apply dark theme colors to all UI components
|
||||
- Respect system preference as default if user hasn't set a preference
|
||||
- Ensure smooth transition when switching themes`,
|
||||
},
|
||||
{
|
||||
input: "fix the login bug",
|
||||
output: `Fix Login Authentication Flow
|
||||
|
||||
Investigate and resolve issues preventing users from successfully logging in to the application.
|
||||
|
||||
Requirements:
|
||||
- Identify the specific failure point in the login process
|
||||
- Ensure proper credential validation
|
||||
- Handle error states with clear user feedback
|
||||
- Verify session token is correctly stored after successful login
|
||||
- Test with various credential combinations (valid, invalid, edge cases)`,
|
||||
},
|
||||
{
|
||||
input: "make it faster",
|
||||
output: `Improve Application Performance
|
||||
|
||||
Optimize application loading and response times to enhance user experience.
|
||||
|
||||
Requirements:
|
||||
- Profile current performance to identify bottlenecks
|
||||
- Implement code splitting and lazy loading for routes/components
|
||||
- Optimize database queries and add appropriate indexes
|
||||
- Add caching for frequently accessed data
|
||||
- Set performance budget targets (e.g., LCP < 2.5s, FID < 100ms)
|
||||
- Measure and document improvements`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Few-shot examples for the "technical" enhancement mode
|
||||
*/
|
||||
export const TECHNICAL_EXAMPLES: EnhancementExample[] = [
|
||||
{
|
||||
input: "Add user profile page",
|
||||
output: `Add User Profile Page
|
||||
|
||||
Create a dedicated profile page for viewing and editing user information.
|
||||
|
||||
Technical Implementation:
|
||||
- Frontend: React component at /profile route with form validation
|
||||
- API Endpoint: GET/PUT /api/users/:id for fetching and updating profile
|
||||
- Data Model: Extend User schema with profile fields (avatar, bio, preferences)
|
||||
- File Upload: Integrate cloud storage for avatar images with client-side resizing
|
||||
- State Management: Use optimistic updates for better UX
|
||||
- Validation: Server-side validation matching client-side rules
|
||||
|
||||
Components:
|
||||
- ProfileHeader (avatar, name, edit button)
|
||||
- ProfileForm (editable fields with validation)
|
||||
- AvatarUploader (drag-drop with preview)
|
||||
|
||||
Security: Ensure users can only edit their own profile (auth middleware)`,
|
||||
},
|
||||
{
|
||||
input: "Add search functionality",
|
||||
output: `Add Search Functionality
|
||||
|
||||
Implement full-text search across application content.
|
||||
|
||||
Technical Implementation:
|
||||
- Search Engine: Use Elasticsearch or PostgreSQL full-text search
|
||||
- API: GET /api/search?q={query}&type={type}&page={page}
|
||||
- Indexing: Create search index with relevant fields, update on content changes
|
||||
- Frontend: Debounced search input (300ms) with typeahead suggestions
|
||||
- Results: Paginated results with highlighted matching text
|
||||
|
||||
Architecture:
|
||||
- SearchService class handling query building and execution
|
||||
- SearchIndex worker for background indexing
|
||||
- SearchResults component with filtering and sorting
|
||||
|
||||
Performance:
|
||||
- Implement search result caching (Redis, 5-minute TTL)
|
||||
- Limit results per page (20 items)
|
||||
- Add query complexity limits to prevent abuse`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Few-shot examples for the "simplify" enhancement mode
|
||||
*/
|
||||
export const SIMPLIFY_EXAMPLES: EnhancementExample[] = [
|
||||
{
|
||||
input: `We need to implement a feature that would allow our users to be able to export their data in various different formats. The formats we are thinking about supporting include CSV which is commonly used for spreadsheet applications, and also JSON which is good for developers, and possibly PDF for people who want a printable version. The user should be able to go somewhere in the UI and select which format they want and then download their data in that format. We should probably also show some kind of progress indicator while the export is happening since it might take a while for large datasets.`,
|
||||
output: `Add Data Export Feature
|
||||
|
||||
Allow users to export their data in CSV, JSON, or PDF format.
|
||||
|
||||
Requirements:
|
||||
- Add export button/menu in the UI
|
||||
- Support three formats: CSV (spreadsheets), JSON (developers), PDF (printing)
|
||||
- Show progress indicator during export
|
||||
- Handle large datasets gracefully`,
|
||||
},
|
||||
{
|
||||
input: `The current notification system is not working very well and users are complaining about it. Sometimes they don't receive notifications at all, and other times they receive too many notifications and feel overwhelmed. We also have had reports that the notifications are not appearing correctly on mobile devices. Additionally, some users have requested the ability to customize which types of notifications they receive. We should look into all of these issues and make the notification system work better overall.`,
|
||||
output: `Fix and Improve Notification System
|
||||
|
||||
Address reliability issues and add user controls for notifications.
|
||||
|
||||
Issues to fix:
|
||||
- Missing notifications (delivery reliability)
|
||||
- Notification overload (implement batching/digest)
|
||||
- Mobile display problems
|
||||
|
||||
Enhancements:
|
||||
- Add notification preferences (per-type controls)
|
||||
- Test across devices and platforms`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Few-shot examples for the "acceptance" enhancement mode
|
||||
*/
|
||||
export const ACCEPTANCE_EXAMPLES: EnhancementExample[] = [
|
||||
{
|
||||
input: "Add password reset functionality",
|
||||
output: `Add Password Reset Functionality
|
||||
|
||||
Allow users to reset their password via email when they forget it.
|
||||
|
||||
Acceptance Criteria:
|
||||
|
||||
1. Given a user is on the login page, when they click "Forgot Password", then they should see a password reset form requesting their email.
|
||||
|
||||
2. Given a user enters a valid registered email, when they submit the form, then they should receive a password reset email within 2 minutes.
|
||||
|
||||
3. Given a user clicks the reset link in their email, when the link is less than 1 hour old, then they should see a form to enter a new password.
|
||||
|
||||
4. Given a user enters a new valid password (min 8 chars, 1 number, 1 special char), when they submit, then their password should be updated and they should be redirected to login.
|
||||
|
||||
5. Given a user enters an unregistered email, when they submit the reset form, then they should see the same success message (security: don't reveal which emails exist).
|
||||
|
||||
6. Given a reset link older than 1 hour, when a user clicks it, then they should see an expiration message with option to request a new link.`,
|
||||
},
|
||||
{
|
||||
input: "Shopping cart checkout",
|
||||
output: `Shopping Cart Checkout
|
||||
|
||||
Implement the checkout flow for purchasing items in the shopping cart.
|
||||
|
||||
Acceptance Criteria:
|
||||
|
||||
1. Given a user has items in their cart, when they click "Checkout", then they should see an order summary with item details and total price.
|
||||
|
||||
2. Given a user is on the checkout page, when they enter valid shipping information, then the form should validate in real-time and show estimated delivery date.
|
||||
|
||||
3. Given valid shipping info is entered, when the user proceeds to payment, then they should see available payment methods (credit card, PayPal).
|
||||
|
||||
4. Given valid payment details are entered, when the user confirms the order, then the payment should be processed and order confirmation displayed within 5 seconds.
|
||||
|
||||
5. Given a successful order, when confirmation is shown, then the user should receive an email receipt and their cart should be emptied.
|
||||
|
||||
6. Given a payment failure, when the error occurs, then the user should see a clear error message and their cart should remain intact.
|
||||
|
||||
7. Given the user closes the browser during checkout, when they return, then their cart contents should still be available.`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Map of enhancement modes to their system prompts
|
||||
*/
|
||||
const SYSTEM_PROMPTS: Record<EnhancementMode, string> = {
|
||||
improve: IMPROVE_SYSTEM_PROMPT,
|
||||
technical: TECHNICAL_SYSTEM_PROMPT,
|
||||
simplify: SIMPLIFY_SYSTEM_PROMPT,
|
||||
acceptance: ACCEPTANCE_SYSTEM_PROMPT,
|
||||
};
|
||||
|
||||
/**
|
||||
* Map of enhancement modes to their few-shot examples
|
||||
*/
|
||||
const EXAMPLES: Record<EnhancementMode, EnhancementExample[]> = {
|
||||
improve: IMPROVE_EXAMPLES,
|
||||
technical: TECHNICAL_EXAMPLES,
|
||||
simplify: SIMPLIFY_EXAMPLES,
|
||||
acceptance: ACCEPTANCE_EXAMPLES,
|
||||
};
|
||||
|
||||
/**
|
||||
* Enhancement prompt configuration returned by getEnhancementPrompt
|
||||
*/
|
||||
export interface EnhancementPromptConfig {
|
||||
/** System prompt for the enhancement mode */
|
||||
systemPrompt: string;
|
||||
/** Description of what this mode does */
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Descriptions for each enhancement mode
|
||||
*/
|
||||
const MODE_DESCRIPTIONS: Record<EnhancementMode, string> = {
|
||||
improve: "Transform vague requests into clear, actionable task descriptions",
|
||||
technical: "Add implementation details and technical specifications",
|
||||
simplify: "Make verbose descriptions concise and focused",
|
||||
acceptance: "Add testable acceptance criteria to task descriptions",
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the enhancement prompt configuration for a given mode
|
||||
*
|
||||
* @param mode - The enhancement mode (falls back to 'improve' if invalid)
|
||||
* @returns The enhancement prompt configuration
|
||||
*/
|
||||
export function getEnhancementPrompt(mode: string): EnhancementPromptConfig {
|
||||
const normalizedMode = mode.toLowerCase() as EnhancementMode;
|
||||
const validMode = normalizedMode in SYSTEM_PROMPTS ? normalizedMode : "improve";
|
||||
|
||||
return {
|
||||
systemPrompt: SYSTEM_PROMPTS[validMode],
|
||||
description: MODE_DESCRIPTIONS[validMode],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the system prompt for a specific enhancement mode
|
||||
*
|
||||
* @param mode - The enhancement mode to get the prompt for
|
||||
* @returns The system prompt string
|
||||
*/
|
||||
export function getSystemPrompt(mode: EnhancementMode): string {
|
||||
return SYSTEM_PROMPTS[mode];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the few-shot examples for a specific enhancement mode
|
||||
*
|
||||
* @param mode - The enhancement mode to get examples for
|
||||
* @returns Array of input/output example pairs
|
||||
*/
|
||||
export function getExamples(mode: EnhancementMode): EnhancementExample[] {
|
||||
return EXAMPLES[mode];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a user prompt for enhancement with optional few-shot examples
|
||||
*
|
||||
* @param mode - The enhancement mode
|
||||
* @param text - The text to enhance
|
||||
* @param includeExamples - Whether to include few-shot examples (default: true)
|
||||
* @returns The formatted user prompt string
|
||||
*/
|
||||
export function buildUserPrompt(
|
||||
mode: EnhancementMode,
|
||||
text: string,
|
||||
includeExamples: boolean = true
|
||||
): string {
|
||||
const examples = includeExamples ? getExamples(mode) : [];
|
||||
|
||||
if (examples.length === 0) {
|
||||
return `Please enhance the following task description:\n\n${text}`;
|
||||
}
|
||||
|
||||
// Build few-shot examples section
|
||||
const examplesSection = examples
|
||||
.map(
|
||||
(example, index) =>
|
||||
`Example ${index + 1}:\nInput: ${example.input}\nOutput: ${example.output}`
|
||||
)
|
||||
.join("\n\n---\n\n");
|
||||
|
||||
return `Here are some examples of how to enhance task descriptions:
|
||||
|
||||
${examplesSection}
|
||||
|
||||
---
|
||||
|
||||
Now, please enhance the following task description:
|
||||
|
||||
${text}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a mode is a valid enhancement mode
|
||||
*
|
||||
* @param mode - The mode to check
|
||||
* @returns True if the mode is valid
|
||||
*/
|
||||
export function isValidEnhancementMode(mode: string): mode is EnhancementMode {
|
||||
return mode in SYSTEM_PROMPTS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available enhancement modes
|
||||
*
|
||||
* @returns Array of available enhancement mode names
|
||||
*/
|
||||
export function getAvailableEnhancementModes(): EnhancementMode[] {
|
||||
return Object.keys(SYSTEM_PROMPTS) as EnhancementMode[];
|
||||
}
|
||||
@@ -56,17 +56,19 @@ ${spec}
|
||||
Generate a prioritized list of implementable features. For each feature provide:
|
||||
|
||||
1. **id**: A unique lowercase-hyphenated identifier
|
||||
2. **title**: Short descriptive title
|
||||
3. **description**: What this feature does (2-3 sentences)
|
||||
4. **priority**: 1 (high), 2 (medium), or 3 (low)
|
||||
5. **complexity**: "simple", "moderate", or "complex"
|
||||
6. **dependencies**: Array of feature IDs this depends on (can be empty)
|
||||
2. **category**: Functional category (e.g., "Core", "UI", "API", "Authentication", "Database")
|
||||
3. **title**: Short descriptive title
|
||||
4. **description**: What this feature does (2-3 sentences)
|
||||
5. **priority**: 1 (high), 2 (medium), or 3 (low)
|
||||
6. **complexity**: "simple", "moderate", or "complex"
|
||||
7. **dependencies**: Array of feature IDs this depends on (can be empty)
|
||||
|
||||
Format as JSON:
|
||||
{
|
||||
"features": [
|
||||
{
|
||||
"id": "feature-id",
|
||||
"category": "Feature Category",
|
||||
"title": "Feature Title",
|
||||
"description": "What it does",
|
||||
"priority": 1,
|
||||
|
||||
@@ -54,6 +54,7 @@ export async function parseAndCreateFeatures(
|
||||
|
||||
const featureData = {
|
||||
id: feature.id,
|
||||
category: feature.category || "Uncategorized",
|
||||
title: feature.title,
|
||||
description: feature.description,
|
||||
status: "backlog", // Features go to backlog - user must manually start them
|
||||
|
||||
@@ -3,9 +3,304 @@
|
||||
*/
|
||||
|
||||
import { createLogger } from "../lib/logger.js";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
type Logger = ReturnType<typeof createLogger>;
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const logger = createLogger("Common");
|
||||
|
||||
// Max file size for generating synthetic diffs (1MB)
|
||||
const MAX_SYNTHETIC_DIFF_SIZE = 1024 * 1024;
|
||||
|
||||
// Binary file extensions to skip
|
||||
const BINARY_EXTENSIONS = new Set([
|
||||
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp", ".svg",
|
||||
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
|
||||
".zip", ".tar", ".gz", ".rar", ".7z",
|
||||
".exe", ".dll", ".so", ".dylib",
|
||||
".mp3", ".mp4", ".wav", ".avi", ".mov", ".mkv",
|
||||
".ttf", ".otf", ".woff", ".woff2", ".eot",
|
||||
".db", ".sqlite", ".sqlite3",
|
||||
".pyc", ".pyo", ".class", ".o", ".obj",
|
||||
]);
|
||||
|
||||
// Status map for git status codes
|
||||
const GIT_STATUS_MAP: Record<string, string> = {
|
||||
M: "Modified",
|
||||
A: "Added",
|
||||
D: "Deleted",
|
||||
R: "Renamed",
|
||||
C: "Copied",
|
||||
U: "Updated",
|
||||
"?": "Untracked",
|
||||
};
|
||||
|
||||
/**
|
||||
* File status interface for git status results
|
||||
*/
|
||||
export interface FileStatus {
|
||||
status: string;
|
||||
path: string;
|
||||
statusText: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is likely binary based on extension
|
||||
*/
|
||||
function isBinaryFile(filePath: string): boolean {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
return BINARY_EXTENSIONS.has(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path is a git repository
|
||||
*/
|
||||
export async function isGitRepo(repoPath: string): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git rev-parse --is-inside-work-tree", { cwd: repoPath });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the output of `git status --porcelain` into FileStatus array
|
||||
*/
|
||||
export function parseGitStatus(statusOutput: string): FileStatus[] {
|
||||
return statusOutput
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const statusChar = line[0];
|
||||
const filePath = line.slice(3);
|
||||
return {
|
||||
status: statusChar,
|
||||
path: filePath,
|
||||
statusText: GIT_STATUS_MAP[statusChar] || "Unknown",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a synthetic unified diff for an untracked (new) file
|
||||
* This is needed because `git diff HEAD` doesn't include untracked files
|
||||
*/
|
||||
export async function generateSyntheticDiffForNewFile(
|
||||
basePath: string,
|
||||
relativePath: string
|
||||
): Promise<string> {
|
||||
const fullPath = path.join(basePath, relativePath);
|
||||
|
||||
try {
|
||||
// Check if it's a binary file
|
||||
if (isBinaryFile(relativePath)) {
|
||||
return `diff --git a/${relativePath} b/${relativePath}
|
||||
new file mode 100644
|
||||
index 0000000..0000000
|
||||
Binary file ${relativePath} added
|
||||
`;
|
||||
}
|
||||
|
||||
// Get file stats to check size
|
||||
const stats = await fs.stat(fullPath);
|
||||
if (stats.size > MAX_SYNTHETIC_DIFF_SIZE) {
|
||||
const sizeKB = Math.round(stats.size / 1024);
|
||||
return `diff --git a/${relativePath} b/${relativePath}
|
||||
new file mode 100644
|
||||
index 0000000..0000000
|
||||
--- /dev/null
|
||||
+++ b/${relativePath}
|
||||
@@ -0,0 +1 @@
|
||||
+[File too large to display: ${sizeKB}KB]
|
||||
`;
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const content = await fs.readFile(fullPath, "utf-8");
|
||||
const hasTrailingNewline = content.endsWith("\n");
|
||||
const lines = content.split("\n");
|
||||
|
||||
// Remove trailing empty line if the file ends with newline
|
||||
if (lines.length > 0 && lines.at(-1) === "") {
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
// Generate diff format
|
||||
const lineCount = lines.length;
|
||||
const addedLines = lines.map(line => `+${line}`).join("\n");
|
||||
|
||||
let diff = `diff --git a/${relativePath} b/${relativePath}
|
||||
new file mode 100644
|
||||
index 0000000..0000000
|
||||
--- /dev/null
|
||||
+++ b/${relativePath}
|
||||
@@ -0,0 +1,${lineCount} @@
|
||||
${addedLines}`;
|
||||
|
||||
// Add "No newline at end of file" indicator if needed
|
||||
if (!hasTrailingNewline && content.length > 0) {
|
||||
diff += "\n\\ No newline at end of file";
|
||||
}
|
||||
|
||||
return diff + "\n";
|
||||
} catch (error) {
|
||||
// Log the error for debugging
|
||||
logger.error(`Failed to generate synthetic diff for ${fullPath}:`, error);
|
||||
// Return a placeholder diff
|
||||
return `diff --git a/${relativePath} b/${relativePath}
|
||||
new file mode 100644
|
||||
index 0000000..0000000
|
||||
--- /dev/null
|
||||
+++ b/${relativePath}
|
||||
@@ -0,0 +1 @@
|
||||
+[Unable to read file content]
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate synthetic diffs for all untracked files and combine with existing diff
|
||||
*/
|
||||
export async function appendUntrackedFileDiffs(
|
||||
basePath: string,
|
||||
existingDiff: string,
|
||||
files: Array<{ status: string; path: string }>
|
||||
): Promise<string> {
|
||||
// Find untracked files (status "?")
|
||||
const untrackedFiles = files.filter(f => f.status === "?");
|
||||
|
||||
if (untrackedFiles.length === 0) {
|
||||
return existingDiff;
|
||||
}
|
||||
|
||||
// Generate synthetic diffs for each untracked file
|
||||
const syntheticDiffs = await Promise.all(
|
||||
untrackedFiles.map(f => generateSyntheticDiffForNewFile(basePath, f.path))
|
||||
);
|
||||
|
||||
// Combine existing diff with synthetic diffs
|
||||
const combinedDiff = existingDiff + syntheticDiffs.join("");
|
||||
|
||||
return combinedDiff;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all files in a directory recursively (for non-git repositories)
|
||||
* Excludes hidden files/folders and common build artifacts
|
||||
*/
|
||||
export async function listAllFilesInDirectory(
|
||||
basePath: string,
|
||||
relativePath: string = ""
|
||||
): Promise<string[]> {
|
||||
const files: string[] = [];
|
||||
const fullPath = path.join(basePath, relativePath);
|
||||
|
||||
// Directories to skip
|
||||
const skipDirs = new Set([
|
||||
"node_modules", ".git", ".automaker", "dist", "build",
|
||||
".next", ".nuxt", "__pycache__", ".cache", "coverage"
|
||||
]);
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
// Skip hidden files/folders (except we want to allow some)
|
||||
if (entry.name.startsWith(".") && entry.name !== ".env") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entryRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (!skipDirs.has(entry.name)) {
|
||||
const subFiles = await listAllFilesInDirectory(basePath, entryRelPath);
|
||||
files.push(...subFiles);
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
files.push(entryRelPath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Log the error to help diagnose file system issues
|
||||
logger.error(`Error reading directory ${fullPath}:`, error);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate diffs for all files in a non-git directory
|
||||
* Treats all files as "new" files
|
||||
*/
|
||||
export async function generateDiffsForNonGitDirectory(
|
||||
basePath: string
|
||||
): Promise<{ diff: string; files: FileStatus[] }> {
|
||||
const allFiles = await listAllFilesInDirectory(basePath);
|
||||
|
||||
const files: FileStatus[] = allFiles.map(filePath => ({
|
||||
status: "?",
|
||||
path: filePath,
|
||||
statusText: "New",
|
||||
}));
|
||||
|
||||
// Generate synthetic diffs for all files
|
||||
const syntheticDiffs = await Promise.all(
|
||||
files.map(f => generateSyntheticDiffForNewFile(basePath, f.path))
|
||||
);
|
||||
|
||||
return {
|
||||
diff: syntheticDiffs.join(""),
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get git repository diffs for a given path
|
||||
* Handles both git repos and non-git directories
|
||||
*/
|
||||
export async function getGitRepositoryDiffs(
|
||||
repoPath: string
|
||||
): Promise<{ diff: string; files: FileStatus[]; hasChanges: boolean }> {
|
||||
// Check if it's a git repository
|
||||
const isRepo = await isGitRepo(repoPath);
|
||||
|
||||
if (!isRepo) {
|
||||
// Not a git repo - list all files and treat them as new
|
||||
const result = await generateDiffsForNonGitDirectory(repoPath);
|
||||
return {
|
||||
diff: result.diff,
|
||||
files: result.files,
|
||||
hasChanges: result.files.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Get git diff and status
|
||||
const { stdout: diff } = await execAsync("git diff HEAD", {
|
||||
cwd: repoPath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
||||
cwd: repoPath,
|
||||
});
|
||||
|
||||
const files = parseGitStatus(status);
|
||||
|
||||
// Generate synthetic diffs for untracked (new) files
|
||||
const combinedDiff = await appendUntrackedFileDiffs(repoPath, diff, files);
|
||||
|
||||
return {
|
||||
diff: combinedDiff,
|
||||
files,
|
||||
hasChanges: files.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error message from error object
|
||||
*/
|
||||
|
||||
22
apps/server/src/routes/enhance-prompt/index.ts
Normal file
22
apps/server/src/routes/enhance-prompt/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Enhance prompt routes - HTTP API for AI-powered text enhancement
|
||||
*
|
||||
* Provides endpoints for enhancing user input text using Claude AI
|
||||
* with different enhancement modes (improve, expand, simplify, etc.)
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { createEnhanceHandler } from "./routes/enhance.js";
|
||||
|
||||
/**
|
||||
* Create the enhance-prompt router
|
||||
*
|
||||
* @returns Express router with enhance-prompt endpoints
|
||||
*/
|
||||
export function createEnhancePromptRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post("/", createEnhanceHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
195
apps/server/src/routes/enhance-prompt/routes/enhance.ts
Normal file
195
apps/server/src/routes/enhance-prompt/routes/enhance.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* POST /enhance-prompt endpoint - Enhance user input text
|
||||
*
|
||||
* Uses Claude AI to enhance text based on the specified enhancement mode.
|
||||
* Supports modes: improve, technical, simplify, acceptance
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { createLogger } from "../../../lib/logger.js";
|
||||
import {
|
||||
getSystemPrompt,
|
||||
buildUserPrompt,
|
||||
isValidEnhancementMode,
|
||||
type EnhancementMode,
|
||||
} from "../../../lib/enhancement-prompts.js";
|
||||
import { resolveModelString, CLAUDE_MODEL_MAP } from "../../../lib/model-resolver.js";
|
||||
|
||||
const logger = createLogger("EnhancePrompt");
|
||||
|
||||
/**
|
||||
* Request body for the enhance endpoint
|
||||
*/
|
||||
interface EnhanceRequestBody {
|
||||
/** The original text to enhance */
|
||||
originalText: string;
|
||||
/** The enhancement mode to apply */
|
||||
enhancementMode: string;
|
||||
/** Optional model override */
|
||||
model?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Success response from the enhance endpoint
|
||||
*/
|
||||
interface EnhanceSuccessResponse {
|
||||
success: true;
|
||||
enhancedText: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response from the enhance endpoint
|
||||
*/
|
||||
interface EnhanceErrorResponse {
|
||||
success: false;
|
||||
error: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from Claude SDK response messages
|
||||
*
|
||||
* @param stream - The async iterable from the query function
|
||||
* @returns The extracted text content
|
||||
*/
|
||||
async function extractTextFromStream(
|
||||
stream: AsyncIterable<{
|
||||
type: string;
|
||||
subtype?: string;
|
||||
result?: string;
|
||||
message?: {
|
||||
content?: Array<{ type: string; text?: string }>;
|
||||
};
|
||||
}>
|
||||
): Promise<string> {
|
||||
let responseText = "";
|
||||
|
||||
for await (const msg of stream) {
|
||||
if (msg.type === "assistant" && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === "text" && block.text) {
|
||||
responseText += block.text;
|
||||
}
|
||||
}
|
||||
} else if (msg.type === "result" && msg.subtype === "success") {
|
||||
responseText = msg.result || responseText;
|
||||
}
|
||||
}
|
||||
|
||||
return responseText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the enhance request handler
|
||||
*
|
||||
* @returns Express request handler for text enhancement
|
||||
*/
|
||||
export function createEnhanceHandler(): (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => Promise<void> {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { originalText, enhancementMode, model } =
|
||||
req.body as EnhanceRequestBody;
|
||||
|
||||
// Validate required fields
|
||||
if (!originalText || typeof originalText !== "string") {
|
||||
const response: EnhanceErrorResponse = {
|
||||
success: false,
|
||||
error: "originalText is required and must be a string",
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enhancementMode || typeof enhancementMode !== "string") {
|
||||
const response: EnhanceErrorResponse = {
|
||||
success: false,
|
||||
error: "enhancementMode is required and must be a string",
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate text is not empty
|
||||
const trimmedText = originalText.trim();
|
||||
if (trimmedText.length === 0) {
|
||||
const response: EnhanceErrorResponse = {
|
||||
success: false,
|
||||
error: "originalText cannot be empty",
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate and normalize enhancement mode
|
||||
const normalizedMode = enhancementMode.toLowerCase();
|
||||
const validMode: EnhancementMode = isValidEnhancementMode(normalizedMode)
|
||||
? normalizedMode
|
||||
: "improve";
|
||||
|
||||
logger.info(
|
||||
`Enhancing text with mode: ${validMode}, length: ${trimmedText.length} chars`
|
||||
);
|
||||
|
||||
// Get the system prompt for this mode
|
||||
const systemPrompt = getSystemPrompt(validMode);
|
||||
|
||||
// Build the user prompt with few-shot examples
|
||||
// This helps the model understand this is text transformation, not a coding task
|
||||
const userPrompt = buildUserPrompt(validMode, trimmedText, true);
|
||||
|
||||
// Resolve the model - use the passed model, default to sonnet for quality
|
||||
const resolvedModel = resolveModelString(model, CLAUDE_MODEL_MAP.sonnet);
|
||||
|
||||
logger.debug(`Using model: ${resolvedModel}`);
|
||||
|
||||
// Call Claude SDK with minimal configuration for text transformation
|
||||
// Key: no tools, just text completion
|
||||
const stream = query({
|
||||
prompt: userPrompt,
|
||||
options: {
|
||||
model: resolvedModel,
|
||||
systemPrompt,
|
||||
maxTurns: 1,
|
||||
allowedTools: [],
|
||||
permissionMode: "acceptEdits",
|
||||
},
|
||||
});
|
||||
|
||||
// Extract the enhanced text from the response
|
||||
const enhancedText = await extractTextFromStream(stream);
|
||||
|
||||
if (!enhancedText || enhancedText.trim().length === 0) {
|
||||
logger.warn("Received empty response from Claude");
|
||||
const response: EnhanceErrorResponse = {
|
||||
success: false,
|
||||
error: "Failed to generate enhanced text - empty response",
|
||||
};
|
||||
res.status(500).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Enhancement complete, output length: ${enhancedText.length} chars`
|
||||
);
|
||||
|
||||
const response: EnhanceSuccessResponse = {
|
||||
success: true,
|
||||
enhancedText: enhancedText.trim(),
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error occurred";
|
||||
logger.error("Enhancement failed:", errorMessage);
|
||||
|
||||
const response: EnhanceErrorResponse = {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
res.status(500).json(response);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -3,11 +3,8 @@
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
import { getGitRepositoryDiffs } from "../../common.js";
|
||||
|
||||
export function createDiffsHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -20,43 +17,15 @@ export function createDiffsHandler() {
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout: diff } = await execAsync("git diff HEAD", {
|
||||
cwd: projectPath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
||||
cwd: projectPath,
|
||||
});
|
||||
|
||||
const files = status
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const statusChar = line[0];
|
||||
const filePath = line.slice(3);
|
||||
const statusMap: Record<string, string> = {
|
||||
M: "Modified",
|
||||
A: "Added",
|
||||
D: "Deleted",
|
||||
R: "Renamed",
|
||||
C: "Copied",
|
||||
U: "Updated",
|
||||
"?": "Untracked",
|
||||
};
|
||||
return {
|
||||
status: statusChar,
|
||||
path: filePath,
|
||||
statusText: statusMap[statusChar] || "Unknown",
|
||||
};
|
||||
});
|
||||
|
||||
const result = await getGitRepositoryDiffs(projectPath);
|
||||
res.json({
|
||||
success: true,
|
||||
diff,
|
||||
files,
|
||||
hasChanges: files.length > 0,
|
||||
diff: result.diff,
|
||||
files: result.files,
|
||||
hasChanges: result.hasChanges,
|
||||
});
|
||||
} catch {
|
||||
} catch (innerError) {
|
||||
logError(innerError, "Git diff failed");
|
||||
res.json({ success: true, diff: "", files: [], hasChanges: false });
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import { generateSyntheticDiffForNewFile } from "../../common.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -25,16 +26,33 @@ export function createFileDiffHandler() {
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout: diff } = await execAsync(
|
||||
// First check if the file is untracked
|
||||
const { stdout: status } = await execAsync(
|
||||
`git status --porcelain -- "${filePath}"`,
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
|
||||
const isUntracked = status.trim().startsWith("??");
|
||||
|
||||
let diff: string;
|
||||
if (isUntracked) {
|
||||
// Generate synthetic diff for untracked file
|
||||
diff = await generateSyntheticDiffForNewFile(projectPath, filePath);
|
||||
} else {
|
||||
// Use regular git diff for tracked files
|
||||
const result = await execAsync(
|
||||
`git diff HEAD -- "${filePath}"`,
|
||||
{
|
||||
cwd: projectPath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
}
|
||||
);
|
||||
diff = result.stdout;
|
||||
}
|
||||
|
||||
res.json({ success: true, diff, filePath });
|
||||
} catch {
|
||||
} catch (innerError) {
|
||||
logError(innerError, "Git file diff failed");
|
||||
res.json({ success: true, diff: "", filePath });
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -17,12 +17,15 @@ export async function getClaudeStatus() {
|
||||
let cliPath = "";
|
||||
let method = "none";
|
||||
|
||||
// Try to find Claude CLI
|
||||
const isWindows = process.platform === "win32";
|
||||
|
||||
// Try to find Claude CLI using platform-specific command
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
"which claude || where claude 2>/dev/null"
|
||||
);
|
||||
cliPath = stdout.trim();
|
||||
// Use 'where' on Windows, 'which' on Unix-like systems
|
||||
const findCommand = isWindows ? "where claude" : "which claude";
|
||||
const { stdout } = await execAsync(findCommand);
|
||||
// 'where' on Windows can return multiple paths - take the first one
|
||||
cliPath = stdout.trim().split(/\r?\n/)[0];
|
||||
installed = true;
|
||||
method = "path";
|
||||
|
||||
@@ -34,8 +37,21 @@ export async function getClaudeStatus() {
|
||||
// Version command might not be available
|
||||
}
|
||||
} catch {
|
||||
// Not in PATH, try common locations
|
||||
const commonPaths = [
|
||||
// Not in PATH, try common locations based on platform
|
||||
const commonPaths = isWindows
|
||||
? (() => {
|
||||
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
||||
return [
|
||||
// Windows-specific paths
|
||||
path.join(os.homedir(), ".local", "bin", "claude.exe"),
|
||||
path.join(appData, "npm", "claude.cmd"),
|
||||
path.join(appData, "npm", "claude"),
|
||||
path.join(appData, ".npm-global", "bin", "claude.cmd"),
|
||||
path.join(appData, ".npm-global", "bin", "claude"),
|
||||
];
|
||||
})()
|
||||
: [
|
||||
// Unix (Linux/macOS) paths
|
||||
path.join(os.homedir(), ".local", "bin", "claude"),
|
||||
path.join(os.homedir(), ".claude", "local", "claude"),
|
||||
"/usr/local/bin/claude",
|
||||
@@ -124,8 +140,14 @@ export async function getClaudeStatus() {
|
||||
// Settings file doesn't exist
|
||||
}
|
||||
|
||||
// Check for credentials file (OAuth tokens from claude login) - legacy/alternative auth
|
||||
const credentialsPath = path.join(claudeDir, "credentials.json");
|
||||
// Check for credentials file (OAuth tokens from claude login)
|
||||
// Note: Claude CLI may use ".credentials.json" (hidden) or "credentials.json" depending on version/platform
|
||||
const credentialsPaths = [
|
||||
path.join(claudeDir, ".credentials.json"),
|
||||
path.join(claudeDir, "credentials.json"),
|
||||
];
|
||||
|
||||
for (const credentialsPath of credentialsPaths) {
|
||||
try {
|
||||
const credentialsContent = await fs.readFile(credentialsPath, "utf-8");
|
||||
const credentials = JSON.parse(credentialsContent);
|
||||
@@ -142,8 +164,10 @@ export async function getClaudeStatus() {
|
||||
auth.authenticated = true;
|
||||
auth.method = "api_key"; // Stored API key in credentials file
|
||||
}
|
||||
break; // Found and processed credentials file
|
||||
} catch {
|
||||
// No credentials file or invalid format
|
||||
// No credentials file at this path or invalid format
|
||||
}
|
||||
}
|
||||
|
||||
// Environment variables override stored credentials (higher priority)
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
import { getGitRepositoryDiffs } from "../../common.js";
|
||||
|
||||
export function createDiffsHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -33,46 +30,34 @@ export function createDiffsHandler() {
|
||||
const worktreePath = path.join(projectPath, ".worktrees", featureId);
|
||||
|
||||
try {
|
||||
// Check if worktree exists
|
||||
await fs.access(worktreePath);
|
||||
const { stdout: diff } = await execAsync("git diff HEAD", {
|
||||
cwd: worktreePath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
|
||||
const files = status
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const statusChar = line[0];
|
||||
const filePath = line.slice(3);
|
||||
const statusMap: Record<string, string> = {
|
||||
M: "Modified",
|
||||
A: "Added",
|
||||
D: "Deleted",
|
||||
R: "Renamed",
|
||||
C: "Copied",
|
||||
U: "Updated",
|
||||
"?": "Untracked",
|
||||
};
|
||||
return {
|
||||
status: statusChar,
|
||||
path: filePath,
|
||||
statusText: statusMap[statusChar] || "Unknown",
|
||||
};
|
||||
});
|
||||
|
||||
// Get diffs from worktree
|
||||
const result = await getGitRepositoryDiffs(worktreePath);
|
||||
res.json({
|
||||
success: true,
|
||||
diff,
|
||||
files,
|
||||
hasChanges: files.length > 0,
|
||||
diff: result.diff,
|
||||
files: result.files,
|
||||
hasChanges: result.hasChanges,
|
||||
});
|
||||
} catch {
|
||||
} catch (innerError) {
|
||||
// Worktree doesn't exist - fallback to main project path
|
||||
logError(innerError, "Worktree access failed, falling back to main project");
|
||||
|
||||
try {
|
||||
const result = await getGitRepositoryDiffs(projectPath);
|
||||
res.json({
|
||||
success: true,
|
||||
diff: result.diff,
|
||||
files: result.files,
|
||||
hasChanges: result.hasChanges,
|
||||
});
|
||||
} catch (fallbackError) {
|
||||
logError(fallbackError, "Fallback to main project also failed");
|
||||
res.json({ success: true, diff: "", files: [], hasChanges: false });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Get worktree diffs failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
|
||||
@@ -8,6 +8,7 @@ import { promisify } from "util";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import { generateSyntheticDiffForNewFile } from "../../common.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -33,16 +34,34 @@ export function createFileDiffHandler() {
|
||||
|
||||
try {
|
||||
await fs.access(worktreePath);
|
||||
const { stdout: diff } = await execAsync(
|
||||
|
||||
// First check if the file is untracked
|
||||
const { stdout: status } = await execAsync(
|
||||
`git status --porcelain -- "${filePath}"`,
|
||||
{ cwd: worktreePath }
|
||||
);
|
||||
|
||||
const isUntracked = status.trim().startsWith("??");
|
||||
|
||||
let diff: string;
|
||||
if (isUntracked) {
|
||||
// Generate synthetic diff for untracked file
|
||||
diff = await generateSyntheticDiffForNewFile(worktreePath, filePath);
|
||||
} else {
|
||||
// Use regular git diff for tracked files
|
||||
const result = await execAsync(
|
||||
`git diff HEAD -- "${filePath}"`,
|
||||
{
|
||||
cwd: worktreePath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
}
|
||||
);
|
||||
diff = result.stdout;
|
||||
}
|
||||
|
||||
res.json({ success: true, diff, filePath });
|
||||
} catch {
|
||||
} catch (innerError) {
|
||||
logError(innerError, "Worktree file diff failed");
|
||||
res.json({ success: true, diff: "", filePath });
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
233
apps/server/tests/unit/lib/enhancement-prompts.test.ts
Normal file
233
apps/server/tests/unit/lib/enhancement-prompts.test.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
getEnhancementPrompt,
|
||||
getSystemPrompt,
|
||||
getExamples,
|
||||
buildUserPrompt,
|
||||
isValidEnhancementMode,
|
||||
getAvailableEnhancementModes,
|
||||
IMPROVE_SYSTEM_PROMPT,
|
||||
TECHNICAL_SYSTEM_PROMPT,
|
||||
SIMPLIFY_SYSTEM_PROMPT,
|
||||
ACCEPTANCE_SYSTEM_PROMPT,
|
||||
IMPROVE_EXAMPLES,
|
||||
TECHNICAL_EXAMPLES,
|
||||
SIMPLIFY_EXAMPLES,
|
||||
ACCEPTANCE_EXAMPLES,
|
||||
type EnhancementMode,
|
||||
} from "@/lib/enhancement-prompts.js";
|
||||
|
||||
describe("enhancement-prompts.ts", () => {
|
||||
describe("System Prompt Constants", () => {
|
||||
it("should have non-empty improve system prompt", () => {
|
||||
expect(IMPROVE_SYSTEM_PROMPT).toBeDefined();
|
||||
expect(IMPROVE_SYSTEM_PROMPT.length).toBeGreaterThan(100);
|
||||
expect(IMPROVE_SYSTEM_PROMPT).toContain("ANALYZE");
|
||||
expect(IMPROVE_SYSTEM_PROMPT).toContain("CLARIFY");
|
||||
});
|
||||
|
||||
it("should have non-empty technical system prompt", () => {
|
||||
expect(TECHNICAL_SYSTEM_PROMPT).toBeDefined();
|
||||
expect(TECHNICAL_SYSTEM_PROMPT.length).toBeGreaterThan(100);
|
||||
expect(TECHNICAL_SYSTEM_PROMPT).toContain("technical");
|
||||
});
|
||||
|
||||
it("should have non-empty simplify system prompt", () => {
|
||||
expect(SIMPLIFY_SYSTEM_PROMPT).toBeDefined();
|
||||
expect(SIMPLIFY_SYSTEM_PROMPT.length).toBeGreaterThan(100);
|
||||
expect(SIMPLIFY_SYSTEM_PROMPT).toContain("simplify");
|
||||
});
|
||||
|
||||
it("should have non-empty acceptance system prompt", () => {
|
||||
expect(ACCEPTANCE_SYSTEM_PROMPT).toBeDefined();
|
||||
expect(ACCEPTANCE_SYSTEM_PROMPT.length).toBeGreaterThan(100);
|
||||
expect(ACCEPTANCE_SYSTEM_PROMPT).toContain("acceptance criteria");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Example Constants", () => {
|
||||
it("should have improve examples with input and output", () => {
|
||||
expect(IMPROVE_EXAMPLES).toBeDefined();
|
||||
expect(IMPROVE_EXAMPLES.length).toBeGreaterThan(0);
|
||||
IMPROVE_EXAMPLES.forEach((example) => {
|
||||
expect(example.input).toBeDefined();
|
||||
expect(example.output).toBeDefined();
|
||||
expect(example.input.length).toBeGreaterThan(0);
|
||||
expect(example.output.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should have technical examples with input and output", () => {
|
||||
expect(TECHNICAL_EXAMPLES).toBeDefined();
|
||||
expect(TECHNICAL_EXAMPLES.length).toBeGreaterThan(0);
|
||||
TECHNICAL_EXAMPLES.forEach((example) => {
|
||||
expect(example.input).toBeDefined();
|
||||
expect(example.output).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should have simplify examples with input and output", () => {
|
||||
expect(SIMPLIFY_EXAMPLES).toBeDefined();
|
||||
expect(SIMPLIFY_EXAMPLES.length).toBeGreaterThan(0);
|
||||
SIMPLIFY_EXAMPLES.forEach((example) => {
|
||||
expect(example.input).toBeDefined();
|
||||
expect(example.output).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should have acceptance examples with input and output", () => {
|
||||
expect(ACCEPTANCE_EXAMPLES).toBeDefined();
|
||||
expect(ACCEPTANCE_EXAMPLES.length).toBeGreaterThan(0);
|
||||
ACCEPTANCE_EXAMPLES.forEach((example) => {
|
||||
expect(example.input).toBeDefined();
|
||||
expect(example.output).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEnhancementPrompt", () => {
|
||||
it("should return config for improve mode", () => {
|
||||
const config = getEnhancementPrompt("improve");
|
||||
expect(config.systemPrompt).toBe(IMPROVE_SYSTEM_PROMPT);
|
||||
expect(config.description).toContain("clear");
|
||||
});
|
||||
|
||||
it("should return config for technical mode", () => {
|
||||
const config = getEnhancementPrompt("technical");
|
||||
expect(config.systemPrompt).toBe(TECHNICAL_SYSTEM_PROMPT);
|
||||
expect(config.description).toContain("technical");
|
||||
});
|
||||
|
||||
it("should return config for simplify mode", () => {
|
||||
const config = getEnhancementPrompt("simplify");
|
||||
expect(config.systemPrompt).toBe(SIMPLIFY_SYSTEM_PROMPT);
|
||||
expect(config.description).toContain("concise");
|
||||
});
|
||||
|
||||
it("should return config for acceptance mode", () => {
|
||||
const config = getEnhancementPrompt("acceptance");
|
||||
expect(config.systemPrompt).toBe(ACCEPTANCE_SYSTEM_PROMPT);
|
||||
expect(config.description).toContain("acceptance");
|
||||
});
|
||||
|
||||
it("should handle case-insensitive mode", () => {
|
||||
const config = getEnhancementPrompt("IMPROVE");
|
||||
expect(config.systemPrompt).toBe(IMPROVE_SYSTEM_PROMPT);
|
||||
});
|
||||
|
||||
it("should fall back to improve for invalid mode", () => {
|
||||
const config = getEnhancementPrompt("invalid-mode");
|
||||
expect(config.systemPrompt).toBe(IMPROVE_SYSTEM_PROMPT);
|
||||
});
|
||||
|
||||
it("should fall back to improve for empty string", () => {
|
||||
const config = getEnhancementPrompt("");
|
||||
expect(config.systemPrompt).toBe(IMPROVE_SYSTEM_PROMPT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSystemPrompt", () => {
|
||||
it("should return correct system prompt for each mode", () => {
|
||||
expect(getSystemPrompt("improve")).toBe(IMPROVE_SYSTEM_PROMPT);
|
||||
expect(getSystemPrompt("technical")).toBe(TECHNICAL_SYSTEM_PROMPT);
|
||||
expect(getSystemPrompt("simplify")).toBe(SIMPLIFY_SYSTEM_PROMPT);
|
||||
expect(getSystemPrompt("acceptance")).toBe(ACCEPTANCE_SYSTEM_PROMPT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getExamples", () => {
|
||||
it("should return correct examples for each mode", () => {
|
||||
expect(getExamples("improve")).toBe(IMPROVE_EXAMPLES);
|
||||
expect(getExamples("technical")).toBe(TECHNICAL_EXAMPLES);
|
||||
expect(getExamples("simplify")).toBe(SIMPLIFY_EXAMPLES);
|
||||
expect(getExamples("acceptance")).toBe(ACCEPTANCE_EXAMPLES);
|
||||
});
|
||||
|
||||
it("should return arrays with example objects", () => {
|
||||
const modes: EnhancementMode[] = ["improve", "technical", "simplify", "acceptance"];
|
||||
modes.forEach((mode) => {
|
||||
const examples = getExamples(mode);
|
||||
expect(Array.isArray(examples)).toBe(true);
|
||||
expect(examples.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildUserPrompt", () => {
|
||||
const testText = "Add a logout button";
|
||||
|
||||
it("should build prompt with examples by default", () => {
|
||||
const prompt = buildUserPrompt("improve", testText);
|
||||
expect(prompt).toContain("Example 1:");
|
||||
expect(prompt).toContain(testText);
|
||||
expect(prompt).toContain("Now, please enhance the following task description:");
|
||||
});
|
||||
|
||||
it("should build prompt without examples when includeExamples is false", () => {
|
||||
const prompt = buildUserPrompt("improve", testText, false);
|
||||
expect(prompt).not.toContain("Example 1:");
|
||||
expect(prompt).toContain(testText);
|
||||
expect(prompt).toContain("Please enhance the following task description:");
|
||||
});
|
||||
|
||||
it("should include all examples for improve mode", () => {
|
||||
const prompt = buildUserPrompt("improve", testText);
|
||||
IMPROVE_EXAMPLES.forEach((example, index) => {
|
||||
expect(prompt).toContain(`Example ${index + 1}:`);
|
||||
expect(prompt).toContain(example.input);
|
||||
});
|
||||
});
|
||||
|
||||
it("should include separator between examples", () => {
|
||||
const prompt = buildUserPrompt("improve", testText);
|
||||
expect(prompt).toContain("---");
|
||||
});
|
||||
|
||||
it("should work with all enhancement modes", () => {
|
||||
const modes: EnhancementMode[] = ["improve", "technical", "simplify", "acceptance"];
|
||||
modes.forEach((mode) => {
|
||||
const prompt = buildUserPrompt(mode, testText);
|
||||
expect(prompt).toContain(testText);
|
||||
expect(prompt.length).toBeGreaterThan(100);
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve the original text exactly", () => {
|
||||
const specialText = "Add feature with special chars: <>&\"'";
|
||||
const prompt = buildUserPrompt("improve", specialText);
|
||||
expect(prompt).toContain(specialText);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidEnhancementMode", () => {
|
||||
it("should return true for valid modes", () => {
|
||||
expect(isValidEnhancementMode("improve")).toBe(true);
|
||||
expect(isValidEnhancementMode("technical")).toBe(true);
|
||||
expect(isValidEnhancementMode("simplify")).toBe(true);
|
||||
expect(isValidEnhancementMode("acceptance")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for invalid modes", () => {
|
||||
expect(isValidEnhancementMode("invalid")).toBe(false);
|
||||
expect(isValidEnhancementMode("IMPROVE")).toBe(false); // case-sensitive
|
||||
expect(isValidEnhancementMode("")).toBe(false);
|
||||
expect(isValidEnhancementMode("random")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAvailableEnhancementModes", () => {
|
||||
it("should return all four enhancement modes", () => {
|
||||
const modes = getAvailableEnhancementModes();
|
||||
expect(modes).toHaveLength(4);
|
||||
expect(modes).toContain("improve");
|
||||
expect(modes).toContain("technical");
|
||||
expect(modes).toContain("simplify");
|
||||
expect(modes).toContain("acceptance");
|
||||
});
|
||||
|
||||
it("should return an array", () => {
|
||||
const modes = getAvailableEnhancementModes();
|
||||
expect(Array.isArray(modes)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
269
backup.json
269
backup.json
@@ -1,269 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "feature-1765387670653-bl83444lj",
|
||||
"category": "Kanban",
|
||||
"description": "In the output logs of the proc agent output in the file diffs Can you add a scroll bar so it actually scroll to see all these new styles right now it seems like I can't scroll",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-10T17:42:09.158Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"summary": "Fixed scrolling for file diffs in agent output modal. Changed approach: parent container (agent-output-modal.tsx) now handles scrolling with overflow-y-auto, while GitDiffPanel uses natural height without flex-based scrolling. Modified: agent-output-modal.tsx (line 304), git-diff-panel.tsx (lines 461, 500, 525, 614).",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765387746902-na752mp1y",
|
||||
"category": "Kanban",
|
||||
"description": "When the add feature modal pops up, make sure that the description is always the main focus. When it first loads up. Do not focus the prompt tab, which is currently doing this.",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-10T17:29:13.854Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"summary": "Added autoFocus prop to DescriptionImageDropZone component. Modified: description-image-dropzone.tsx (added autoFocus prop support), board-view.tsx (enabled autoFocus on add feature modal). Now the description textarea receives focus when the modal opens instead of the prompt tab.",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765388139100-ln31jgp5n",
|
||||
"category": "Uncategorized",
|
||||
"description": "Can you add a disclaimer .md file to this project saying that this uses a bunch of AI related tooling which could have access to your operating system and change and delete files and so use at your own risk. We tried to check it for security of vulnerability to make sure it's good. But you assume the risk and you should be reviewing the code yourself before you try to run it. And also sandboxing this so it doesn't have access to your whole operating system like using Docker to sandbox before you run it or use a virtual machine to sandbox it. and that we do not recommend running locally on your computer due to the risk of it having access to everything on your computer.\n\nUpdate or read me with a short paragraph overview/description at the top followed by a disclaimer section in red that points to the disclaimer file with the same disclaimer information.\n\nThen a section that lists out all the features of cool emojis.",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-10T17:35:40.700Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"summary": "Created DISCLAIMER.md with comprehensive security warnings about AI tooling risks and sandboxing recommendations. Updated README.md with project overview, red caution disclaimer section linking to DISCLAIMER.md, and features list with emojis covering all major functionality (Kanban, AI agents, multi-model support, etc.).",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765388388144-oa1dewze9",
|
||||
"category": "Uncategorized",
|
||||
"description": "Please fix the styling of the hotkeys to be more using the theme colors. Notice that they're kind of gray. I would rather than have some type of like light green if they're not active and then the brighter green if they are active and also the add feature but in the top right it's not very legible. So fix the accessibility of the hotkey but also keep it within the theme. You might just have to change the text inside of it to be bright green.",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-10T17:40:02.745Z",
|
||||
"imagePaths": [
|
||||
{
|
||||
"id": "img-1765388352835-dgx4ishp0",
|
||||
"path": "/Users/webdevcody/Library/Application Support/automaker/images/1765388352832-6jnbgw8kg_Screenshot_2025-12-10_at_12.39.10_PM.png",
|
||||
"filename": "Screenshot 2025-12-10 at 12.39.10 PM.png",
|
||||
"mimeType": "image/png"
|
||||
},
|
||||
{
|
||||
"id": "img-1765388356955-a0gdovp5b",
|
||||
"path": "/Users/webdevcody/Library/Application Support/automaker/images/1765388356954-d59a65nf9_Screenshot_2025-12-10_at_12.39.15_PM.png",
|
||||
"filename": "Screenshot 2025-12-10 at 12.39.15 PM.png",
|
||||
"mimeType": "image/png"
|
||||
}
|
||||
],
|
||||
"skipTests": true,
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765388402095-x66aduwg3",
|
||||
"category": "Uncategorized",
|
||||
"description": "Can you please add some spacing and fix the styling of the hotkey with the command enter and make it so they're both vertically aligned for those icons?",
|
||||
"steps": [],
|
||||
"status": "waiting_approval",
|
||||
"startedAt": "2025-12-10T17:44:08.667Z",
|
||||
"imagePaths": [
|
||||
{
|
||||
"id": "img-1765388390408-eefybe95t",
|
||||
"path": "/Users/webdevcody/Library/Application Support/automaker/images/1765388390408-nn320yoyc_Screenshot_2025-12-10_at_12.39.47_PM.png",
|
||||
"filename": "Screenshot 2025-12-10 at 12.39.47 PM.png",
|
||||
"mimeType": "image/png"
|
||||
}
|
||||
],
|
||||
"skipTests": true,
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765388662444-as3hqn7be",
|
||||
"category": "Uncategorized",
|
||||
"description": "Fix the styling on all the buttons when I hover over them with my mouse they never change to a click mouse cursor. In order they seem to show any type of like hover state changes, if they do, at least for the certain game I'm using, it's not very obvious that you're hovering over the button.",
|
||||
"steps": [],
|
||||
"status": "waiting_approval",
|
||||
"startedAt": "2025-12-10T17:45:59.666Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"summary": "Fixed hover cursor styling on all interactive elements. Modified: button.tsx (added cursor-pointer to base styles), dropdown-menu.tsx (added cursor-pointer to all menu items), checkbox.tsx (added cursor-pointer), tabs.tsx (added cursor-pointer to triggers), dialog.tsx (added cursor-pointer to close button), slider.tsx (added cursor-grab to thumb, cursor-pointer to track), globals.css (added global CSS rules for clickable elements to ensure consistent cursor behavior).",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765388693856-yx1dk1acj",
|
||||
"category": "Kanban",
|
||||
"description": "The tabs in the add new feature modal for the prompt model and testing tabs. They don't seem to look like tabs when I'm on a certain theme. Can you verify that those are hooked into the theme? And make sure that the active one is colored differently than the unactive ones. Keep the primary colors when doing this.",
|
||||
"steps": [],
|
||||
"status": "waiting_approval",
|
||||
"startedAt": "2025-12-10T17:46:00.019Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"summary": "Fixed tabs component theme integration. Modified: tabs.tsx. Changes: (1) Added visible border to TabsList container using theme's border color, (2) Changed inactive tab text to foreground/70 for better contrast, (3) Enhanced active tab with shadow-md and semi-transparent primary border, (4) Improved hover state with full accent background. Active tabs now properly use bg-primary/text-primary-foreground which adapts to each theme.",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765388754462-bek0flvkj",
|
||||
"category": "Uncategorized",
|
||||
"description": "There's a strange issue when I when when these agents are like doing things it seems like it completely refreshes the whole Kanban board and there's like a black flash. Can you verify that the data loading does not cause the entire component to refresh? Maybe there's an issue with the react effect or how the component is rendered maybe we need some used memos or something but it shouldn't refresh the whole page it should just like update the individual cards when they change.",
|
||||
"steps": [],
|
||||
"status": "waiting_approval",
|
||||
"startedAt": "2025-12-10T17:47:20.170Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"summary": "Fixed Kanban board flash/refresh issue. Changes: (1) board-view.tsx - Added isInitialLoadRef to only show loading spinner on initial load, not on feature reloads; memoized column features with useMemo to prevent recalculation on every render. (2) kanban-card.tsx - Wrapped with React.memo to prevent unnecessary re-renders. (3) kanban-column.tsx - Wrapped with React.memo for performance. The flash was caused by loadFeatures setting isLoading=true on every reload, which caused the entire board to unmount and show a loading spinner.",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765388793845-yhluf0sry",
|
||||
"category": "Uncategorized",
|
||||
"description": "Add in the ability so that every project can have its own selected theme. This will allow me to have different projects have different themes so I can easily differentiate when I have one project selected or not.",
|
||||
"steps": [],
|
||||
"status": "waiting_approval",
|
||||
"startedAt": "2025-12-10T18:00:33.814Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"summary": "Fixed per-project theme support. Modified: settings-view.tsx (now saves theme to project when project is selected, shows label indicating scope), page.tsx (computes effectiveTheme from currentProject?.theme || theme), app-store.ts (added setProjectTheme action, theme property on Project interface). When a project is selected, changing theme in Settings saves to that project only.",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765389333728-y74hmz2yp",
|
||||
"category": "Agent Runner",
|
||||
"description": "On the Agent Runner, I took a screenshot and dropped it into the text area and after a certain amount of time, it's like the image preview just completely went away. Can you debug and fix this on the Agent Runner?",
|
||||
"steps": [],
|
||||
"status": "backlog",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765389352488-j9bez5ztx",
|
||||
"category": "Kanban",
|
||||
"description": "It seems like the category typehead is no longer working. Can you double check that code didn't break? It should have kept track of categories inside of the categories.json file inside the .automaker folder when adding new features modal",
|
||||
"steps": [],
|
||||
"status": "backlog",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765389420151-jzdsjzn9u",
|
||||
"category": "Kanban",
|
||||
"description": "Add in the ability to just click and drag a card from the waiting approval directly into the verify column as I can usually just commit it manually if I want to.",
|
||||
"steps": [],
|
||||
"status": "waiting_approval",
|
||||
"startedAt": "2025-12-10T18:05:08.252Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"summary": "Fixed drag-and-drop from waiting_approval to verified column. The issue was condition ordering in handleDragEnd - the skipTests check was intercepting waiting_approval features before they could be handled. Moved waiting_approval status check before skipTests check in board-view.tsx:731-752. Also updated agent memory with this lesson.",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765389468077-9x3vt1yjq",
|
||||
"category": "Uncategorized",
|
||||
"description": "The commit functionality on the waiting approval cards doesn't seem to work. It just committed everything in my working copy for git. I think I should be a little bit more intelligent and figure out what files it changed for that AI session and then only try to git add those individual files and commit those. Right now it just basically did a git add all and committed those. Re-factor the prompting or figure out a way to make it so it's more specific on what it's going to commit with the future change.",
|
||||
"steps": [],
|
||||
"status": "backlog",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765389502705-6deep7mvi",
|
||||
"category": "Uncategorized",
|
||||
"description": "I'm noticing that a lot of buttons in the UI, especially the ones that are submitting, are either missing the submit hotkey or they're not styled properly. Look at the add feature submit button that's on the add feature modal and abstract away a submit button so that on every single page that needs to submit something I can reuse this type of hotkey functionality. In fact, every single button should be abstracted enough where I can provide a hotkey and it will automatically listen if I press that hotkey when it's in view.",
|
||||
"steps": [],
|
||||
"status": "backlog",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765389772166-an3yk3kpo",
|
||||
"category": "Uncategorized",
|
||||
"description": "Can you add some more padding to the bottom of the settings panel? Notice that I can't scroll down all the way. And that doesn't highlight the left sub navigation to highlight it pink when I'm on that section. I should be able to scroll a bit further and just have like blank space at the bottom. So I can eventually get to that actual section.",
|
||||
"steps": [],
|
||||
"status": "backlog",
|
||||
"imagePaths": [
|
||||
{
|
||||
"id": "img-1765389750685-jhq6rcidc",
|
||||
"path": "/Users/webdevcody/Library/Application Support/automaker/images/1765389750683-mqb0j7a3z_Screenshot_2025-12-10_at_1.02.26_PM.png",
|
||||
"filename": "Screenshot 2025-12-10 at 1.02.26 PM.png",
|
||||
"mimeType": "image/png"
|
||||
}
|
||||
],
|
||||
"skipTests": true,
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765389829239-bbk596u6z",
|
||||
"category": "Uncategorized",
|
||||
"description": "Add some type of XML highlighting to the spec editor view. Right now it's just all grayscale and it's kind of ugly to look at. And try to make the syntax highlighting match the current selected theme.",
|
||||
"steps": [],
|
||||
"status": "backlog",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765389859334-si9ivtehw",
|
||||
"category": "Uncategorized",
|
||||
"description": "Add a search bar to the top of the Kanban column that allows me to search the filter down just to show the cards I'm interested in by keyword.",
|
||||
"steps": [],
|
||||
"status": "in_progress",
|
||||
"startedAt": "2025-12-10T18:09:26.193Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"summary": "Added search bar to Kanban board. Modified: board-view.tsx. Features: 1) Search input with icon at top of columns, 2) Case-insensitive filtering by description or category, 3) Clear button to reset search, 4) Real-time filtering as you type.",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765390022638-nalulsdxv",
|
||||
"category": "Uncategorized",
|
||||
"description": "In the project select can you actually remove the whole like 1 2 3 4 5 hotkeys instead? Just make it be a type ahead so when I open the panel I just should be able to type in the first letter or two of the project that I want and press enter and that should Just select it for me",
|
||||
"steps": [],
|
||||
"status": "backlog",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765390055621-ewc4w7k5h",
|
||||
"category": "Uncategorized",
|
||||
"description": "In the add new feature prompt, instead of disabling the add feature button until we type into the description, keep it enabled. But if you click it, make sure you just show the client side validation and turn the description box in any other required field as red so that the user knows they have to fill it in.",
|
||||
"steps": [],
|
||||
"status": "backlog",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765390131625-ymqxr5gln",
|
||||
"category": "Uncategorized",
|
||||
"description": "Can you please in the top right of the Kanban board use the show three icons for the Kanban card display formatting. You can look at the settings page to see that there's three different settings that we use for displaying the Kanban card information. But I also just want this to be really quickly accessible at the top right of the Kanban that they can switch between those three toggles. Keep them simple only just icons you don't need to put words in them. Make sure they do have harbor states though, or tooltips I mean.",
|
||||
"steps": [],
|
||||
"status": "backlog",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
}
|
||||
]
|
||||
237
docs/checkout-branch-pr.md
Normal file
237
docs/checkout-branch-pr.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Git Workflow: Branch, Commit, Push, and Pull Request
|
||||
|
||||
This document outlines the standard workflow for creating a branch, committing changes, pushing to remote, and creating a pull request.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Git installed and configured
|
||||
- GitHub CLI (`gh`) installed (optional, but recommended for PR creation)
|
||||
- Access to the repository
|
||||
- Authentication configured (SSH keys or GitHub CLI authentication)
|
||||
|
||||
## Step-by-Step Workflow
|
||||
|
||||
### 1. Check Current Status
|
||||
|
||||
First, check what changes exist in your working directory:
|
||||
|
||||
```bash
|
||||
git status
|
||||
```
|
||||
|
||||
This shows:
|
||||
|
||||
- Modified files
|
||||
- Deleted files
|
||||
- Untracked files
|
||||
- Current branch
|
||||
|
||||
### 2. Create a New Branch
|
||||
|
||||
Create and switch to a new branch for your changes:
|
||||
|
||||
```bash
|
||||
git checkout -b <branch-name>
|
||||
```
|
||||
|
||||
**Branch naming conventions:**
|
||||
|
||||
- `feature/` - for new features
|
||||
- `fix/` or `bugfix/` - for bug fixes
|
||||
- `refactor/` - for code refactoring
|
||||
- `docs/` - for documentation changes
|
||||
- `chore/` - for maintenance tasks
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
git checkout -b refactor/monorepo-restructure
|
||||
```
|
||||
|
||||
### 3. Stage Changes
|
||||
|
||||
Stage all changes (including deletions and new files):
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
```
|
||||
|
||||
Or stage specific files:
|
||||
|
||||
```bash
|
||||
git add <file1> <file2>
|
||||
```
|
||||
|
||||
### 4. Commit Changes
|
||||
|
||||
Create a commit with a descriptive message:
|
||||
|
||||
```bash
|
||||
git commit -m "type: descriptive commit message"
|
||||
```
|
||||
|
||||
**Commit message conventions:**
|
||||
|
||||
- Use conventional commits format: `type: description`
|
||||
- Types: `feat`, `fix`, `refactor`, `docs`, `chore`, `test`, `style`
|
||||
- Keep messages concise but descriptive
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
git commit -m "refactor: restructure project to monorepo with apps directory"
|
||||
```
|
||||
|
||||
### 5. Push Branch to Remote
|
||||
|
||||
Push your branch to the remote repository:
|
||||
|
||||
```bash
|
||||
git push -u origin <branch-name>
|
||||
```
|
||||
|
||||
The `-u` flag sets up tracking so future pushes can use `git push` without specifying the branch.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
git push -u origin refactor/monorepo-restructure
|
||||
```
|
||||
|
||||
### 6. Create Pull Request
|
||||
|
||||
#### Option A: Using GitHub CLI (Recommended)
|
||||
|
||||
If you have GitHub CLI installed:
|
||||
|
||||
```bash
|
||||
gh pr create --title "Your PR Title" --body "Description of changes"
|
||||
```
|
||||
|
||||
To open in browser for review before creating:
|
||||
|
||||
```bash
|
||||
gh pr create --title "Your PR Title" --body "Description" --web
|
||||
```
|
||||
|
||||
#### Option B: Using GitHub Web Interface
|
||||
|
||||
After pushing, GitHub will provide a URL in the terminal output:
|
||||
|
||||
```
|
||||
remote: Create a pull request for '<branch-name>' on GitHub by visiting:
|
||||
remote: https://github.com/<org>/<repo>/pull/new/<branch-name>
|
||||
```
|
||||
|
||||
Visit that URL to create the PR through the web interface.
|
||||
|
||||
#### Option C: Manual PR Creation
|
||||
|
||||
1. Go to your repository on GitHub
|
||||
2. Click "Pull requests" tab
|
||||
3. Click "New pull request"
|
||||
4. Select your branch as the source
|
||||
5. Select the target branch (usually `main` or `master`)
|
||||
6. Fill in title and description
|
||||
7. Click "Create pull request"
|
||||
|
||||
## Complete Example Workflow
|
||||
|
||||
```bash
|
||||
# 1. Check status
|
||||
git status
|
||||
|
||||
# 2. Create branch
|
||||
git checkout -b feature/add-new-component
|
||||
|
||||
# 3. Make your changes (edit files, etc.)
|
||||
|
||||
# 4. Stage changes
|
||||
git add -A
|
||||
|
||||
# 5. Commit
|
||||
git commit -m "feat: add new user dashboard component"
|
||||
|
||||
# 6. Push
|
||||
git push -u origin feature/add-new-component
|
||||
|
||||
# 7. Create PR
|
||||
gh pr create --title "feat: add new user dashboard component" --body "Implements new dashboard component with user statistics and activity feed."
|
||||
```
|
||||
|
||||
## Handling Additional Changes
|
||||
|
||||
If you need to make more changes after pushing:
|
||||
|
||||
```bash
|
||||
# Make your changes
|
||||
git add -A
|
||||
git commit -m "fix: address review feedback"
|
||||
git push
|
||||
```
|
||||
|
||||
The PR will automatically update with the new commits.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Branch already exists
|
||||
|
||||
```bash
|
||||
git checkout <existing-branch-name>
|
||||
```
|
||||
|
||||
### Need to update from main before creating PR
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout <your-branch>
|
||||
git merge main
|
||||
# Resolve conflicts if any
|
||||
git push
|
||||
```
|
||||
|
||||
### PR creation fails
|
||||
|
||||
- Ensure branch is pushed: `git push -u origin <branch-name>`
|
||||
- Check GitHub CLI authentication: `gh auth status`
|
||||
- Verify repository access permissions
|
||||
- Try creating PR via web interface instead
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep branches focused**: One branch = one feature/fix
|
||||
2. **Write clear commit messages**: Help reviewers understand changes
|
||||
3. **Keep PRs small**: Easier to review and merge
|
||||
4. **Update before creating PR**: Merge latest `main` into your branch
|
||||
5. **Add tests**: Include tests for new features
|
||||
6. **Update documentation**: Keep docs in sync with code changes
|
||||
7. **Request reviews**: Tag relevant team members for review
|
||||
|
||||
## Quick Reference Commands
|
||||
|
||||
```bash
|
||||
# Status check
|
||||
git status
|
||||
|
||||
# Create branch
|
||||
git checkout -b <branch-name>
|
||||
|
||||
# Stage all changes
|
||||
git add -A
|
||||
|
||||
# Commit
|
||||
git commit -m "type: message"
|
||||
|
||||
# Push
|
||||
git push -u origin <branch-name>
|
||||
|
||||
# Create PR (GitHub CLI)
|
||||
gh pr create --title "Title" --body "Description"
|
||||
|
||||
# View PR
|
||||
gh pr view
|
||||
|
||||
# List PRs
|
||||
gh pr list
|
||||
```
|
||||
@@ -1,42 +0,0 @@
|
||||
|
||||
> automaker@1.0.0 dev:server
|
||||
> npm run dev --workspace=apps/server
|
||||
|
||||
|
||||
> @automaker/server@0.1.0 dev
|
||||
> tsx watch src/index.ts
|
||||
|
||||
[dotenv@17.2.3] injecting env (1) from .env -- tip: ⚙️ override existing env vars with { override: true }
|
||||
[Server] ✓ CLAUDE_CODE_OAUTH_TOKEN detected (subscription auth)
|
||||
|
||||
╔═══════════════════════════════════════════════════════╗
|
||||
║ Automaker Backend Server ║
|
||||
╠═══════════════════════════════════════════════════════╣
|
||||
║ HTTP API: http://localhost:3008 ║
|
||||
║ WebSocket: ws://localhost:3008/api/events ║
|
||||
║ Terminal: ws://localhost:3008/api/terminal/ws ║
|
||||
║ Health: http://localhost:3008/api/health ║
|
||||
║ Terminal: enabled ║
|
||||
╚═══════════════════════════════════════════════════════╝
|
||||
|
||||
[Server] Agent service initialized
|
||||
[WebSocket] Client connected
|
||||
12:52:41 AM [tsx] change in ./src\index.ts Rerunning...
|
||||
c12:52:41 AM [tsx] unlink in ./src\services\auto-mode-service.ts Restarting...
|
||||
c[dotenv@17.2.3] injecting env (1) from .env -- tip: 🔐 encrypt with Dotenvx: https://dotenvx.com
|
||||
[Server] ✓ CLAUDE_CODE_OAUTH_TOKEN detected (subscription auth)
|
||||
|
||||
╔═══════════════════════════════════════════════════════╗
|
||||
║ Automaker Backend Server ║
|
||||
╠═══════════════════════════════════════════════════════╣
|
||||
║ HTTP API: http://localhost:3008 ║
|
||||
║ WebSocket: ws://localhost:3008/api/events ║
|
||||
║ Terminal: ws://localhost:3008/api/terminal/ws ║
|
||||
║ Health: http://localhost:3008/api/health ║
|
||||
║ Terminal: enabled ║
|
||||
╚═══════════════════════════════════════════════════════╝
|
||||
|
||||
[Server] Agent service initialized
|
||||
[WebSocket] Client connected
|
||||
[WebSocket] Client disconnected
|
||||
^C^C
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": []
|
||||
}
|
||||
Reference in New Issue
Block a user