mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
33
.github/workflows/pr-check.yml
vendored
Normal file
33
.github/workflows/pr-check.yml
vendored
Normal 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
|
||||
@@ -1,7 +1,12 @@
|
||||
"use client";
|
||||
|
||||
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 {
|
||||
Card,
|
||||
@@ -763,7 +768,17 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
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 { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -313,11 +313,11 @@ export function InterviewView() {
|
||||
);
|
||||
|
||||
// Create initial feature in the features folder
|
||||
const initialFeature = {
|
||||
const initialFeature: Feature = {
|
||||
id: `feature-${Date.now()}-0`,
|
||||
category: "Core",
|
||||
description: "Initial project setup",
|
||||
status: "backlog",
|
||||
status: "backlog" as const,
|
||||
steps: [
|
||||
"Step 1: Review app_spec.txt",
|
||||
"Step 2: Set up development environment",
|
||||
@@ -325,6 +325,9 @@ export function InterviewView() {
|
||||
],
|
||||
skipTests: true,
|
||||
};
|
||||
if (!api.features) {
|
||||
throw new Error("Features API not available");
|
||||
}
|
||||
await api.features.create(fullProjectPath, initialFeature);
|
||||
|
||||
const project = {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Project } from "@/store/app-store";
|
||||
import type { Project } from "@/lib/electron";
|
||||
|
||||
interface DeleteProjectDialogProps {
|
||||
open: boolean;
|
||||
@@ -49,14 +49,19 @@ export function DeleteProjectDialog({
|
||||
<Folder className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-foreground truncate">{project.name}</p>
|
||||
<p className="text-xs text-muted-foreground truncate">{project.path}</p>
|
||||
<p className="font-medium text-foreground truncate">
|
||||
{project.name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{project.path}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Project } from "@/store/app-store";
|
||||
import type { Project } from "@/lib/electron";
|
||||
import type { NavigationItem } from "../config/navigation";
|
||||
|
||||
interface SettingsNavigationProps {
|
||||
|
||||
@@ -61,7 +61,12 @@ function StatusBadge({
|
||||
status,
|
||||
label,
|
||||
}: {
|
||||
status: "installed" | "not_installed" | "checking" | "authenticated" | "not_authenticated";
|
||||
status:
|
||||
| "installed"
|
||||
| "not_installed"
|
||||
| "checking"
|
||||
| "authenticated"
|
||||
| "not_authenticated";
|
||||
label: string;
|
||||
}) {
|
||||
const getStatusConfig = () => {
|
||||
@@ -128,8 +133,8 @@ function WelcomeStep({ onNext }: { onNext: () => void }) {
|
||||
Welcome to Automaker
|
||||
</h2>
|
||||
<p className="text-muted-foreground max-w-md mx-auto">
|
||||
Let's set up your development environment. We'll check for required
|
||||
CLI tools and help you configure them.
|
||||
Let's set up your development environment. We'll check for
|
||||
required CLI tools and help you configure them.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -143,7 +148,8 @@ function WelcomeStep({ onNext }: { onNext: () => void }) {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Anthropic's powerful AI assistant for code generation and analysis
|
||||
Anthropic's powerful AI assistant for code generation and
|
||||
analysis
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -200,7 +206,9 @@ function ClaudeSetupStep({
|
||||
|
||||
const [isChecking, setIsChecking] = 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 [apiKey, setApiKey] = useState("");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -213,9 +221,18 @@ function ClaudeSetupStep({
|
||||
const setupApi = api.setup;
|
||||
|
||||
// Debug: Check what's available
|
||||
console.log("[Claude Setup] isElectron:", 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] isElectron:",
|
||||
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);
|
||||
|
||||
if (setupApi?.getClaudeStatus) {
|
||||
@@ -224,7 +241,7 @@ function ClaudeSetupStep({
|
||||
|
||||
if (result.success) {
|
||||
const cliStatus = {
|
||||
installed: result.installed || result.status === "installed",
|
||||
installed: result.status === "installed",
|
||||
path: result.path || null,
|
||||
version: result.version || null,
|
||||
method: result.method || "none",
|
||||
@@ -235,14 +252,16 @@ function ClaudeSetupStep({
|
||||
if (result.auth) {
|
||||
const authStatus = {
|
||||
authenticated: result.auth.authenticated,
|
||||
method: result.auth.method === "oauth_token"
|
||||
? "oauth"
|
||||
: result.auth.method?.includes("api_key")
|
||||
? "api_key"
|
||||
: "none",
|
||||
method:
|
||||
result.auth.method === "oauth_token"
|
||||
? "oauth"
|
||||
: result.auth.method?.includes("api_key")
|
||||
? "api_key"
|
||||
: "none",
|
||||
hasCredentialsFile: false,
|
||||
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);
|
||||
setClaudeAuthStatus(authStatus as any);
|
||||
@@ -274,13 +293,18 @@ function ClaudeSetupStep({
|
||||
const setupApi = api.setup;
|
||||
|
||||
if (setupApi?.installClaude) {
|
||||
const unsubscribe = setupApi.onInstallProgress?.((progress: { cli?: string; data?: string; type?: string }) => {
|
||||
if (progress.cli === "claude") {
|
||||
setClaudeInstallProgress({
|
||||
output: [...claudeInstallProgress.output, progress.data || progress.type || ""],
|
||||
});
|
||||
const unsubscribe = setupApi.onInstallProgress?.(
|
||||
(progress: { cli?: string; data?: string; type?: string }) => {
|
||||
if (progress.cli === "claude") {
|
||||
setClaudeInstallProgress({
|
||||
output: [
|
||||
...claudeInstallProgress.output,
|
||||
progress.data || progress.type || "",
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
const result = await setupApi.installClaude();
|
||||
unsubscribe?.();
|
||||
@@ -290,17 +314,17 @@ function ClaudeSetupStep({
|
||||
// Wait a bit for installation to complete and PATH to update, then retry status check
|
||||
let retries = 5;
|
||||
let detected = false;
|
||||
|
||||
|
||||
// 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++) {
|
||||
// Check status
|
||||
await checkStatus();
|
||||
|
||||
|
||||
// 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
|
||||
const currentStatus = useSetupStore.getState().claudeCliStatus;
|
||||
if (currentStatus?.installed) {
|
||||
@@ -308,18 +332,21 @@ function ClaudeSetupStep({
|
||||
toast.success("Claude CLI installed and detected successfully");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// Wait before next retry (longer delays for later retries)
|
||||
if (i < retries - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000 + (i * 500)));
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, 2000 + i * 500)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show appropriate message based on detection
|
||||
if (!detected) {
|
||||
// Installation completed but CLI not detected - this is common if PATH wasn't updated in current process
|
||||
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,
|
||||
});
|
||||
}
|
||||
@@ -349,7 +376,10 @@ function ClaudeSetupStep({
|
||||
const setupApi = api.setup;
|
||||
|
||||
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);
|
||||
|
||||
if (result.success) {
|
||||
@@ -434,7 +464,8 @@ function ClaudeSetupStep({
|
||||
const getAuthMethodLabel = () => {
|
||||
if (!isAuthenticated) return null;
|
||||
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";
|
||||
};
|
||||
|
||||
@@ -457,8 +488,15 @@ function ClaudeSetupStep({
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">Status</CardTitle>
|
||||
<Button variant="ghost" size="sm" onClick={checkStatus} disabled={isChecking}>
|
||||
<RefreshCw className={`w-4 h-4 ${isChecking ? "animate-spin" : ""}`} />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={checkStatus}
|
||||
disabled={isChecking}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-4 h-4 ${isChecking ? "animate-spin" : ""}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -477,7 +515,9 @@ function ClaudeSetupStep({
|
||||
{claudeCliStatus?.version && (
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -487,11 +527,16 @@ function ClaudeSetupStep({
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusBadge status="authenticated" label="Authenticated" />
|
||||
{getAuthMethodLabel() && (
|
||||
<span className="text-xs text-muted-foreground">({getAuthMethodLabel()})</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({getAuthMethodLabel()})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<StatusBadge status="not_authenticated" label="Not Authenticated" />
|
||||
<StatusBadge
|
||||
status="not_authenticated"
|
||||
label="Not Authenticated"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -505,16 +550,28 @@ function ClaudeSetupStep({
|
||||
<Download className="w-5 h-5" />
|
||||
Install Claude CLI
|
||||
</CardTitle>
|
||||
<CardDescription>Required for subscription-based authentication</CardDescription>
|
||||
<CardDescription>
|
||||
Required for subscription-based authentication
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<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">
|
||||
<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
|
||||
</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" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -526,13 +583,21 @@ function ClaudeSetupStep({
|
||||
<code className="flex-1 bg-muted px-3 py-2 rounded text-sm font-mono text-foreground">
|
||||
irm https://claude.ai/install.ps1 | iex
|
||||
</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" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{claudeInstallProgress.isInstalling && <TerminalOutput lines={claudeInstallProgress.output} />}
|
||||
{claudeInstallProgress.isInstalling && (
|
||||
<TerminalOutput lines={claudeInstallProgress.output} />
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
@@ -573,25 +638,37 @@ function ClaudeSetupStep({
|
||||
<div className="flex items-start gap-3">
|
||||
<Shield className="w-5 h-5 text-brand-500 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-foreground">Subscription Token</p>
|
||||
<p className="text-sm text-muted-foreground mb-3">Use your Claude subscription (no API charges)</p>
|
||||
<p className="font-medium text-foreground">
|
||||
Subscription Token
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
Use your Claude subscription (no API charges)
|
||||
</p>
|
||||
|
||||
{claudeCliStatus?.installed ? (
|
||||
<>
|
||||
<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">
|
||||
<code className="flex-1 bg-muted px-3 py-2 rounded text-sm font-mono text-foreground">
|
||||
claude setup-token
|
||||
</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" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
type="password"
|
||||
placeholder="Paste token from claude setup-token..."
|
||||
@@ -603,7 +680,11 @@ function ClaudeSetupStep({
|
||||
</div>
|
||||
|
||||
<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
|
||||
</Button>
|
||||
<Button
|
||||
@@ -612,7 +693,11 @@ function ClaudeSetupStep({
|
||||
className="flex-1 bg-brand-500 hover:bg-brand-600 text-white"
|
||||
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>
|
||||
</div>
|
||||
</>
|
||||
@@ -620,7 +705,8 @@ function ClaudeSetupStep({
|
||||
<div className="p-3 rounded bg-yellow-500/10 border border-yellow-500/20">
|
||||
<p className="text-sm text-yellow-600">
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
@@ -634,10 +720,17 @@ function ClaudeSetupStep({
|
||||
<Key className="w-5 h-5 text-green-500 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<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">
|
||||
<Label htmlFor="anthropic-key" className="text-foreground">Anthropic API Key</Label>
|
||||
<Label
|
||||
htmlFor="anthropic-key"
|
||||
className="text-foreground"
|
||||
>
|
||||
Anthropic API Key
|
||||
</Label>
|
||||
<Input
|
||||
id="anthropic-key"
|
||||
type="password"
|
||||
@@ -662,7 +755,11 @@ function ClaudeSetupStep({
|
||||
</div>
|
||||
|
||||
<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
|
||||
</Button>
|
||||
<Button
|
||||
@@ -671,7 +768,11 @@ function ClaudeSetupStep({
|
||||
className="flex-1 bg-green-500 hover:bg-green-600 text-white"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -688,9 +789,15 @@ function ClaudeSetupStep({
|
||||
<div className="flex items-start gap-3">
|
||||
<Shield className="w-6 h-6 text-brand-500" />
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Subscription</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>
|
||||
<p className="font-medium text-foreground">
|
||||
Subscription
|
||||
</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>
|
||||
</button>
|
||||
@@ -704,7 +811,9 @@ function ClaudeSetupStep({
|
||||
<Key className="w-6 h-6 text-green-500" />
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -724,9 +833,12 @@ function ClaudeSetupStep({
|
||||
<CheckCircle2 className="w-6 h-6 text-green-500" />
|
||||
</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">
|
||||
{getAuthMethodLabel() && `Using ${getAuthMethodLabel()}. `}You can proceed to the next step
|
||||
{getAuthMethodLabel() && `Using ${getAuthMethodLabel()}. `}You
|
||||
can proceed to the next step
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -736,15 +848,27 @@ function ClaudeSetupStep({
|
||||
|
||||
{/* Navigation */}
|
||||
<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" />
|
||||
Back
|
||||
</Button>
|
||||
<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
|
||||
</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
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
@@ -804,7 +928,10 @@ function CodexSetupStep({
|
||||
const setupApi = api.setup;
|
||||
|
||||
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) {
|
||||
const result = await setupApi.getCodexStatus();
|
||||
@@ -822,12 +949,15 @@ function CodexSetupStep({
|
||||
|
||||
if (result.auth) {
|
||||
const method = mapAuthMethod(result.auth.method);
|
||||
|
||||
|
||||
const authStatus: CodexAuthStatus = {
|
||||
authenticated: result.auth.authenticated,
|
||||
method,
|
||||
// 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);
|
||||
setCodexAuthStatus(authStatus);
|
||||
@@ -866,16 +996,18 @@ function CodexSetupStep({
|
||||
const setupApi = api.setup;
|
||||
|
||||
if (setupApi?.installCodex) {
|
||||
const unsubscribe = setupApi.onInstallProgress?.((progress: { cli?: string; data?: string; type?: string }) => {
|
||||
if (progress.cli === "codex") {
|
||||
setCodexInstallProgress({
|
||||
output: [
|
||||
...codexInstallProgress.output,
|
||||
progress.data || progress.type || "",
|
||||
],
|
||||
});
|
||||
const unsubscribe = setupApi.onInstallProgress?.(
|
||||
(progress: { cli?: string; data?: string; type?: string }) => {
|
||||
if (progress.cli === "codex") {
|
||||
setCodexInstallProgress({
|
||||
output: [
|
||||
...codexInstallProgress.output,
|
||||
progress.data || progress.type || "",
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
const result = await setupApi.installCodex();
|
||||
|
||||
@@ -912,7 +1044,10 @@ function CodexSetupStep({
|
||||
const api = getElectronAPI();
|
||||
const setupApi = api.setup;
|
||||
|
||||
console.log("[Codex Setup] storeApiKey available:", !!setupApi?.storeApiKey);
|
||||
console.log(
|
||||
"[Codex Setup] storeApiKey available:",
|
||||
!!setupApi?.storeApiKey
|
||||
);
|
||||
|
||||
if (setupApi?.storeApiKey) {
|
||||
console.log("[Codex Setup] Calling storeApiKey for openai...");
|
||||
@@ -920,7 +1055,9 @@ function CodexSetupStep({
|
||||
console.log("[Codex Setup] storeApiKey result:", result);
|
||||
|
||||
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 });
|
||||
setCodexAuthStatus({
|
||||
authenticated: true,
|
||||
@@ -933,7 +1070,9 @@ function CodexSetupStep({
|
||||
console.log("[Codex Setup] Failed to store API key:", result.error);
|
||||
}
|
||||
} 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 });
|
||||
setCodexAuthStatus({
|
||||
authenticated: true,
|
||||
@@ -957,13 +1096,14 @@ function CodexSetupStep({
|
||||
};
|
||||
|
||||
const isAuthenticated = codexAuthStatus?.authenticated || apiKeys.openai;
|
||||
|
||||
|
||||
const getAuthMethodLabel = () => {
|
||||
if (!isAuthenticated) return null;
|
||||
if (apiKeys.openai) return "API Key (Manual)";
|
||||
if (codexAuthStatus?.method === "api_key") return "API Key (Auth File)";
|
||||
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";
|
||||
};
|
||||
|
||||
@@ -1031,7 +1171,10 @@ function CodexSetupStep({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<StatusBadge status="not_authenticated" label="Not Authenticated" />
|
||||
<StatusBadge
|
||||
status="not_authenticated"
|
||||
label="Not Authenticated"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -1114,9 +1257,7 @@ function CodexSetupStep({
|
||||
<Key className="w-5 h-5" />
|
||||
Authentication
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Codex requires an OpenAI API key
|
||||
</CardDescription>
|
||||
<CardDescription>Codex requires an OpenAI API key</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{codexCliStatus?.installed && (
|
||||
@@ -1236,7 +1377,8 @@ function CodexSetupStep({
|
||||
Codex is ready to use!
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{getAuthMethodLabel() && `Authenticated via ${getAuthMethodLabel()}. `}
|
||||
{getAuthMethodLabel() &&
|
||||
`Authenticated via ${getAuthMethodLabel()}. `}
|
||||
You can proceed to complete setup
|
||||
</p>
|
||||
</div>
|
||||
@@ -1381,22 +1523,34 @@ function CompleteStep({ onFinish }: { onFinish: () => void }) {
|
||||
|
||||
// Main Setup View
|
||||
export function SetupView() {
|
||||
const { currentStep, setCurrentStep, completeSetup, setSkipClaudeSetup, setSkipCodexSetup } =
|
||||
useSetupStore();
|
||||
const {
|
||||
currentStep,
|
||||
setCurrentStep,
|
||||
completeSetup,
|
||||
setSkipClaudeSetup,
|
||||
setSkipCodexSetup,
|
||||
} = useSetupStore();
|
||||
const { setCurrentView } = useAppStore();
|
||||
|
||||
const steps = ["welcome", "claude", "codex", "complete"] as const;
|
||||
type StepName = typeof steps[number];
|
||||
type StepName = (typeof steps)[number];
|
||||
const getStepName = (): StepName => {
|
||||
if (currentStep === "claude_detect" || currentStep === "claude_auth") return "claude";
|
||||
if (currentStep === "codex_detect" || currentStep === "codex_auth") return "codex";
|
||||
if (currentStep === "claude_detect" || currentStep === "claude_auth")
|
||||
return "claude";
|
||||
if (currentStep === "codex_detect" || currentStep === "codex_auth")
|
||||
return "codex";
|
||||
if (currentStep === "welcome") return "welcome";
|
||||
return "complete";
|
||||
};
|
||||
const currentIndex = steps.indexOf(getStepName());
|
||||
|
||||
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) {
|
||||
case "welcome":
|
||||
console.log("[Setup Flow] Moving to claude_detect step");
|
||||
@@ -1445,10 +1599,7 @@ export function SetupView() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full flex flex-col content-bg"
|
||||
data-testid="setup-view"
|
||||
>
|
||||
<div className="h-full flex flex-col content-bg" data-testid="setup-view">
|
||||
{/* Header */}
|
||||
<div className="flex-shrink-0 border-b border-border bg-glass backdrop-blur-md titlebar-drag-region">
|
||||
<div className="px-8 py-4">
|
||||
@@ -1467,7 +1618,10 @@ export function SetupView() {
|
||||
<div className="p-8">
|
||||
<div className="w-full max-w-2xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<StepIndicator currentStep={currentIndex} totalSteps={steps.length} />
|
||||
<StepIndicator
|
||||
currentStep={currentIndex}
|
||||
totalSteps={steps.length}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="py-8">
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function initializeProject(
|
||||
const exists = await api.exists(fullPath);
|
||||
|
||||
if (!exists) {
|
||||
await api.writeFile(fullPath, defaultContent);
|
||||
await api.writeFile(fullPath, defaultContent as string);
|
||||
createdFiles.push(relativePath);
|
||||
} else {
|
||||
existingFiles.push(relativePath);
|
||||
|
||||
91
app/src/types/electron.d.ts
vendored
91
app/src/types/electron.d.ts
vendored
@@ -272,7 +272,10 @@ export interface SpecRegenerationAPI {
|
||||
}
|
||||
|
||||
export interface AutoModeAPI {
|
||||
start: (projectPath: string, maxConcurrency?: number) => Promise<{
|
||||
start: (
|
||||
projectPath: string,
|
||||
maxConcurrency?: number
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
@@ -299,25 +302,38 @@ export interface AutoModeAPI {
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
runFeature: (projectPath: string, featureId: string, useWorktrees?: boolean) => Promise<{
|
||||
runFeature: (
|
||||
projectPath: string,
|
||||
featureId: string,
|
||||
useWorktrees?: boolean
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
passes?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
verifyFeature: (projectPath: string, featureId: string) => Promise<{
|
||||
verifyFeature: (
|
||||
projectPath: string,
|
||||
featureId: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
passes?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
resumeFeature: (projectPath: string, featureId: string) => Promise<{
|
||||
resumeFeature: (
|
||||
projectPath: string,
|
||||
featureId: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
passes?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
contextExists: (projectPath: string, featureId: string) => Promise<{
|
||||
contextExists: (
|
||||
projectPath: string,
|
||||
featureId: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
exists?: boolean;
|
||||
error?: string;
|
||||
@@ -329,13 +345,21 @@ export interface AutoModeAPI {
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
followUpFeature: (projectPath: string, featureId: string, prompt: string, imagePaths?: string[]) => Promise<{
|
||||
followUpFeature: (
|
||||
projectPath: string,
|
||||
featureId: string,
|
||||
prompt: string,
|
||||
imagePaths?: string[]
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
passes?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
commitFeature: (projectPath: string, featureId: string) => Promise<{
|
||||
commitFeature: (
|
||||
projectPath: string,
|
||||
featureId: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
@@ -345,6 +369,9 @@ export interface AutoModeAPI {
|
||||
|
||||
export interface ElectronAPI {
|
||||
ping: () => Promise<string>;
|
||||
openExternalLink: (
|
||||
url: string
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
|
||||
// Dialog APIs
|
||||
openDirectory: () => Promise<{
|
||||
@@ -362,7 +389,10 @@ export interface ElectronAPI {
|
||||
content?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
writeFile: (filePath: string, content: string) => Promise<{
|
||||
writeFile: (
|
||||
filePath: string,
|
||||
content: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
@@ -525,25 +555,35 @@ export interface FileDiffResult {
|
||||
|
||||
export interface WorktreeAPI {
|
||||
// Revert feature changes by removing the worktree
|
||||
revertFeature: (projectPath: string, featureId: string) => Promise<{
|
||||
revertFeature: (
|
||||
projectPath: string,
|
||||
featureId: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
removedPath?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
// Merge feature worktree changes back to main branch
|
||||
mergeFeature: (projectPath: string, featureId: string, options?: {
|
||||
squash?: boolean;
|
||||
commitMessage?: string;
|
||||
squashMessage?: string;
|
||||
}) => Promise<{
|
||||
mergeFeature: (
|
||||
projectPath: string,
|
||||
featureId: string,
|
||||
options?: {
|
||||
squash?: boolean;
|
||||
commitMessage?: string;
|
||||
squashMessage?: string;
|
||||
}
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
mergedBranch?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
// Get worktree info for a feature
|
||||
getInfo: (projectPath: string, featureId: string) => Promise<{
|
||||
getInfo: (
|
||||
projectPath: string,
|
||||
featureId: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
worktreePath?: string;
|
||||
branchName?: string;
|
||||
@@ -552,7 +592,10 @@ export interface WorktreeAPI {
|
||||
}>;
|
||||
|
||||
// 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: (projectPath: string) => Promise<{
|
||||
@@ -562,10 +605,17 @@ export interface WorktreeAPI {
|
||||
}>;
|
||||
|
||||
// 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
|
||||
getFileDiff: (projectPath: string, featureId: string, filePath: string) => Promise<FileDiffResult>;
|
||||
getFileDiff: (
|
||||
projectPath: string,
|
||||
featureId: string,
|
||||
filePath: string
|
||||
) => Promise<FileDiffResult>;
|
||||
}
|
||||
|
||||
export interface GitAPI {
|
||||
@@ -573,7 +623,10 @@ export interface GitAPI {
|
||||
getDiffs: (projectPath: string) => Promise<FileDiffsResult>;
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user