feat: add PR build check workflow and enhance feature management

- Introduced a new GitHub Actions workflow for PR build checks to ensure code quality and consistency.
- Updated `analysis-view.tsx`, `interview-view.tsx`, and `setup-view.tsx` to incorporate a new `Feature` type for better feature management.
- Refactored various components to improve code readability and maintainability.
- Adjusted type imports in `delete-project-dialog.tsx` and `settings-navigation.tsx` for consistency.
- Enhanced project initialization logic in `project-init.ts` to ensure proper type handling.
- Updated Electron API types in `electron.d.ts` for better clarity and functionality.
This commit is contained in:
Cody Seibert
2025-12-10 23:10:04 -05:00
parent f17abc93c2
commit 67a448ce91
8 changed files with 390 additions and 127 deletions

33
.github/workflows/pr-check.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: PR Build Check
on:
pull_request:
branches:
- "*"
push:
branches:
- main
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: app/package-lock.json
- name: Install dependencies
working-directory: ./app
run: npm ci
- name: Run build:electron
working-directory: ./app
run: npm run build:electron

View File

@@ -1,7 +1,12 @@
"use client"; "use client";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useAppStore, FileTreeNode, ProjectAnalysis } from "@/store/app-store"; import {
useAppStore,
FileTreeNode,
ProjectAnalysis,
Feature,
} from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron"; import { getElectronAPI } from "@/lib/electron";
import { import {
Card, Card,
@@ -763,7 +768,17 @@ ${Object.entries(projectAnalysis.filesByExtension)
throw new Error("Features API not available"); throw new Error("Features API not available");
} }
for (const feature of detectedFeatures) { // Convert DetectedFeature to Feature by adding required id and status
for (const detectedFeature of detectedFeatures) {
const feature: Feature = {
id: `feature-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}`,
category: detectedFeature.category,
description: detectedFeature.description,
steps: detectedFeature.steps,
status: "backlog" as const,
};
await api.features.create(currentProject.path, feature); await api.features.create(currentProject.path, feature);
} }

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, useCallback, useRef, useEffect } from "react"; import { useState, useCallback, useRef, useEffect } from "react";
import { useAppStore } from "@/store/app-store"; import { useAppStore, Feature } from "@/store/app-store";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -313,11 +313,11 @@ export function InterviewView() {
); );
// Create initial feature in the features folder // Create initial feature in the features folder
const initialFeature = { const initialFeature: Feature = {
id: `feature-${Date.now()}-0`, id: `feature-${Date.now()}-0`,
category: "Core", category: "Core",
description: "Initial project setup", description: "Initial project setup",
status: "backlog", status: "backlog" as const,
steps: [ steps: [
"Step 1: Review app_spec.txt", "Step 1: Review app_spec.txt",
"Step 2: Set up development environment", "Step 2: Set up development environment",
@@ -325,6 +325,9 @@ export function InterviewView() {
], ],
skipTests: true, skipTests: true,
}; };
if (!api.features) {
throw new Error("Features API not available");
}
await api.features.create(fullProjectPath, initialFeature); await api.features.create(fullProjectPath, initialFeature);
const project = { const project = {

View File

@@ -8,7 +8,7 @@ import {
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import type { Project } from "@/store/app-store"; import type { Project } from "@/lib/electron";
interface DeleteProjectDialogProps { interface DeleteProjectDialogProps {
open: boolean; open: boolean;
@@ -49,14 +49,19 @@ export function DeleteProjectDialog({
<Folder className="w-5 h-5 text-brand-500" /> <Folder className="w-5 h-5 text-brand-500" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="font-medium text-foreground truncate">{project.name}</p> <p className="font-medium text-foreground truncate">
<p className="text-xs text-muted-foreground truncate">{project.path}</p> {project.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{project.path}
</p>
</div> </div>
</div> </div>
)} )}
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
The folder will remain on disk until you permanently delete it from Trash. The folder will remain on disk until you permanently delete it from
Trash.
</p> </p>
<DialogFooter className="gap-2 sm:gap-0"> <DialogFooter className="gap-2 sm:gap-0">

View File

@@ -1,5 +1,5 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Project } from "@/store/app-store"; import type { Project } from "@/lib/electron";
import type { NavigationItem } from "../config/navigation"; import type { NavigationItem } from "../config/navigation";
interface SettingsNavigationProps { interface SettingsNavigationProps {

View File

@@ -61,7 +61,12 @@ function StatusBadge({
status, status,
label, label,
}: { }: {
status: "installed" | "not_installed" | "checking" | "authenticated" | "not_authenticated"; status:
| "installed"
| "not_installed"
| "checking"
| "authenticated"
| "not_authenticated";
label: string; label: string;
}) { }) {
const getStatusConfig = () => { const getStatusConfig = () => {
@@ -128,8 +133,8 @@ function WelcomeStep({ onNext }: { onNext: () => void }) {
Welcome to Automaker Welcome to Automaker
</h2> </h2>
<p className="text-muted-foreground max-w-md mx-auto"> <p className="text-muted-foreground max-w-md mx-auto">
Let&apos;s set up your development environment. We&apos;ll check for required Let&apos;s set up your development environment. We&apos;ll check for
CLI tools and help you configure them. required CLI tools and help you configure them.
</p> </p>
</div> </div>
@@ -143,7 +148,8 @@ function WelcomeStep({ onNext }: { onNext: () => void }) {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Anthropic&apos;s powerful AI assistant for code generation and analysis Anthropic&apos;s powerful AI assistant for code generation and
analysis
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@@ -200,7 +206,9 @@ function ClaudeSetupStep({
const [isChecking, setIsChecking] = useState(false); const [isChecking, setIsChecking] = useState(false);
const [isInstalling, setIsInstalling] = useState(false); const [isInstalling, setIsInstalling] = useState(false);
const [authMethod, setAuthMethod] = useState<"token" | "api_key" | null>(null); const [authMethod, setAuthMethod] = useState<"token" | "api_key" | null>(
null
);
const [oauthToken, setOAuthToken] = useState(""); const [oauthToken, setOAuthToken] = useState("");
const [apiKey, setApiKey] = useState(""); const [apiKey, setApiKey] = useState("");
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
@@ -213,9 +221,18 @@ function ClaudeSetupStep({
const setupApi = api.setup; const setupApi = api.setup;
// Debug: Check what's available // Debug: Check what's available
console.log("[Claude Setup] isElectron:", typeof window !== "undefined" && (window as any).isElectron); console.log(
console.log("[Claude Setup] electronAPI exists:", typeof window !== "undefined" && !!(window as any).electronAPI); "[Claude Setup] isElectron:",
console.log("[Claude Setup] electronAPI.setup exists:", typeof window !== "undefined" && !!(window as any).electronAPI?.setup); typeof window !== "undefined" && (window as any).isElectron
);
console.log(
"[Claude Setup] electronAPI exists:",
typeof window !== "undefined" && !!(window as any).electronAPI
);
console.log(
"[Claude Setup] electronAPI.setup exists:",
typeof window !== "undefined" && !!(window as any).electronAPI?.setup
);
console.log("[Claude Setup] Setup API available:", !!setupApi); console.log("[Claude Setup] Setup API available:", !!setupApi);
if (setupApi?.getClaudeStatus) { if (setupApi?.getClaudeStatus) {
@@ -224,7 +241,7 @@ function ClaudeSetupStep({
if (result.success) { if (result.success) {
const cliStatus = { const cliStatus = {
installed: result.installed || result.status === "installed", installed: result.status === "installed",
path: result.path || null, path: result.path || null,
version: result.version || null, version: result.version || null,
method: result.method || "none", method: result.method || "none",
@@ -235,14 +252,16 @@ function ClaudeSetupStep({
if (result.auth) { if (result.auth) {
const authStatus = { const authStatus = {
authenticated: result.auth.authenticated, authenticated: result.auth.authenticated,
method: result.auth.method === "oauth_token" method:
? "oauth" result.auth.method === "oauth_token"
: result.auth.method?.includes("api_key") ? "oauth"
? "api_key" : result.auth.method?.includes("api_key")
: "none", ? "api_key"
: "none",
hasCredentialsFile: false, hasCredentialsFile: false,
oauthTokenValid: result.auth.hasStoredOAuthToken, oauthTokenValid: result.auth.hasStoredOAuthToken,
apiKeyValid: result.auth.hasStoredApiKey || result.auth.hasEnvApiKey, apiKeyValid:
result.auth.hasStoredApiKey || result.auth.hasEnvApiKey,
}; };
console.log("[Claude Setup] Auth Status:", authStatus); console.log("[Claude Setup] Auth Status:", authStatus);
setClaudeAuthStatus(authStatus as any); setClaudeAuthStatus(authStatus as any);
@@ -274,13 +293,18 @@ function ClaudeSetupStep({
const setupApi = api.setup; const setupApi = api.setup;
if (setupApi?.installClaude) { if (setupApi?.installClaude) {
const unsubscribe = setupApi.onInstallProgress?.((progress: { cli?: string; data?: string; type?: string }) => { const unsubscribe = setupApi.onInstallProgress?.(
if (progress.cli === "claude") { (progress: { cli?: string; data?: string; type?: string }) => {
setClaudeInstallProgress({ if (progress.cli === "claude") {
output: [...claudeInstallProgress.output, progress.data || progress.type || ""], setClaudeInstallProgress({
}); output: [
...claudeInstallProgress.output,
progress.data || progress.type || "",
],
});
}
} }
}); );
const result = await setupApi.installClaude(); const result = await setupApi.installClaude();
unsubscribe?.(); unsubscribe?.();
@@ -292,14 +316,14 @@ function ClaudeSetupStep({
let detected = false; let detected = false;
// Initial delay to let the installation script finish setting up // Initial delay to let the installation script finish setting up
await new Promise(resolve => setTimeout(resolve, 1500)); await new Promise((resolve) => setTimeout(resolve, 1500));
for (let i = 0; i < retries; i++) { for (let i = 0; i < retries; i++) {
// Check status // Check status
await checkStatus(); await checkStatus();
// Small delay to let state update // Small delay to let state update
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise((resolve) => setTimeout(resolve, 300));
// Check if CLI is now detected by re-reading from store // Check if CLI is now detected by re-reading from store
const currentStatus = useSetupStore.getState().claudeCliStatus; const currentStatus = useSetupStore.getState().claudeCliStatus;
@@ -311,7 +335,9 @@ function ClaudeSetupStep({
// Wait before next retry (longer delays for later retries) // Wait before next retry (longer delays for later retries)
if (i < retries - 1) { if (i < retries - 1) {
await new Promise(resolve => setTimeout(resolve, 2000 + (i * 500))); await new Promise((resolve) =>
setTimeout(resolve, 2000 + i * 500)
);
} }
} }
@@ -319,7 +345,8 @@ function ClaudeSetupStep({
if (!detected) { if (!detected) {
// Installation completed but CLI not detected - this is common if PATH wasn't updated in current process // Installation completed but CLI not detected - this is common if PATH wasn't updated in current process
toast.success("Claude CLI installation completed", { toast.success("Claude CLI installation completed", {
description: "The CLI was installed but may need a terminal restart to be detected. You can continue with authentication if you have a token.", description:
"The CLI was installed but may need a terminal restart to be detected. You can continue with authentication if you have a token.",
duration: 7000, duration: 7000,
}); });
} }
@@ -349,7 +376,10 @@ function ClaudeSetupStep({
const setupApi = api.setup; const setupApi = api.setup;
if (setupApi?.storeApiKey) { if (setupApi?.storeApiKey) {
const result = await setupApi.storeApiKey("anthropic_oauth_token", oauthToken); const result = await setupApi.storeApiKey(
"anthropic_oauth_token",
oauthToken
);
console.log("[Claude Setup] Store OAuth token result:", result); console.log("[Claude Setup] Store OAuth token result:", result);
if (result.success) { if (result.success) {
@@ -434,7 +464,8 @@ function ClaudeSetupStep({
const getAuthMethodLabel = () => { const getAuthMethodLabel = () => {
if (!isAuthenticated) return null; if (!isAuthenticated) return null;
if (claudeAuthStatus?.method === "oauth") return "Subscription Token"; if (claudeAuthStatus?.method === "oauth") return "Subscription Token";
if (apiKeys.anthropic || claudeAuthStatus?.method === "api_key") return "API Key"; if (apiKeys.anthropic || claudeAuthStatus?.method === "api_key")
return "API Key";
return "Authenticated"; return "Authenticated";
}; };
@@ -457,8 +488,15 @@ function ClaudeSetupStep({
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-lg">Status</CardTitle> <CardTitle className="text-lg">Status</CardTitle>
<Button variant="ghost" size="sm" onClick={checkStatus} disabled={isChecking}> <Button
<RefreshCw className={`w-4 h-4 ${isChecking ? "animate-spin" : ""}`} /> variant="ghost"
size="sm"
onClick={checkStatus}
disabled={isChecking}
>
<RefreshCw
className={`w-4 h-4 ${isChecking ? "animate-spin" : ""}`}
/>
</Button> </Button>
</div> </div>
</CardHeader> </CardHeader>
@@ -477,7 +515,9 @@ function ClaudeSetupStep({
{claudeCliStatus?.version && ( {claudeCliStatus?.version && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Version</span> <span className="text-sm text-muted-foreground">Version</span>
<span className="text-sm font-mono text-foreground">{claudeCliStatus.version}</span> <span className="text-sm font-mono text-foreground">
{claudeCliStatus.version}
</span>
</div> </div>
)} )}
@@ -487,11 +527,16 @@ function ClaudeSetupStep({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<StatusBadge status="authenticated" label="Authenticated" /> <StatusBadge status="authenticated" label="Authenticated" />
{getAuthMethodLabel() && ( {getAuthMethodLabel() && (
<span className="text-xs text-muted-foreground">({getAuthMethodLabel()})</span> <span className="text-xs text-muted-foreground">
({getAuthMethodLabel()})
</span>
)} )}
</div> </div>
) : ( ) : (
<StatusBadge status="not_authenticated" label="Not Authenticated" /> <StatusBadge
status="not_authenticated"
label="Not Authenticated"
/>
)} )}
</div> </div>
</CardContent> </CardContent>
@@ -505,16 +550,28 @@ function ClaudeSetupStep({
<Download className="w-5 h-5" /> <Download className="w-5 h-5" />
Install Claude CLI Install Claude CLI
</CardTitle> </CardTitle>
<CardDescription>Required for subscription-based authentication</CardDescription> <CardDescription>
Required for subscription-based authentication
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm text-muted-foreground">macOS / Linux</Label> <Label className="text-sm text-muted-foreground">
macOS / Linux
</Label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<code className="flex-1 bg-muted px-3 py-2 rounded text-sm font-mono text-foreground"> <code className="flex-1 bg-muted px-3 py-2 rounded text-sm font-mono text-foreground">
curl -fsSL https://claude.ai/install.sh | bash curl -fsSL https://claude.ai/install.sh | bash
</code> </code>
<Button variant="ghost" size="icon" onClick={() => copyCommand("curl -fsSL https://claude.ai/install.sh | bash")}> <Button
variant="ghost"
size="icon"
onClick={() =>
copyCommand(
"curl -fsSL https://claude.ai/install.sh | bash"
)
}
>
<Copy className="w-4 h-4" /> <Copy className="w-4 h-4" />
</Button> </Button>
</div> </div>
@@ -526,13 +583,21 @@ function ClaudeSetupStep({
<code className="flex-1 bg-muted px-3 py-2 rounded text-sm font-mono text-foreground"> <code className="flex-1 bg-muted px-3 py-2 rounded text-sm font-mono text-foreground">
irm https://claude.ai/install.ps1 | iex irm https://claude.ai/install.ps1 | iex
</code> </code>
<Button variant="ghost" size="icon" onClick={() => copyCommand("irm https://claude.ai/install.ps1 | iex")}> <Button
variant="ghost"
size="icon"
onClick={() =>
copyCommand("irm https://claude.ai/install.ps1 | iex")
}
>
<Copy className="w-4 h-4" /> <Copy className="w-4 h-4" />
</Button> </Button>
</div> </div>
</div> </div>
{claudeInstallProgress.isInstalling && <TerminalOutput lines={claudeInstallProgress.output} />} {claudeInstallProgress.isInstalling && (
<TerminalOutput lines={claudeInstallProgress.output} />
)}
<Button <Button
onClick={handleInstall} onClick={handleInstall}
@@ -573,25 +638,37 @@ function ClaudeSetupStep({
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Shield className="w-5 h-5 text-brand-500 mt-0.5" /> <Shield className="w-5 h-5 text-brand-500 mt-0.5" />
<div className="flex-1"> <div className="flex-1">
<p className="font-medium text-foreground">Subscription Token</p> <p className="font-medium text-foreground">
<p className="text-sm text-muted-foreground mb-3">Use your Claude subscription (no API charges)</p> Subscription Token
</p>
<p className="text-sm text-muted-foreground mb-3">
Use your Claude subscription (no API charges)
</p>
{claudeCliStatus?.installed ? ( {claudeCliStatus?.installed ? (
<> <>
<div className="mb-3"> <div className="mb-3">
<p className="text-sm text-muted-foreground mb-2">1. Run this command in your terminal:</p> <p className="text-sm text-muted-foreground mb-2">
1. Run this command in your terminal:
</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<code className="flex-1 bg-muted px-3 py-2 rounded text-sm font-mono text-foreground"> <code className="flex-1 bg-muted px-3 py-2 rounded text-sm font-mono text-foreground">
claude setup-token claude setup-token
</code> </code>
<Button variant="ghost" size="icon" onClick={() => copyCommand("claude setup-token")}> <Button
variant="ghost"
size="icon"
onClick={() => copyCommand("claude setup-token")}
>
<Copy className="w-4 h-4" /> <Copy className="w-4 h-4" />
</Button> </Button>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-foreground">2. Paste the token here:</Label> <Label className="text-foreground">
2. Paste the token here:
</Label>
<Input <Input
type="password" type="password"
placeholder="Paste token from claude setup-token..." placeholder="Paste token from claude setup-token..."
@@ -603,7 +680,11 @@ function ClaudeSetupStep({
</div> </div>
<div className="flex gap-2 mt-3"> <div className="flex gap-2 mt-3">
<Button variant="outline" onClick={() => setAuthMethod(null)} className="border-border"> <Button
variant="outline"
onClick={() => setAuthMethod(null)}
className="border-border"
>
Cancel Cancel
</Button> </Button>
<Button <Button
@@ -612,7 +693,11 @@ function ClaudeSetupStep({
className="flex-1 bg-brand-500 hover:bg-brand-600 text-white" className="flex-1 bg-brand-500 hover:bg-brand-600 text-white"
data-testid="save-oauth-token-button" data-testid="save-oauth-token-button"
> >
{isSaving ? <Loader2 className="w-4 h-4 animate-spin" /> : "Save Token"} {isSaving ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
"Save Token"
)}
</Button> </Button>
</div> </div>
</> </>
@@ -620,7 +705,8 @@ function ClaudeSetupStep({
<div className="p-3 rounded bg-yellow-500/10 border border-yellow-500/20"> <div className="p-3 rounded bg-yellow-500/10 border border-yellow-500/20">
<p className="text-sm text-yellow-600"> <p className="text-sm text-yellow-600">
<AlertCircle className="w-4 h-4 inline mr-1" /> <AlertCircle className="w-4 h-4 inline mr-1" />
Install Claude CLI first to use subscription authentication Install Claude CLI first to use subscription
authentication
</p> </p>
</div> </div>
)} )}
@@ -634,10 +720,17 @@ function ClaudeSetupStep({
<Key className="w-5 h-5 text-green-500 mt-0.5" /> <Key className="w-5 h-5 text-green-500 mt-0.5" />
<div className="flex-1"> <div className="flex-1">
<p className="font-medium text-foreground">API Key</p> <p className="font-medium text-foreground">API Key</p>
<p className="text-sm text-muted-foreground mb-3">Pay-per-use with your Anthropic API key</p> <p className="text-sm text-muted-foreground mb-3">
Pay-per-use with your Anthropic API key
</p>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="anthropic-key" className="text-foreground">Anthropic API Key</Label> <Label
htmlFor="anthropic-key"
className="text-foreground"
>
Anthropic API Key
</Label>
<Input <Input
id="anthropic-key" id="anthropic-key"
type="password" type="password"
@@ -662,7 +755,11 @@ function ClaudeSetupStep({
</div> </div>
<div className="flex gap-2 mt-3"> <div className="flex gap-2 mt-3">
<Button variant="outline" onClick={() => setAuthMethod(null)} className="border-border"> <Button
variant="outline"
onClick={() => setAuthMethod(null)}
className="border-border"
>
Cancel Cancel
</Button> </Button>
<Button <Button
@@ -671,7 +768,11 @@ function ClaudeSetupStep({
className="flex-1 bg-green-500 hover:bg-green-600 text-white" className="flex-1 bg-green-500 hover:bg-green-600 text-white"
data-testid="save-anthropic-key-button" data-testid="save-anthropic-key-button"
> >
{isSaving ? <Loader2 className="w-4 h-4 animate-spin" /> : "Save API Key"} {isSaving ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
"Save API Key"
)}
</Button> </Button>
</div> </div>
</div> </div>
@@ -688,9 +789,15 @@ function ClaudeSetupStep({
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Shield className="w-6 h-6 text-brand-500" /> <Shield className="w-6 h-6 text-brand-500" />
<div> <div>
<p className="font-medium text-foreground">Subscription</p> <p className="font-medium text-foreground">
<p className="text-sm text-muted-foreground mt-1">Use your Claude subscription</p> Subscription
<p className="text-xs text-brand-500 mt-2">No API charges</p> </p>
<p className="text-sm text-muted-foreground mt-1">
Use your Claude subscription
</p>
<p className="text-xs text-brand-500 mt-2">
No API charges
</p>
</div> </div>
</div> </div>
</button> </button>
@@ -704,7 +811,9 @@ function ClaudeSetupStep({
<Key className="w-6 h-6 text-green-500" /> <Key className="w-6 h-6 text-green-500" />
<div> <div>
<p className="font-medium text-foreground">API Key</p> <p className="font-medium text-foreground">API Key</p>
<p className="text-sm text-muted-foreground mt-1">Use Anthropic API key</p> <p className="text-sm text-muted-foreground mt-1">
Use Anthropic API key
</p>
<p className="text-xs text-green-500 mt-2">Pay-per-use</p> <p className="text-xs text-green-500 mt-2">Pay-per-use</p>
</div> </div>
</div> </div>
@@ -724,9 +833,12 @@ function ClaudeSetupStep({
<CheckCircle2 className="w-6 h-6 text-green-500" /> <CheckCircle2 className="w-6 h-6 text-green-500" />
</div> </div>
<div> <div>
<p className="font-medium text-foreground">Claude is ready to use!</p> <p className="font-medium text-foreground">
Claude is ready to use!
</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{getAuthMethodLabel() && `Using ${getAuthMethodLabel()}. `}You can proceed to the next step {getAuthMethodLabel() && `Using ${getAuthMethodLabel()}. `}You
can proceed to the next step
</p> </p>
</div> </div>
</div> </div>
@@ -736,15 +848,27 @@ function ClaudeSetupStep({
{/* Navigation */} {/* Navigation */}
<div className="flex justify-between pt-4"> <div className="flex justify-between pt-4">
<Button variant="ghost" onClick={onBack} className="text-muted-foreground"> <Button
variant="ghost"
onClick={onBack}
className="text-muted-foreground"
>
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />
Back Back
</Button> </Button>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="ghost" onClick={onSkip} className="text-muted-foreground"> <Button
variant="ghost"
onClick={onSkip}
className="text-muted-foreground"
>
Skip for now Skip for now
</Button> </Button>
<Button onClick={onNext} className="bg-brand-500 hover:bg-brand-600 text-white" data-testid="claude-next-button"> <Button
onClick={onNext}
className="bg-brand-500 hover:bg-brand-600 text-white"
data-testid="claude-next-button"
>
Continue Continue
<ArrowRight className="w-4 h-4 ml-2" /> <ArrowRight className="w-4 h-4 ml-2" />
</Button> </Button>
@@ -804,7 +928,10 @@ function CodexSetupStep({
const setupApi = api.setup; const setupApi = api.setup;
console.log("[Codex Setup] Setup API available:", !!setupApi); console.log("[Codex Setup] Setup API available:", !!setupApi);
console.log("[Codex Setup] getCodexStatus available:", !!setupApi?.getCodexStatus); console.log(
"[Codex Setup] getCodexStatus available:",
!!setupApi?.getCodexStatus
);
if (setupApi?.getCodexStatus) { if (setupApi?.getCodexStatus) {
const result = await setupApi.getCodexStatus(); const result = await setupApi.getCodexStatus();
@@ -827,7 +954,10 @@ function CodexSetupStep({
authenticated: result.auth.authenticated, authenticated: result.auth.authenticated,
method, method,
// Only set apiKeyValid for actual API key methods, not CLI login // Only set apiKeyValid for actual API key methods, not CLI login
apiKeyValid: method === "cli_verified" || method === "cli_tokens" ? undefined : result.auth.authenticated, apiKeyValid:
method === "cli_verified" || method === "cli_tokens"
? undefined
: result.auth.authenticated,
}; };
console.log("[Codex Setup] Auth Status:", authStatus); console.log("[Codex Setup] Auth Status:", authStatus);
setCodexAuthStatus(authStatus); setCodexAuthStatus(authStatus);
@@ -866,16 +996,18 @@ function CodexSetupStep({
const setupApi = api.setup; const setupApi = api.setup;
if (setupApi?.installCodex) { if (setupApi?.installCodex) {
const unsubscribe = setupApi.onInstallProgress?.((progress: { cli?: string; data?: string; type?: string }) => { const unsubscribe = setupApi.onInstallProgress?.(
if (progress.cli === "codex") { (progress: { cli?: string; data?: string; type?: string }) => {
setCodexInstallProgress({ if (progress.cli === "codex") {
output: [ setCodexInstallProgress({
...codexInstallProgress.output, output: [
progress.data || progress.type || "", ...codexInstallProgress.output,
], progress.data || progress.type || "",
}); ],
});
}
} }
}); );
const result = await setupApi.installCodex(); const result = await setupApi.installCodex();
@@ -912,7 +1044,10 @@ function CodexSetupStep({
const api = getElectronAPI(); const api = getElectronAPI();
const setupApi = api.setup; const setupApi = api.setup;
console.log("[Codex Setup] storeApiKey available:", !!setupApi?.storeApiKey); console.log(
"[Codex Setup] storeApiKey available:",
!!setupApi?.storeApiKey
);
if (setupApi?.storeApiKey) { if (setupApi?.storeApiKey) {
console.log("[Codex Setup] Calling storeApiKey for openai..."); console.log("[Codex Setup] Calling storeApiKey for openai...");
@@ -920,7 +1055,9 @@ function CodexSetupStep({
console.log("[Codex Setup] storeApiKey result:", result); console.log("[Codex Setup] storeApiKey result:", result);
if (result.success) { if (result.success) {
console.log("[Codex Setup] API key stored successfully, updating state..."); console.log(
"[Codex Setup] API key stored successfully, updating state..."
);
setApiKeys({ ...apiKeys, openai: apiKey }); setApiKeys({ ...apiKeys, openai: apiKey });
setCodexAuthStatus({ setCodexAuthStatus({
authenticated: true, authenticated: true,
@@ -933,7 +1070,9 @@ function CodexSetupStep({
console.log("[Codex Setup] Failed to store API key:", result.error); console.log("[Codex Setup] Failed to store API key:", result.error);
} }
} else { } else {
console.log("[Codex Setup] Web mode - storing API key in app state only"); console.log(
"[Codex Setup] Web mode - storing API key in app state only"
);
setApiKeys({ ...apiKeys, openai: apiKey }); setApiKeys({ ...apiKeys, openai: apiKey });
setCodexAuthStatus({ setCodexAuthStatus({
authenticated: true, authenticated: true,
@@ -963,7 +1102,8 @@ function CodexSetupStep({
if (apiKeys.openai) return "API Key (Manual)"; if (apiKeys.openai) return "API Key (Manual)";
if (codexAuthStatus?.method === "api_key") return "API Key (Auth File)"; if (codexAuthStatus?.method === "api_key") return "API Key (Auth File)";
if (codexAuthStatus?.method === "env") return "API Key (Environment)"; if (codexAuthStatus?.method === "env") return "API Key (Environment)";
if (codexAuthStatus?.method === "cli_verified") return "CLI Login (ChatGPT)"; if (codexAuthStatus?.method === "cli_verified")
return "CLI Login (ChatGPT)";
return "Authenticated"; return "Authenticated";
}; };
@@ -1031,7 +1171,10 @@ function CodexSetupStep({
)} )}
</div> </div>
) : ( ) : (
<StatusBadge status="not_authenticated" label="Not Authenticated" /> <StatusBadge
status="not_authenticated"
label="Not Authenticated"
/>
)} )}
</div> </div>
</CardContent> </CardContent>
@@ -1114,9 +1257,7 @@ function CodexSetupStep({
<Key className="w-5 h-5" /> <Key className="w-5 h-5" />
Authentication Authentication
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>Codex requires an OpenAI API key</CardDescription>
Codex requires an OpenAI API key
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{codexCliStatus?.installed && ( {codexCliStatus?.installed && (
@@ -1236,7 +1377,8 @@ function CodexSetupStep({
Codex is ready to use! Codex is ready to use!
</p> </p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{getAuthMethodLabel() && `Authenticated via ${getAuthMethodLabel()}. `} {getAuthMethodLabel() &&
`Authenticated via ${getAuthMethodLabel()}. `}
You can proceed to complete setup You can proceed to complete setup
</p> </p>
</div> </div>
@@ -1381,22 +1523,34 @@ function CompleteStep({ onFinish }: { onFinish: () => void }) {
// Main Setup View // Main Setup View
export function SetupView() { export function SetupView() {
const { currentStep, setCurrentStep, completeSetup, setSkipClaudeSetup, setSkipCodexSetup } = const {
useSetupStore(); currentStep,
setCurrentStep,
completeSetup,
setSkipClaudeSetup,
setSkipCodexSetup,
} = useSetupStore();
const { setCurrentView } = useAppStore(); const { setCurrentView } = useAppStore();
const steps = ["welcome", "claude", "codex", "complete"] as const; const steps = ["welcome", "claude", "codex", "complete"] as const;
type StepName = typeof steps[number]; type StepName = (typeof steps)[number];
const getStepName = (): StepName => { const getStepName = (): StepName => {
if (currentStep === "claude_detect" || currentStep === "claude_auth") return "claude"; if (currentStep === "claude_detect" || currentStep === "claude_auth")
if (currentStep === "codex_detect" || currentStep === "codex_auth") return "codex"; return "claude";
if (currentStep === "codex_detect" || currentStep === "codex_auth")
return "codex";
if (currentStep === "welcome") return "welcome"; if (currentStep === "welcome") return "welcome";
return "complete"; return "complete";
}; };
const currentIndex = steps.indexOf(getStepName()); const currentIndex = steps.indexOf(getStepName());
const handleNext = (from: string) => { const handleNext = (from: string) => {
console.log("[Setup Flow] handleNext called from:", from, "currentStep:", currentStep); console.log(
"[Setup Flow] handleNext called from:",
from,
"currentStep:",
currentStep
);
switch (from) { switch (from) {
case "welcome": case "welcome":
console.log("[Setup Flow] Moving to claude_detect step"); console.log("[Setup Flow] Moving to claude_detect step");
@@ -1445,10 +1599,7 @@ export function SetupView() {
}; };
return ( return (
<div <div className="h-full flex flex-col content-bg" data-testid="setup-view">
className="h-full flex flex-col content-bg"
data-testid="setup-view"
>
{/* Header */} {/* Header */}
<div className="flex-shrink-0 border-b border-border bg-glass backdrop-blur-md titlebar-drag-region"> <div className="flex-shrink-0 border-b border-border bg-glass backdrop-blur-md titlebar-drag-region">
<div className="px-8 py-4"> <div className="px-8 py-4">
@@ -1467,7 +1618,10 @@ export function SetupView() {
<div className="p-8"> <div className="p-8">
<div className="w-full max-w-2xl mx-auto"> <div className="w-full max-w-2xl mx-auto">
<div className="mb-8"> <div className="mb-8">
<StepIndicator currentStep={currentIndex} totalSteps={steps.length} /> <StepIndicator
currentStep={currentIndex}
totalSteps={steps.length}
/>
</div> </div>
<div className="py-8"> <div className="py-8">

View File

@@ -57,7 +57,7 @@ export async function initializeProject(
const exists = await api.exists(fullPath); const exists = await api.exists(fullPath);
if (!exists) { if (!exists) {
await api.writeFile(fullPath, defaultContent); await api.writeFile(fullPath, defaultContent as string);
createdFiles.push(relativePath); createdFiles.push(relativePath);
} else { } else {
existingFiles.push(relativePath); existingFiles.push(relativePath);

View File

@@ -272,7 +272,10 @@ export interface SpecRegenerationAPI {
} }
export interface AutoModeAPI { export interface AutoModeAPI {
start: (projectPath: string, maxConcurrency?: number) => Promise<{ start: (
projectPath: string,
maxConcurrency?: number
) => Promise<{
success: boolean; success: boolean;
error?: string; error?: string;
}>; }>;
@@ -299,25 +302,38 @@ export interface AutoModeAPI {
error?: string; error?: string;
}>; }>;
runFeature: (projectPath: string, featureId: string, useWorktrees?: boolean) => Promise<{ runFeature: (
projectPath: string,
featureId: string,
useWorktrees?: boolean
) => Promise<{
success: boolean; success: boolean;
passes?: boolean; passes?: boolean;
error?: string; error?: string;
}>; }>;
verifyFeature: (projectPath: string, featureId: string) => Promise<{ verifyFeature: (
projectPath: string,
featureId: string
) => Promise<{
success: boolean; success: boolean;
passes?: boolean; passes?: boolean;
error?: string; error?: string;
}>; }>;
resumeFeature: (projectPath: string, featureId: string) => Promise<{ resumeFeature: (
projectPath: string,
featureId: string
) => Promise<{
success: boolean; success: boolean;
passes?: boolean; passes?: boolean;
error?: string; error?: string;
}>; }>;
contextExists: (projectPath: string, featureId: string) => Promise<{ contextExists: (
projectPath: string,
featureId: string
) => Promise<{
success: boolean; success: boolean;
exists?: boolean; exists?: boolean;
error?: string; error?: string;
@@ -329,13 +345,21 @@ export interface AutoModeAPI {
error?: string; error?: string;
}>; }>;
followUpFeature: (projectPath: string, featureId: string, prompt: string, imagePaths?: string[]) => Promise<{ followUpFeature: (
projectPath: string,
featureId: string,
prompt: string,
imagePaths?: string[]
) => Promise<{
success: boolean; success: boolean;
passes?: boolean; passes?: boolean;
error?: string; error?: string;
}>; }>;
commitFeature: (projectPath: string, featureId: string) => Promise<{ commitFeature: (
projectPath: string,
featureId: string
) => Promise<{
success: boolean; success: boolean;
error?: string; error?: string;
}>; }>;
@@ -345,6 +369,9 @@ export interface AutoModeAPI {
export interface ElectronAPI { export interface ElectronAPI {
ping: () => Promise<string>; ping: () => Promise<string>;
openExternalLink: (
url: string
) => Promise<{ success: boolean; error?: string }>;
// Dialog APIs // Dialog APIs
openDirectory: () => Promise<{ openDirectory: () => Promise<{
@@ -362,7 +389,10 @@ export interface ElectronAPI {
content?: string; content?: string;
error?: string; error?: string;
}>; }>;
writeFile: (filePath: string, content: string) => Promise<{ writeFile: (
filePath: string,
content: string
) => Promise<{
success: boolean; success: boolean;
error?: string; error?: string;
}>; }>;
@@ -525,25 +555,35 @@ export interface FileDiffResult {
export interface WorktreeAPI { export interface WorktreeAPI {
// Revert feature changes by removing the worktree // Revert feature changes by removing the worktree
revertFeature: (projectPath: string, featureId: string) => Promise<{ revertFeature: (
projectPath: string,
featureId: string
) => Promise<{
success: boolean; success: boolean;
removedPath?: string; removedPath?: string;
error?: string; error?: string;
}>; }>;
// Merge feature worktree changes back to main branch // Merge feature worktree changes back to main branch
mergeFeature: (projectPath: string, featureId: string, options?: { mergeFeature: (
squash?: boolean; projectPath: string,
commitMessage?: string; featureId: string,
squashMessage?: string; options?: {
}) => Promise<{ squash?: boolean;
commitMessage?: string;
squashMessage?: string;
}
) => Promise<{
success: boolean; success: boolean;
mergedBranch?: string; mergedBranch?: string;
error?: string; error?: string;
}>; }>;
// Get worktree info for a feature // Get worktree info for a feature
getInfo: (projectPath: string, featureId: string) => Promise<{ getInfo: (
projectPath: string,
featureId: string
) => Promise<{
success: boolean; success: boolean;
worktreePath?: string; worktreePath?: string;
branchName?: string; branchName?: string;
@@ -552,7 +592,10 @@ export interface WorktreeAPI {
}>; }>;
// Get worktree status (changed files, commits) // Get worktree status (changed files, commits)
getStatus: (projectPath: string, featureId: string) => Promise<WorktreeStatus>; getStatus: (
projectPath: string,
featureId: string
) => Promise<WorktreeStatus>;
// List all feature worktrees // List all feature worktrees
list: (projectPath: string) => Promise<{ list: (projectPath: string) => Promise<{
@@ -562,10 +605,17 @@ export interface WorktreeAPI {
}>; }>;
// Get file diffs for a feature worktree // Get file diffs for a feature worktree
getDiffs: (projectPath: string, featureId: string) => Promise<FileDiffsResult>; getDiffs: (
projectPath: string,
featureId: string
) => Promise<FileDiffsResult>;
// Get diff for a specific file in a worktree // Get diff for a specific file in a worktree
getFileDiff: (projectPath: string, featureId: string, filePath: string) => Promise<FileDiffResult>; getFileDiff: (
projectPath: string,
featureId: string,
filePath: string
) => Promise<FileDiffResult>;
} }
export interface GitAPI { export interface GitAPI {
@@ -573,7 +623,10 @@ export interface GitAPI {
getDiffs: (projectPath: string) => Promise<FileDiffsResult>; getDiffs: (projectPath: string) => Promise<FileDiffsResult>;
// Get diff for a specific file in the main project // Get diff for a specific file in the main project
getFileDiff: (projectPath: string, filePath: string) => Promise<FileDiffResult>; getFileDiff: (
projectPath: string,
filePath: string
) => Promise<FileDiffResult>;
} }
// Model definition type // Model definition type