mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
Merge branch 'main' into move-marketing
This commit is contained in:
1
apps/app/.gitignore
vendored
1
apps/app/.gitignore
vendored
@@ -48,3 +48,4 @@ next-env.d.ts
|
||||
|
||||
# Electron
|
||||
/dist/
|
||||
/server-bundle/
|
||||
|
||||
@@ -8,21 +8,114 @@
|
||||
const path = require("path");
|
||||
const { spawn } = require("child_process");
|
||||
const fs = require("fs");
|
||||
|
||||
// Load environment variables from .env file
|
||||
require("dotenv").config({ path: path.join(__dirname, "../.env") });
|
||||
|
||||
const http = require("http");
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron");
|
||||
|
||||
// Load environment variables from .env file (development only)
|
||||
if (!app.isPackaged) {
|
||||
try {
|
||||
require("dotenv").config({ path: path.join(__dirname, "../.env") });
|
||||
} catch (error) {
|
||||
console.warn("[Electron] dotenv not available:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
let mainWindow = null;
|
||||
let serverProcess = null;
|
||||
let staticServer = null;
|
||||
const SERVER_PORT = 3008;
|
||||
const STATIC_PORT = 3007;
|
||||
|
||||
// Get icon path - works in both dev and production
|
||||
// Get icon path - works in both dev and production, cross-platform
|
||||
function getIconPath() {
|
||||
return app.isPackaged
|
||||
? path.join(process.resourcesPath, "app", "public", "logo.png")
|
||||
: path.join(__dirname, "../public/logo.png");
|
||||
// Different icon formats for different platforms
|
||||
let iconFile;
|
||||
if (process.platform === "win32") {
|
||||
iconFile = "icon.ico";
|
||||
} else if (process.platform === "darwin") {
|
||||
iconFile = "logo_larger.png";
|
||||
} else {
|
||||
// Linux
|
||||
iconFile = "logo_larger.png";
|
||||
}
|
||||
|
||||
const iconPath = path.join(__dirname, "../public", iconFile);
|
||||
|
||||
// Verify the icon exists
|
||||
if (!fs.existsSync(iconPath)) {
|
||||
console.warn(`[Electron] Icon not found at: ${iconPath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return iconPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start static file server for production builds
|
||||
*/
|
||||
async function startStaticServer() {
|
||||
const staticPath = path.join(__dirname, "../out");
|
||||
|
||||
staticServer = http.createServer((request, response) => {
|
||||
// Parse the URL and remove query string
|
||||
let filePath = path.join(staticPath, request.url.split("?")[0]);
|
||||
|
||||
// Default to index.html for directory requests
|
||||
if (filePath.endsWith("/")) {
|
||||
filePath = path.join(filePath, "index.html");
|
||||
} else if (!path.extname(filePath)) {
|
||||
filePath += ".html";
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if (err || !stats.isFile()) {
|
||||
// Try index.html for SPA fallback
|
||||
filePath = path.join(staticPath, "index.html");
|
||||
}
|
||||
|
||||
// Read and serve the file
|
||||
fs.readFile(filePath, (error, content) => {
|
||||
if (error) {
|
||||
response.writeHead(500);
|
||||
response.end("Server Error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set content type based on file extension
|
||||
const ext = path.extname(filePath);
|
||||
const contentTypes = {
|
||||
".html": "text/html",
|
||||
".js": "application/javascript",
|
||||
".css": "text/css",
|
||||
".json": "application/json",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".svg": "image/svg+xml",
|
||||
".ico": "image/x-icon",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
".ttf": "font/ttf",
|
||||
".eot": "application/vnd.ms-fontobject",
|
||||
};
|
||||
|
||||
response.writeHead(200, { "Content-Type": contentTypes[ext] || "application/octet-stream" });
|
||||
response.end(content);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
staticServer.listen(STATIC_PORT, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`[Electron] Static server running at http://localhost:${STATIC_PORT}`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,14 +128,18 @@ async function startServer() {
|
||||
let command, args, serverPath;
|
||||
if (isDev) {
|
||||
// In development, use tsx to run TypeScript directly
|
||||
// Use the node executable that's running Electron
|
||||
command = process.execPath; // This is the path to node.exe
|
||||
// Use node from PATH (process.execPath in Electron points to Electron, not Node.js)
|
||||
// spawn() resolves "node" from PATH on all platforms (Windows, Linux, macOS)
|
||||
command = "node";
|
||||
serverPath = path.join(__dirname, "../../server/src/index.ts");
|
||||
|
||||
|
||||
// Find tsx CLI - check server node_modules first, then root
|
||||
const serverNodeModules = path.join(__dirname, "../../server/node_modules/tsx");
|
||||
const serverNodeModules = path.join(
|
||||
__dirname,
|
||||
"../../server/node_modules/tsx"
|
||||
);
|
||||
const rootNodeModules = path.join(__dirname, "../../../node_modules/tsx");
|
||||
|
||||
|
||||
let tsxCliPath;
|
||||
if (fs.existsSync(path.join(serverNodeModules, "dist/cli.mjs"))) {
|
||||
tsxCliPath = path.join(serverNodeModules, "dist/cli.mjs");
|
||||
@@ -51,30 +148,61 @@ async function startServer() {
|
||||
} else {
|
||||
// Last resort: try require.resolve
|
||||
try {
|
||||
tsxCliPath = require.resolve("tsx/cli.mjs", { paths: [path.join(__dirname, "../../server")] });
|
||||
tsxCliPath = require.resolve("tsx/cli.mjs", {
|
||||
paths: [path.join(__dirname, "../../server")],
|
||||
});
|
||||
} catch {
|
||||
throw new Error("Could not find tsx. Please run 'npm install' in the server directory.");
|
||||
throw new Error(
|
||||
"Could not find tsx. Please run 'npm install' in the server directory."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
args = [tsxCliPath, "watch", serverPath];
|
||||
} else {
|
||||
// In production, use compiled JavaScript
|
||||
command = "node";
|
||||
serverPath = path.join(process.resourcesPath, "server", "index.js");
|
||||
args = [serverPath];
|
||||
|
||||
// Verify server files exist
|
||||
if (!fs.existsSync(serverPath)) {
|
||||
throw new Error(`Server not found at: ${serverPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Set environment variables for server
|
||||
const serverNodeModules = app.isPackaged
|
||||
? path.join(process.resourcesPath, "server", "node_modules")
|
||||
: path.join(__dirname, "../../server/node_modules");
|
||||
|
||||
// Set default workspace directory to user's Documents/Automaker
|
||||
const defaultWorkspaceDir = path.join(app.getPath("documents"), "Automaker");
|
||||
|
||||
// Ensure workspace directory exists
|
||||
if (!fs.existsSync(defaultWorkspaceDir)) {
|
||||
try {
|
||||
fs.mkdirSync(defaultWorkspaceDir, { recursive: true });
|
||||
console.log("[Electron] Created workspace directory:", defaultWorkspaceDir);
|
||||
} catch (error) {
|
||||
console.error("[Electron] Failed to create workspace directory:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PORT: SERVER_PORT.toString(),
|
||||
DATA_DIR: app.getPath("userData"),
|
||||
NODE_PATH: serverNodeModules,
|
||||
WORKSPACE_DIR: process.env.WORKSPACE_DIR || defaultWorkspaceDir,
|
||||
};
|
||||
|
||||
console.log("[Electron] Starting backend server...");
|
||||
console.log("[Electron] Server path:", serverPath);
|
||||
console.log("[Electron] NODE_PATH:", serverNodeModules);
|
||||
|
||||
serverProcess = spawn(command, args, {
|
||||
cwd: path.dirname(serverPath),
|
||||
env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
@@ -92,6 +220,11 @@ async function startServer() {
|
||||
serverProcess = null;
|
||||
});
|
||||
|
||||
serverProcess.on("error", (err) => {
|
||||
console.error(`[Server] Failed to start server process:`, err);
|
||||
serverProcess = null;
|
||||
});
|
||||
|
||||
// Wait for server to be ready
|
||||
await waitForServer();
|
||||
}
|
||||
@@ -105,13 +238,16 @@ async function waitForServer(maxAttempts = 30) {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const req = http.get(`http://localhost:${SERVER_PORT}/api/health`, (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Status: ${res.statusCode}`));
|
||||
const req = http.get(
|
||||
`http://localhost:${SERVER_PORT}/api/health`,
|
||||
(res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Status: ${res.statusCode}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
req.on("error", reject);
|
||||
req.setTimeout(1000, () => {
|
||||
req.destroy();
|
||||
@@ -132,12 +268,12 @@ async function waitForServer(maxAttempts = 30) {
|
||||
* Create the main window
|
||||
*/
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
const iconPath = getIconPath();
|
||||
const windowOptions = {
|
||||
width: 1400,
|
||||
height: 900,
|
||||
minWidth: 1024,
|
||||
minHeight: 700,
|
||||
icon: getIconPath(),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
contextIsolation: true,
|
||||
@@ -145,17 +281,20 @@ function createWindow() {
|
||||
},
|
||||
titleBarStyle: "hiddenInset",
|
||||
backgroundColor: "#0a0a0a",
|
||||
});
|
||||
};
|
||||
|
||||
// Load Next.js dev server in development or production build
|
||||
// Only set icon if it exists
|
||||
if (iconPath) {
|
||||
windowOptions.icon = iconPath;
|
||||
}
|
||||
|
||||
mainWindow = new BrowserWindow(windowOptions);
|
||||
|
||||
// Load Next.js dev server in development or static server in production
|
||||
const isDev = !app.isPackaged;
|
||||
if (isDev) {
|
||||
mainWindow.loadURL("http://localhost:3007");
|
||||
if (process.env.OPEN_DEVTOOLS === "true") {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, "../.next/server/app/index.html"));
|
||||
mainWindow.loadURL(`http://localhost:${STATIC_PORT}`);
|
||||
if (isDev && process.env.OPEN_DEVTOOLS === "true") {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
@@ -173,10 +312,22 @@ function createWindow() {
|
||||
app.whenReady().then(async () => {
|
||||
// Set app icon (dock icon on macOS)
|
||||
if (process.platform === "darwin" && app.dock) {
|
||||
app.dock.setIcon(getIconPath());
|
||||
const iconPath = getIconPath();
|
||||
if (iconPath) {
|
||||
try {
|
||||
app.dock.setIcon(iconPath);
|
||||
} catch (error) {
|
||||
console.warn("[Electron] Failed to set dock icon:", error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Start static file server in production
|
||||
if (app.isPackaged) {
|
||||
await startStaticServer();
|
||||
}
|
||||
|
||||
// Start backend server
|
||||
await startServer();
|
||||
|
||||
@@ -207,6 +358,13 @@ app.on("before-quit", () => {
|
||||
serverProcess.kill();
|
||||
serverProcess = null;
|
||||
}
|
||||
|
||||
// Close static server
|
||||
if (staticServer) {
|
||||
console.log("[Electron] Stopping static server...");
|
||||
staticServer.close();
|
||||
staticServer = null;
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "export",
|
||||
env: {
|
||||
CLAUDE_CODE_OAUTH_TOKEN: process.env.CLAUDE_CODE_OAUTH_TOKEN || "",
|
||||
},
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
"dev:electron": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && electron .\"",
|
||||
"dev:electron:debug": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && OPEN_DEVTOOLS=true electron .\"",
|
||||
"build": "next build",
|
||||
"build:electron": "next build && electron-builder",
|
||||
"build:electron": "node scripts/prepare-server.js && next build && electron-builder",
|
||||
"build:electron:win": "node scripts/prepare-server.js && next build && electron-builder --win",
|
||||
"build:electron:mac": "node scripts/prepare-server.js && next build && electron-builder --mac",
|
||||
"build:electron:linux": "node scripts/prepare-server.js && next build && electron-builder --linux",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"test": "playwright test",
|
||||
@@ -51,7 +55,7 @@
|
||||
"dotenv": "^17.2.3",
|
||||
"geist": "^1.5.1",
|
||||
"lucide-react": "^0.556.0",
|
||||
"next": "16.0.7",
|
||||
"next": "^16.0.10",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -79,35 +83,46 @@
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"concurrently": "^9.2.1",
|
||||
"electron": "^39.2.6",
|
||||
"electron": "39.2.7",
|
||||
"electron-builder": "^26.0.12",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.7",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5",
|
||||
"typescript": "5.9.3",
|
||||
"wait-on": "^9.0.3"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.automaker.app",
|
||||
"productName": "Automaker",
|
||||
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
||||
"afterPack": "./scripts/rebuild-server-natives.js",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"files": [
|
||||
"electron/**/*",
|
||||
".next/**/*",
|
||||
"out/**/*",
|
||||
"public/**/*",
|
||||
"!node_modules/**/*"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": ".env",
|
||||
"from": "server-bundle/dist",
|
||||
"to": "server"
|
||||
},
|
||||
{
|
||||
"from": "server-bundle/node_modules",
|
||||
"to": "server/node_modules"
|
||||
},
|
||||
{
|
||||
"from": "server-bundle/package.json",
|
||||
"to": "server/package.json"
|
||||
},
|
||||
{
|
||||
"from": "../../.env",
|
||||
"to": ".env",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
"filter": ["**/*"]
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
@@ -139,7 +154,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "public/logo_larger.png"
|
||||
"icon": "public/icon.ico"
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
|
||||
BIN
apps/app/public/icon.ico
Normal file
BIN
apps/app/public/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 279 KiB |
81
apps/app/scripts/prepare-server.js
Normal file
81
apps/app/scripts/prepare-server.js
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* This script prepares the server for bundling with Electron.
|
||||
* It copies the server dist and installs production dependencies
|
||||
* in a way that works with npm workspaces.
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const APP_DIR = join(__dirname, '..');
|
||||
const SERVER_DIR = join(APP_DIR, '..', 'server');
|
||||
const BUNDLE_DIR = join(APP_DIR, 'server-bundle');
|
||||
|
||||
console.log('🔧 Preparing server for Electron bundling...\n');
|
||||
|
||||
// Step 1: Clean up previous bundle
|
||||
if (existsSync(BUNDLE_DIR)) {
|
||||
console.log('🗑️ Cleaning previous server-bundle...');
|
||||
rmSync(BUNDLE_DIR, { recursive: true });
|
||||
}
|
||||
mkdirSync(BUNDLE_DIR, { recursive: true });
|
||||
|
||||
// Step 2: Build the server TypeScript
|
||||
console.log('📦 Building server TypeScript...');
|
||||
execSync('npm run build', { cwd: SERVER_DIR, stdio: 'inherit' });
|
||||
|
||||
// Step 3: Copy server dist
|
||||
console.log('📋 Copying server dist...');
|
||||
cpSync(join(SERVER_DIR, 'dist'), join(BUNDLE_DIR, 'dist'), { recursive: true });
|
||||
|
||||
// Step 4: Create a minimal package.json for the server
|
||||
console.log('📝 Creating server package.json...');
|
||||
const serverPkg = JSON.parse(readFileSync(join(SERVER_DIR, 'package.json'), 'utf-8'));
|
||||
|
||||
const bundlePkg = {
|
||||
name: '@automaker/server-bundle',
|
||||
version: serverPkg.version,
|
||||
type: 'module',
|
||||
main: 'dist/index.js',
|
||||
dependencies: serverPkg.dependencies
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(BUNDLE_DIR, 'package.json'),
|
||||
JSON.stringify(bundlePkg, null, 2)
|
||||
);
|
||||
|
||||
// Step 5: Install production dependencies
|
||||
console.log('📥 Installing server production dependencies...');
|
||||
execSync('npm install --omit=dev', {
|
||||
cwd: BUNDLE_DIR,
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
// Prevent npm from using workspace resolution
|
||||
npm_config_workspace: ''
|
||||
}
|
||||
});
|
||||
|
||||
// Step 6: Rebuild native modules for current architecture
|
||||
// This is critical for modules like node-pty that have native bindings
|
||||
console.log('🔨 Rebuilding native modules for current architecture...');
|
||||
try {
|
||||
execSync('npm rebuild', {
|
||||
cwd: BUNDLE_DIR,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
console.log('✅ Native modules rebuilt successfully');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Warning: Failed to rebuild native modules. Terminal functionality may not work.');
|
||||
console.warn(' Error:', error.message);
|
||||
}
|
||||
|
||||
console.log('\n✅ Server prepared for bundling at:', BUNDLE_DIR);
|
||||
66
apps/app/scripts/rebuild-server-natives.js
Normal file
66
apps/app/scripts/rebuild-server-natives.js
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Electron-builder afterPack hook
|
||||
* Rebuilds native modules in the server bundle for the target architecture
|
||||
*/
|
||||
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const path = require('path');
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
exports.default = async function(context) {
|
||||
const { appOutDir, electronPlatformName, arch, packager } = context;
|
||||
const electronVersion = packager.config.electronVersion;
|
||||
|
||||
// Convert arch to string if it's a number (electron-builder sometimes passes indices)
|
||||
const archNames = ['ia32', 'x64', 'armv7l', 'arm64', 'universal'];
|
||||
const archStr = typeof arch === 'number' ? archNames[arch] : arch;
|
||||
|
||||
console.log(`\n🔨 Rebuilding server native modules for ${electronPlatformName}-${archStr}...`);
|
||||
|
||||
// Path to server node_modules in the packaged app
|
||||
let serverNodeModulesPath;
|
||||
if (electronPlatformName === 'darwin') {
|
||||
serverNodeModulesPath = path.join(
|
||||
appOutDir,
|
||||
`${packager.appInfo.productName}.app`,
|
||||
'Contents',
|
||||
'Resources',
|
||||
'server',
|
||||
'node_modules'
|
||||
);
|
||||
} else if (electronPlatformName === 'win32') {
|
||||
serverNodeModulesPath = path.join(
|
||||
appOutDir,
|
||||
'resources',
|
||||
'server',
|
||||
'node_modules'
|
||||
);
|
||||
} else {
|
||||
serverNodeModulesPath = path.join(
|
||||
appOutDir,
|
||||
'resources',
|
||||
'server',
|
||||
'node_modules'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Rebuild native modules for the target architecture
|
||||
const rebuildCmd = `npx --yes @electron/rebuild --version=${electronVersion} --arch=${archStr} --force --module-dir="${serverNodeModulesPath}/.."`;
|
||||
|
||||
console.log(` Command: ${rebuildCmd}`);
|
||||
|
||||
const { stdout, stderr } = await execAsync(rebuildCmd);
|
||||
if (stdout) console.log(stdout);
|
||||
if (stderr) console.error(stderr);
|
||||
|
||||
console.log(`✅ Server native modules rebuilt successfully for ${archStr}\n`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to rebuild server native modules:`, error.message);
|
||||
// Don't fail the build, just warn
|
||||
}
|
||||
};
|
||||
@@ -113,8 +113,8 @@ export function FileBrowserDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="bg-popover border-border max-w-2xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogContent className="bg-popover border-border max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader className="pb-2">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FolderOpen className="w-5 h-5 text-brand-500" />
|
||||
{title}
|
||||
@@ -124,7 +124,7 @@ export function FileBrowserDialog({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-3 min-h-[400px]">
|
||||
<div className="flex flex-col gap-3 min-h-[400px] flex-1 overflow-hidden py-2">
|
||||
{/* Drives selector (Windows only) */}
|
||||
{drives.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
|
||||
@@ -216,7 +216,7 @@ export function FileBrowserDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<DialogFooter className="border-t border-border pt-4 gap-2">
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -238,6 +238,25 @@ export function Sidebar() {
|
||||
// Ref for project search input
|
||||
const projectSearchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Auto-collapse sidebar on small screens
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia('(max-width: 1024px)'); // lg breakpoint
|
||||
|
||||
const handleResize = () => {
|
||||
if (mediaQuery.matches && sidebarOpen) {
|
||||
// Auto-collapse on small screens
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
// Check on mount
|
||||
handleResize();
|
||||
|
||||
// Listen for changes
|
||||
mediaQuery.addEventListener('change', handleResize);
|
||||
return () => mediaQuery.removeEventListener('change', handleResize);
|
||||
}, [sidebarOpen, toggleSidebar]);
|
||||
|
||||
// Filtered projects based on search query
|
||||
const filteredProjects = useMemo(() => {
|
||||
if (!projectSearchQuery.trim()) {
|
||||
|
||||
@@ -198,7 +198,10 @@ export function NewProjectModal({
|
||||
}
|
||||
};
|
||||
|
||||
const projectPath = workspaceDir && projectName ? `${workspaceDir}/${projectName}` : "";
|
||||
// Use platform-specific path separator
|
||||
const pathSep = typeof window !== 'undefined' && (window as any).electronAPI ?
|
||||
(navigator.platform.indexOf('Win') !== -1 ? '\\' : '/') : '/';
|
||||
const projectPath = workspaceDir && projectName ? `${workspaceDir}${pathSep}${projectName}` : "";
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
|
||||
@@ -121,7 +121,7 @@ type ModelOption = {
|
||||
label: string;
|
||||
description: string;
|
||||
badge?: string;
|
||||
provider: "claude" | "codex";
|
||||
provider: "claude";
|
||||
};
|
||||
|
||||
const CLAUDE_MODELS: ModelOption[] = [
|
||||
@@ -148,37 +148,6 @@ const CLAUDE_MODELS: ModelOption[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const CODEX_MODELS: ModelOption[] = [
|
||||
{
|
||||
id: "gpt-5.1-codex-max",
|
||||
label: "GPT-5.1 Codex Max",
|
||||
description: "Flagship Codex model tuned for deep coding tasks.",
|
||||
badge: "Flagship",
|
||||
provider: "codex",
|
||||
},
|
||||
{
|
||||
id: "gpt-5.1-codex",
|
||||
label: "GPT-5.1 Codex",
|
||||
description: "Strong coding performance with lower cost.",
|
||||
badge: "Standard",
|
||||
provider: "codex",
|
||||
},
|
||||
{
|
||||
id: "gpt-5.1-codex-mini",
|
||||
label: "GPT-5.1 Codex Mini",
|
||||
description: "Fastest Codex option for lightweight edits.",
|
||||
badge: "Fast",
|
||||
provider: "codex",
|
||||
},
|
||||
{
|
||||
id: "gpt-5.1",
|
||||
label: "GPT-5.1",
|
||||
description: "General-purpose reasoning with solid coding ability.",
|
||||
badge: "General",
|
||||
provider: "codex",
|
||||
},
|
||||
];
|
||||
|
||||
// Profile icon mapping
|
||||
const PROFILE_ICONS: Record<
|
||||
string,
|
||||
@@ -1693,12 +1662,8 @@ export function BoardView() {
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{options.map((option) => {
|
||||
const isSelected = selectedModel === option.id;
|
||||
const isCodex = option.provider === "codex";
|
||||
// Shorter display names for compact view
|
||||
const shortName = option.label
|
||||
.replace("Claude ", "")
|
||||
.replace("GPT-5.1 Codex ", "")
|
||||
.replace("GPT-5.1 ", "");
|
||||
const shortName = option.label.replace("Claude ", "");
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
@@ -1708,9 +1673,7 @@ export function BoardView() {
|
||||
className={cn(
|
||||
"flex-1 min-w-[80px] px-3 py-2 rounded-md border text-sm font-medium transition-colors",
|
||||
isSelected
|
||||
? isCodex
|
||||
? "bg-emerald-600 text-white border-emerald-500"
|
||||
: "bg-primary text-primary-foreground border-primary"
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "bg-background hover:bg-accent border-input"
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-${option.id}`}
|
||||
@@ -2270,7 +2233,6 @@ export function BoardView() {
|
||||
const IconComponent = profile.icon
|
||||
? PROFILE_ICONS[profile.icon]
|
||||
: Brain;
|
||||
const isCodex = profile.provider === "codex";
|
||||
const isSelected =
|
||||
newFeature.model === profile.model &&
|
||||
newFeature.thinkingLevel === profile.thinkingLevel;
|
||||
@@ -2284,13 +2246,6 @@ export function BoardView() {
|
||||
model: profile.model,
|
||||
thinkingLevel: profile.thinkingLevel,
|
||||
});
|
||||
if (profile.thinkingLevel === "ultrathink") {
|
||||
toast.warning("Ultrathink Selected", {
|
||||
description:
|
||||
"Ultrathink uses extensive reasoning (45-180s, ~$0.48/task).",
|
||||
duration: 4000,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2 p-2 rounded-lg border text-left transition-all",
|
||||
@@ -2300,19 +2255,9 @@ export function BoardView() {
|
||||
)}
|
||||
data-testid={`profile-quick-select-${profile.id}`}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"w-7 h-7 rounded flex items-center justify-center flex-shrink-0",
|
||||
isCodex ? "bg-emerald-500/10" : "bg-primary/10"
|
||||
)}
|
||||
>
|
||||
<div className="w-7 h-7 rounded flex items-center justify-center flex-shrink-0 bg-primary/10">
|
||||
{IconComponent && (
|
||||
<IconComponent
|
||||
className={cn(
|
||||
"w-4 h-4",
|
||||
isCodex ? "text-emerald-500" : "text-primary"
|
||||
)}
|
||||
/>
|
||||
<IconComponent className="w-4 h-4 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -2401,13 +2346,6 @@ export function BoardView() {
|
||||
...newFeature,
|
||||
thinkingLevel: level,
|
||||
});
|
||||
if (level === "ultrathink") {
|
||||
toast.warning("Ultrathink Selected", {
|
||||
description:
|
||||
"Ultrathink uses extensive reasoning (45-180s, ~$0.48/task). Best for complex architecture, migrations, or debugging.",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[60px]",
|
||||
@@ -2433,36 +2371,6 @@ export function BoardView() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Separator */}
|
||||
{(!showProfilesOnly || showAdvancedOptions) && (
|
||||
<div className="border-t border-border" />
|
||||
)}
|
||||
|
||||
{/* Codex Models Section - Hidden when showProfilesOnly is true and showAdvancedOptions is false */}
|
||||
{(!showProfilesOnly || showAdvancedOptions) && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Zap className="w-4 h-4 text-emerald-500" />
|
||||
OpenAI via Codex CLI
|
||||
</Label>
|
||||
<span className="text-[11px] px-2 py-0.5 rounded-full border border-emerald-500/50 text-emerald-600 dark:text-emerald-300">
|
||||
CLI
|
||||
</span>
|
||||
</div>
|
||||
{renderModelOptions(CODEX_MODELS, newFeature.model, (model) =>
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model,
|
||||
thinkingLevel: "none",
|
||||
})
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Codex models do not support thinking levels.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Testing Tab */}
|
||||
@@ -2688,7 +2596,6 @@ export function BoardView() {
|
||||
const IconComponent = profile.icon
|
||||
? PROFILE_ICONS[profile.icon]
|
||||
: Brain;
|
||||
const isCodex = profile.provider === "codex";
|
||||
const isSelected =
|
||||
editingFeature.model === profile.model &&
|
||||
editingFeature.thinkingLevel ===
|
||||
@@ -2703,13 +2610,6 @@ export function BoardView() {
|
||||
model: profile.model,
|
||||
thinkingLevel: profile.thinkingLevel,
|
||||
});
|
||||
if (profile.thinkingLevel === "ultrathink") {
|
||||
toast.warning("Ultrathink Selected", {
|
||||
description:
|
||||
"Ultrathink uses extensive reasoning (45-180s, ~$0.48/task).",
|
||||
duration: 4000,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2 p-2 rounded-lg border text-left transition-all",
|
||||
@@ -2719,21 +2619,9 @@ export function BoardView() {
|
||||
)}
|
||||
data-testid={`edit-profile-quick-select-${profile.id}`}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"w-7 h-7 rounded flex items-center justify-center flex-shrink-0",
|
||||
isCodex ? "bg-emerald-500/10" : "bg-primary/10"
|
||||
)}
|
||||
>
|
||||
<div className="w-7 h-7 rounded flex items-center justify-center flex-shrink-0 bg-primary/10">
|
||||
{IconComponent && (
|
||||
<IconComponent
|
||||
className={cn(
|
||||
"w-4 h-4",
|
||||
isCodex
|
||||
? "text-emerald-500"
|
||||
: "text-primary"
|
||||
)}
|
||||
/>
|
||||
<IconComponent className="w-4 h-4 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -2813,13 +2701,6 @@ export function BoardView() {
|
||||
...editingFeature,
|
||||
thinkingLevel: level,
|
||||
});
|
||||
if (level === "ultrathink") {
|
||||
toast.warning("Ultrathink Selected", {
|
||||
description:
|
||||
"Ultrathink uses extensive reasoning (45-180s, ~$0.48/task). Best for complex architecture, migrations, or debugging.",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors min-w-[60px]",
|
||||
@@ -2846,40 +2727,6 @@ export function BoardView() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Separator */}
|
||||
{(!showProfilesOnly || showEditAdvancedOptions) && (
|
||||
<div className="border-t border-border" />
|
||||
)}
|
||||
|
||||
{/* Codex Models Section - Hidden when showProfilesOnly is true and showEditAdvancedOptions is false */}
|
||||
{(!showProfilesOnly || showEditAdvancedOptions) && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Zap className="w-4 h-4 text-emerald-500" />
|
||||
OpenAI via Codex CLI
|
||||
</Label>
|
||||
<span className="text-[11px] px-2 py-0.5 rounded-full border border-emerald-500/50 text-emerald-600 dark:text-emerald-300">
|
||||
CLI
|
||||
</span>
|
||||
</div>
|
||||
{renderModelOptions(
|
||||
CODEX_MODELS,
|
||||
(editingFeature.model ?? "opus") as AgentModel,
|
||||
(model) =>
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model,
|
||||
thinkingLevel: "none",
|
||||
}),
|
||||
"edit-model-select"
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Codex models do not support thinking levels.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Testing Tab */}
|
||||
|
||||
@@ -305,7 +305,10 @@ export function InterviewView() {
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const fullProjectPath = `${projectPath}/${projectName}`;
|
||||
// Use platform-specific path separator
|
||||
const pathSep = typeof window !== 'undefined' && (window as any).electronAPI ?
|
||||
(navigator.platform.indexOf('Win') !== -1 ? '\\' : '/') : '/';
|
||||
const fullProjectPath = `${projectPath}${pathSep}${projectName}`;
|
||||
|
||||
// Create project directory
|
||||
await api.mkdir(fullProjectPath);
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
GripVertical,
|
||||
Lock,
|
||||
Check,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -88,13 +89,6 @@ const CLAUDE_MODELS: { id: AgentModel; label: string }[] = [
|
||||
{ id: "opus", label: "Claude Opus" },
|
||||
];
|
||||
|
||||
const CODEX_MODELS: { id: AgentModel; label: string }[] = [
|
||||
{ id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.1-codex", label: "GPT-5.1 Codex" },
|
||||
{ id: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex Mini" },
|
||||
{ id: "gpt-5.1", label: "GPT-5.1" },
|
||||
];
|
||||
|
||||
const THINKING_LEVELS: { id: ThinkingLevel; label: string }[] = [
|
||||
{ id: "none", label: "None" },
|
||||
{ id: "low", label: "Low" },
|
||||
@@ -105,9 +99,6 @@ const THINKING_LEVELS: { id: ThinkingLevel; label: string }[] = [
|
||||
|
||||
// Helper to determine provider from model
|
||||
function getProviderFromModel(model: AgentModel): ModelProvider {
|
||||
if (model.startsWith("gpt")) {
|
||||
return "codex";
|
||||
}
|
||||
return "claude";
|
||||
}
|
||||
|
||||
@@ -137,7 +128,6 @@ function SortableProfileCard({
|
||||
};
|
||||
|
||||
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain;
|
||||
const isCodex = profile.provider === "codex";
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -165,18 +155,10 @@ function SortableProfileCard({
|
||||
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center",
|
||||
isCodex ? "bg-emerald-500/10" : "bg-primary/10"
|
||||
)}
|
||||
className="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center bg-primary/10"
|
||||
>
|
||||
{IconComponent && (
|
||||
<IconComponent
|
||||
className={cn(
|
||||
"w-5 h-5",
|
||||
isCodex ? "text-emerald-500" : "text-primary"
|
||||
)}
|
||||
/>
|
||||
<IconComponent className="w-5 h-5 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -196,12 +178,7 @@ function SortableProfileCard({
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs px-2 py-0.5 rounded-full border",
|
||||
isCodex
|
||||
? "border-emerald-500/30 text-emerald-600 dark:text-emerald-400 bg-emerald-500/10"
|
||||
: "border-primary/30 text-primary bg-primary/10"
|
||||
)}
|
||||
className="text-xs px-2 py-0.5 rounded-full border border-primary/30 text-primary bg-primary/10"
|
||||
>
|
||||
{profile.model}
|
||||
</span>
|
||||
@@ -266,12 +243,9 @@ function ProfileForm({
|
||||
const supportsThinking = modelSupportsThinking(formData.model);
|
||||
|
||||
const handleModelChange = (model: AgentModel) => {
|
||||
const newProvider = getProviderFromModel(model);
|
||||
setFormData({
|
||||
...formData,
|
||||
model,
|
||||
// Reset thinking level when switching to Codex (doesn't support thinking)
|
||||
thinkingLevel: newProvider === "codex" ? "none" : formData.thinkingLevel,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -344,11 +318,11 @@ function ProfileForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Selection - Claude */}
|
||||
{/* Model Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Brain className="w-4 h-4 text-primary" />
|
||||
Claude Models
|
||||
Model
|
||||
</Label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{CLAUDE_MODELS.map(({ id, label }) => (
|
||||
@@ -370,33 +344,7 @@ function ProfileForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Selection - Codex */}
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Zap className="w-4 h-4 text-emerald-500" />
|
||||
Codex Models
|
||||
</Label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{CODEX_MODELS.map(({ id, label }) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => handleModelChange(id)}
|
||||
className={cn(
|
||||
"flex-1 min-w-[100px] px-3 py-2 rounded-md border text-sm font-medium transition-colors",
|
||||
formData.model === id
|
||||
? "bg-emerald-600 text-white border-emerald-500"
|
||||
: "bg-background hover:bg-accent border-input"
|
||||
)}
|
||||
data-testid={`model-select-${id}`}
|
||||
>
|
||||
{label.replace("GPT-5.1 ", "").replace("Codex ", "")}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thinking Level - Only for Claude models */}
|
||||
{/* Thinking Level */}
|
||||
{supportsThinking && (
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
@@ -461,6 +409,7 @@ export function ProfilesView() {
|
||||
updateAIProfile,
|
||||
removeAIProfile,
|
||||
reorderAIProfiles,
|
||||
resetAIProfiles,
|
||||
} = useAppStore();
|
||||
const shortcuts = useKeyboardShortcutsConfig();
|
||||
|
||||
@@ -529,6 +478,13 @@ export function ProfilesView() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleResetProfiles = () => {
|
||||
resetAIProfiles();
|
||||
toast.success("Profiles refreshed", {
|
||||
description: "Default profiles have been updated to the latest version",
|
||||
});
|
||||
};
|
||||
|
||||
// Build keyboard shortcuts for profiles view
|
||||
const profilesShortcuts: KeyboardShortcut[] = useMemo(() => {
|
||||
const shortcutsList: KeyboardShortcut[] = [];
|
||||
@@ -568,15 +524,26 @@ export function ProfilesView() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<HotkeyButton
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
hotkey={shortcuts.addProfile}
|
||||
hotkeyActive={false}
|
||||
data-testid="add-profile-button"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Profile
|
||||
</HotkeyButton>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleResetProfiles}
|
||||
data-testid="refresh-profiles-button"
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Refresh Defaults
|
||||
</Button>
|
||||
<HotkeyButton
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
hotkey={shortcuts.addProfile}
|
||||
hotkeyActive={false}
|
||||
data-testid="add-profile-button"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Profile
|
||||
</HotkeyButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Key,
|
||||
Palette,
|
||||
Terminal,
|
||||
Atom,
|
||||
FlaskConical,
|
||||
Trash2,
|
||||
Settings2,
|
||||
@@ -24,7 +23,6 @@ import { DeleteProjectDialog } from "./settings-view/components/delete-project-d
|
||||
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 { CodexCliStatus } from "./settings-view/cli-status/codex-cli-status";
|
||||
import { AppearanceSection } from "./settings-view/appearance/appearance-section";
|
||||
import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts/keyboard-shortcuts-section";
|
||||
import { FeatureDefaultsSection } from "./settings-view/feature-defaults/feature-defaults-section";
|
||||
@@ -39,7 +37,6 @@ import type { Project as ElectronProject } from "@/lib/electron";
|
||||
const NAV_ITEMS = [
|
||||
{ id: "api-keys", label: "API Keys", icon: Key },
|
||||
{ id: "claude", label: "Claude", icon: Terminal },
|
||||
{ id: "codex", label: "Codex", icon: Atom },
|
||||
{ id: "appearance", label: "Appearance", icon: Palette },
|
||||
{ id: "audio", label: "Audio", icon: Volume2 },
|
||||
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
|
||||
@@ -96,11 +93,8 @@ export function SettingsView() {
|
||||
// Use CLI status hook
|
||||
const {
|
||||
claudeCliStatus,
|
||||
codexCliStatus,
|
||||
isCheckingClaudeCli,
|
||||
isCheckingCodexCli,
|
||||
handleRefreshClaudeCli,
|
||||
handleRefreshCodexCli,
|
||||
} = useCliStatus();
|
||||
|
||||
// Use scroll tracking hook
|
||||
@@ -147,15 +141,6 @@ export function SettingsView() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Codex CLI Status Section */}
|
||||
{codexCliStatus && (
|
||||
<CodexCliStatus
|
||||
status={codexCliStatus}
|
||||
isChecking={isCheckingCodexCli}
|
||||
onRefresh={handleRefreshCodexCli}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Appearance Section */}
|
||||
<AppearanceSection
|
||||
effectiveTheme={effectiveTheme}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useApiKeyManagement } from "./hooks/use-api-key-management";
|
||||
|
||||
export function ApiKeysSection() {
|
||||
const { apiKeys } = useAppStore();
|
||||
const { claudeAuthStatus, codexAuthStatus } = useSetupStore();
|
||||
const { claudeAuthStatus } = useSetupStore();
|
||||
|
||||
const { providerConfigParams, apiKeyStatus, handleSave, saved } =
|
||||
useApiKeyManagement();
|
||||
@@ -41,7 +41,6 @@ export function ApiKeysSection() {
|
||||
{/* Authentication Status Display */}
|
||||
<AuthenticationStatusDisplay
|
||||
claudeAuthStatus={claudeAuthStatus}
|
||||
codexAuthStatus={codexAuthStatus}
|
||||
apiKeyStatus={apiKeyStatus}
|
||||
apiKeys={apiKeys}
|
||||
/>
|
||||
|
||||
@@ -4,29 +4,24 @@ import {
|
||||
AlertCircle,
|
||||
Info,
|
||||
Terminal,
|
||||
Atom,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import type { ClaudeAuthStatus, CodexAuthStatus } from "@/store/setup-store";
|
||||
import type { ClaudeAuthStatus } from "@/store/setup-store";
|
||||
|
||||
interface AuthenticationStatusDisplayProps {
|
||||
claudeAuthStatus: ClaudeAuthStatus | null;
|
||||
codexAuthStatus: CodexAuthStatus | null;
|
||||
apiKeyStatus: {
|
||||
hasAnthropicKey: boolean;
|
||||
hasOpenAIKey: boolean;
|
||||
hasGoogleKey: boolean;
|
||||
} | null;
|
||||
apiKeys: {
|
||||
anthropic: string;
|
||||
google: string;
|
||||
openai: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function AuthenticationStatusDisplay({
|
||||
claudeAuthStatus,
|
||||
codexAuthStatus,
|
||||
apiKeyStatus,
|
||||
apiKeys,
|
||||
}: AuthenticationStatusDisplayProps) {
|
||||
@@ -93,56 +88,6 @@ export function AuthenticationStatusDisplay({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Codex/OpenAI Authentication Status */}
|
||||
<div className="p-3 rounded-lg bg-card border border-border">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Atom className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
Codex (OpenAI)
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1.5 text-xs min-h-12">
|
||||
{codexAuthStatus?.authenticated ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-3 h-3 text-green-500 shrink-0" />
|
||||
<span className="text-green-400 font-medium">Authenticated</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Info className="w-3 h-3 shrink-0" />
|
||||
<span>
|
||||
{codexAuthStatus.method === "subscription"
|
||||
? "Using Codex subscription (Plus/Team)"
|
||||
: codexAuthStatus.method === "cli_verified" ||
|
||||
codexAuthStatus.method === "cli_tokens"
|
||||
? "Using CLI login (OpenAI account)"
|
||||
: codexAuthStatus.method === "api_key"
|
||||
? "Using stored API key"
|
||||
: codexAuthStatus.method === "env"
|
||||
? "Using OPENAI_API_KEY"
|
||||
: `Using ${codexAuthStatus.method || "unknown"} authentication`}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
) : apiKeyStatus?.hasOpenAIKey ? (
|
||||
<div className="flex items-center gap-2 text-blue-400">
|
||||
<Info className="w-3 h-3 shrink-0" />
|
||||
<span>Using environment variable (OPENAI_API_KEY)</span>
|
||||
</div>
|
||||
) : apiKeys.openai ? (
|
||||
<div className="flex items-center gap-2 text-blue-400">
|
||||
<Info className="w-3 h-3 shrink-0" />
|
||||
<span>Using manual API key from settings</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 text-yellow-500 py-0.5">
|
||||
<AlertCircle className="w-3 h-3 shrink-0" />
|
||||
<span className="text-xs">Not configured</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Google/Gemini Authentication Status */}
|
||||
<div className="p-3 rounded-lg bg-card border border-border">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
|
||||
@@ -10,7 +10,6 @@ interface TestResult {
|
||||
|
||||
interface ApiKeyStatus {
|
||||
hasAnthropicKey: boolean;
|
||||
hasOpenAIKey: boolean;
|
||||
hasGoogleKey: boolean;
|
||||
}
|
||||
|
||||
@@ -24,12 +23,10 @@ export function useApiKeyManagement() {
|
||||
// API key values
|
||||
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
|
||||
const [googleKey, setGoogleKey] = useState(apiKeys.google);
|
||||
const [openaiKey, setOpenaiKey] = useState(apiKeys.openai);
|
||||
|
||||
// Visibility toggles
|
||||
const [showAnthropicKey, setShowAnthropicKey] = useState(false);
|
||||
const [showGoogleKey, setShowGoogleKey] = useState(false);
|
||||
const [showOpenaiKey, setShowOpenaiKey] = useState(false);
|
||||
|
||||
// Test connection states
|
||||
const [testingConnection, setTestingConnection] = useState(false);
|
||||
@@ -38,10 +35,6 @@ export function useApiKeyManagement() {
|
||||
const [geminiTestResult, setGeminiTestResult] = useState<TestResult | null>(
|
||||
null
|
||||
);
|
||||
const [testingOpenaiConnection, setTestingOpenaiConnection] = useState(false);
|
||||
const [openaiTestResult, setOpenaiTestResult] = useState<TestResult | null>(
|
||||
null
|
||||
);
|
||||
|
||||
// API key status from environment
|
||||
const [apiKeyStatus, setApiKeyStatus] = useState<ApiKeyStatus | null>(null);
|
||||
@@ -53,7 +46,6 @@ export function useApiKeyManagement() {
|
||||
useEffect(() => {
|
||||
setAnthropicKey(apiKeys.anthropic);
|
||||
setGoogleKey(apiKeys.google);
|
||||
setOpenaiKey(apiKeys.openai);
|
||||
}, [apiKeys]);
|
||||
|
||||
// Check API key status from environment on mount
|
||||
@@ -66,7 +58,6 @@ export function useApiKeyManagement() {
|
||||
if (status.success) {
|
||||
setApiKeyStatus({
|
||||
hasAnthropicKey: status.hasAnthropicKey,
|
||||
hasOpenAIKey: status.hasOpenAIKey,
|
||||
hasGoogleKey: status.hasGoogleKey,
|
||||
});
|
||||
}
|
||||
@@ -152,68 +143,11 @@ export function useApiKeyManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
// Test OpenAI connection
|
||||
const handleTestOpenaiConnection = async () => {
|
||||
setTestingOpenaiConnection(true);
|
||||
setOpenaiTestResult(null);
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (api?.testOpenAIConnection) {
|
||||
const result = await api.testOpenAIConnection(openaiKey);
|
||||
if (result.success) {
|
||||
setOpenaiTestResult({
|
||||
success: true,
|
||||
message:
|
||||
result.message || "Connection successful! OpenAI API responded.",
|
||||
});
|
||||
} else {
|
||||
setOpenaiTestResult({
|
||||
success: false,
|
||||
message: result.error || "Failed to connect to OpenAI API.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Fallback to web API test
|
||||
const response = await fetch("/api/openai/test", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ apiKey: openaiKey }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
setOpenaiTestResult({
|
||||
success: true,
|
||||
message:
|
||||
data.message || "Connection successful! OpenAI API responded.",
|
||||
});
|
||||
} else {
|
||||
setOpenaiTestResult({
|
||||
success: false,
|
||||
message: data.error || "Failed to connect to OpenAI API.",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setOpenaiTestResult({
|
||||
success: false,
|
||||
message: "Network error. Please check your connection.",
|
||||
});
|
||||
} finally {
|
||||
setTestingOpenaiConnection(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Save API keys
|
||||
const handleSave = () => {
|
||||
setApiKeys({
|
||||
anthropic: anthropicKey,
|
||||
google: googleKey,
|
||||
openai: openaiKey,
|
||||
});
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
@@ -240,15 +174,6 @@ export function useApiKeyManagement() {
|
||||
onTest: handleTestGeminiConnection,
|
||||
result: geminiTestResult,
|
||||
},
|
||||
openai: {
|
||||
value: openaiKey,
|
||||
setValue: setOpenaiKey,
|
||||
show: showOpenaiKey,
|
||||
setShow: setShowOpenaiKey,
|
||||
testing: testingOpenaiConnection,
|
||||
onTest: handleTestOpenaiConnection,
|
||||
result: openaiTestResult,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Terminal,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import type { CliStatus } from "../shared/types";
|
||||
|
||||
interface CliStatusProps {
|
||||
status: CliStatus | null;
|
||||
isChecking: boolean;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
export function CodexCliStatus({
|
||||
status,
|
||||
isChecking,
|
||||
onRefresh,
|
||||
}: CliStatusProps) {
|
||||
if (!status) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="codex"
|
||||
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
||||
>
|
||||
<div className="p-6 border-b border-border">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-5 h-5 text-green-500" />
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
OpenAI Codex CLI
|
||||
</h2>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onRefresh}
|
||||
disabled={isChecking}
|
||||
data-testid="refresh-codex-cli"
|
||||
title="Refresh Codex CLI detection"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-4 h-4 ${isChecking ? "animate-spin" : ""}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Codex CLI enables GPT-5.1 Codex models for autonomous coding tasks.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
{status.success && status.status === "installed" ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-500 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-green-400">
|
||||
Codex CLI Installed
|
||||
</p>
|
||||
<div className="text-xs text-green-400/80 mt-1 space-y-1">
|
||||
{status.method && (
|
||||
<p>
|
||||
Method: <span className="font-mono">{status.method}</span>
|
||||
</p>
|
||||
)}
|
||||
{status.version && (
|
||||
<p>
|
||||
Version:{" "}
|
||||
<span className="font-mono">{status.version}</span>
|
||||
</p>
|
||||
)}
|
||||
{status.path && (
|
||||
<p className="truncate" title={status.path}>
|
||||
Path:{" "}
|
||||
<span className="font-mono text-[10px]">
|
||||
{status.path}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{status.recommendation && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{status.recommendation}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : status.status === "api_key_only" ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-blue-500/10 border border-blue-500/20">
|
||||
<AlertCircle className="w-5 h-5 text-blue-500 mt-0.5 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-blue-400">
|
||||
API Key Detected - CLI Not Installed
|
||||
</p>
|
||||
<p className="text-xs text-blue-400/80 mt-1">
|
||||
{status.recommendation ||
|
||||
"OPENAI_API_KEY found but Codex CLI not installed. Install the CLI for full agentic capabilities."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{status.installCommands && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-foreground-secondary">
|
||||
Installation Commands:
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{status.installCommands.npm && (
|
||||
<div className="p-2 rounded bg-background border border-border-glass">
|
||||
<p className="text-xs text-muted-foreground mb-1">npm:</p>
|
||||
<code className="text-xs text-foreground-secondary font-mono break-all">
|
||||
{status.installCommands.npm}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
|
||||
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-yellow-400">
|
||||
Codex CLI Not Detected
|
||||
</p>
|
||||
<p className="text-xs text-yellow-400/80 mt-1">
|
||||
{status.recommendation ||
|
||||
"Install OpenAI Codex CLI to use GPT-5.1 Codex models for autonomous coding."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{status.installCommands && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-foreground-secondary">
|
||||
Installation Commands:
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{status.installCommands.npm && (
|
||||
<div className="p-2 rounded bg-background border border-border-glass">
|
||||
<p className="text-xs text-muted-foreground mb-1">npm:</p>
|
||||
<code className="text-xs text-foreground-secondary font-mono break-all">
|
||||
{status.installCommands.npm}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
{status.installCommands.macos && (
|
||||
<div className="p-2 rounded bg-background border border-border-glass">
|
||||
<p className="text-xs text-muted-foreground mb-1">
|
||||
macOS (Homebrew):
|
||||
</p>
|
||||
<code className="text-xs text-foreground-secondary font-mono break-all">
|
||||
{status.installCommands.macos}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
Key,
|
||||
Terminal,
|
||||
Atom,
|
||||
Palette,
|
||||
LayoutGrid,
|
||||
Settings2,
|
||||
@@ -20,7 +19,6 @@ export interface NavigationItem {
|
||||
export const NAV_ITEMS: NavigationItem[] = [
|
||||
{ id: "api-keys", label: "API Keys", icon: Key },
|
||||
{ id: "claude", label: "Claude", icon: Terminal },
|
||||
{ id: "codex", label: "Codex", icon: Atom },
|
||||
{ id: "appearance", label: "Appearance", icon: Palette },
|
||||
{ id: "kanban", label: "Kanban Display", icon: LayoutGrid },
|
||||
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
|
||||
|
||||
@@ -59,7 +59,7 @@ export function FeatureDefaultsSection({
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When enabled, the Add Feature dialog will show only AI profiles
|
||||
and hide advanced model tweaking options (Claude SDK, thinking
|
||||
levels, and OpenAI Codex CLI). This creates a cleaner, less
|
||||
levels). This creates a cleaner, less
|
||||
overwhelming UI. You can always disable this to access advanced
|
||||
settings.
|
||||
</p>
|
||||
|
||||
@@ -18,25 +18,17 @@ interface CliStatusResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface CodexCliStatusResult extends CliStatusResult {
|
||||
hasApiKey?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing Claude and Codex CLI status
|
||||
* Custom hook for managing Claude CLI status
|
||||
* Handles checking CLI installation, authentication, and refresh functionality
|
||||
*/
|
||||
export function useCliStatus() {
|
||||
const { setClaudeAuthStatus, setCodexAuthStatus } = useSetupStore();
|
||||
const { setClaudeAuthStatus } = useSetupStore();
|
||||
|
||||
const [claudeCliStatus, setClaudeCliStatus] =
|
||||
useState<CliStatusResult | null>(null);
|
||||
|
||||
const [codexCliStatus, setCodexCliStatus] =
|
||||
useState<CodexCliStatusResult | null>(null);
|
||||
|
||||
const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false);
|
||||
const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false);
|
||||
|
||||
// Check CLI status on mount
|
||||
useEffect(() => {
|
||||
@@ -53,16 +45,6 @@ export function useCliStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
// Check Codex CLI
|
||||
if (api?.checkCodexCli) {
|
||||
try {
|
||||
const status = await api.checkCodexCli();
|
||||
setCodexCliStatus(status);
|
||||
} catch (error) {
|
||||
console.error("Failed to check Codex CLI status:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Check Claude auth status (re-fetch on mount to ensure persistence)
|
||||
if (api?.setup?.getClaudeStatus) {
|
||||
try {
|
||||
@@ -95,47 +77,10 @@ export function useCliStatus() {
|
||||
console.error("Failed to check Claude auth status:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Check Codex auth status (re-fetch on mount to ensure persistence)
|
||||
if (api?.setup?.getCodexStatus) {
|
||||
try {
|
||||
const result = await api.setup.getCodexStatus();
|
||||
if (result.success && result.auth) {
|
||||
// Cast to extended type that includes server-added fields
|
||||
const auth = result.auth as typeof result.auth & {
|
||||
hasSubscription?: boolean;
|
||||
cliLoggedIn?: boolean;
|
||||
hasEnvApiKey?: boolean;
|
||||
};
|
||||
// Map server method names to client method types
|
||||
// Server returns: subscription, cli_verified, cli_tokens, api_key, env, none
|
||||
const validMethods = ["subscription", "cli_verified", "cli_tokens", "api_key", "env", "none"] as const;
|
||||
type CodexMethod = typeof validMethods[number];
|
||||
const method: CodexMethod = validMethods.includes(auth.method as CodexMethod)
|
||||
? (auth.method as CodexMethod)
|
||||
: auth.authenticated ? "api_key" : "none"; // Default authenticated to api_key
|
||||
|
||||
const authStatus = {
|
||||
authenticated: auth.authenticated,
|
||||
method,
|
||||
// Only set apiKeyValid for actual API key methods, not CLI login or subscription
|
||||
apiKeyValid:
|
||||
method === "cli_verified" || method === "cli_tokens" || method === "subscription"
|
||||
? undefined
|
||||
: auth.hasAuthFile || auth.hasEnvKey || auth.hasEnvApiKey,
|
||||
hasSubscription: auth.hasSubscription,
|
||||
cliLoggedIn: auth.cliLoggedIn,
|
||||
};
|
||||
setCodexAuthStatus(authStatus);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check Codex auth status:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkCliStatus();
|
||||
}, [setClaudeAuthStatus, setCodexAuthStatus]);
|
||||
}, [setClaudeAuthStatus]);
|
||||
|
||||
// Refresh Claude CLI status
|
||||
const handleRefreshClaudeCli = useCallback(async () => {
|
||||
@@ -153,28 +98,9 @@ export function useCliStatus() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Refresh Codex CLI status
|
||||
const handleRefreshCodexCli = useCallback(async () => {
|
||||
setIsCheckingCodexCli(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (api?.checkCodexCli) {
|
||||
const status = await api.checkCodexCli();
|
||||
setCodexCliStatus(status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh Codex CLI status:", error);
|
||||
} finally {
|
||||
setIsCheckingCodexCli(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
claudeCliStatus,
|
||||
codexCliStatus,
|
||||
isCheckingClaudeCli,
|
||||
isCheckingCodexCli,
|
||||
handleRefreshClaudeCli,
|
||||
handleRefreshCodexCli,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
WelcomeStep,
|
||||
CompleteStep,
|
||||
ClaudeSetupStep,
|
||||
CodexSetupStep,
|
||||
} from "./setup-view/steps";
|
||||
|
||||
// Main Setup View
|
||||
@@ -17,17 +16,14 @@ export function SetupView() {
|
||||
setCurrentStep,
|
||||
completeSetup,
|
||||
setSkipClaudeSetup,
|
||||
setSkipCodexSetup,
|
||||
} = useSetupStore();
|
||||
const { setCurrentView } = useAppStore();
|
||||
|
||||
const steps = ["welcome", "claude", "codex", "complete"] as const;
|
||||
const steps = ["welcome", "claude", "complete"] as const;
|
||||
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 === "welcome") return "welcome";
|
||||
return "complete";
|
||||
};
|
||||
@@ -46,10 +42,6 @@ export function SetupView() {
|
||||
setCurrentStep("claude_detect");
|
||||
break;
|
||||
case "claude":
|
||||
console.log("[Setup Flow] Moving to codex_detect step");
|
||||
setCurrentStep("codex_detect");
|
||||
break;
|
||||
case "codex":
|
||||
console.log("[Setup Flow] Moving to complete step");
|
||||
setCurrentStep("complete");
|
||||
break;
|
||||
@@ -62,21 +54,12 @@ export function SetupView() {
|
||||
case "claude":
|
||||
setCurrentStep("welcome");
|
||||
break;
|
||||
case "codex":
|
||||
setCurrentStep("claude_detect");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkipClaude = () => {
|
||||
console.log("[Setup Flow] Skipping Claude setup");
|
||||
setSkipClaudeSetup(true);
|
||||
setCurrentStep("codex_detect");
|
||||
};
|
||||
|
||||
const handleSkipCodex = () => {
|
||||
console.log("[Setup Flow] Skipping Codex setup");
|
||||
setSkipCodexSetup(true);
|
||||
setCurrentStep("complete");
|
||||
};
|
||||
|
||||
@@ -127,15 +110,6 @@ export function SetupView() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{(currentStep === "codex_detect" ||
|
||||
currentStep === "codex_auth") && (
|
||||
<CodexSetupStep
|
||||
onNext={() => handleNext("codex")}
|
||||
onBack={() => handleBack("codex")}
|
||||
onSkip={handleSkipCodex}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === "complete" && (
|
||||
<CompleteStep onFinish={handleFinish} />
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useCallback } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface UseCliInstallationOptions {
|
||||
cliType: "claude" | "codex";
|
||||
cliType: "claude";
|
||||
installApi: () => Promise<any>;
|
||||
onProgressEvent?: (callback: (progress: any) => void) => (() => void) | undefined;
|
||||
onSuccess?: () => void;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
interface UseCliStatusOptions {
|
||||
cliType: "claude" | "codex";
|
||||
cliType: "claude";
|
||||
statusApi: () => Promise<any>;
|
||||
setCliStatus: (status: any) => void;
|
||||
setAuthStatus: (status: any) => void;
|
||||
@@ -33,65 +33,35 @@ export function useCliStatus({
|
||||
setCliStatus(cliStatus);
|
||||
|
||||
if (result.auth) {
|
||||
if (cliType === "claude") {
|
||||
// Validate method is one of the expected values, default to "none"
|
||||
const validMethods = [
|
||||
"oauth_token_env",
|
||||
"oauth_token",
|
||||
"api_key",
|
||||
"api_key_env",
|
||||
"credentials_file",
|
||||
"cli_authenticated",
|
||||
"none",
|
||||
] as const;
|
||||
type AuthMethod = (typeof validMethods)[number];
|
||||
const method: AuthMethod = validMethods.includes(
|
||||
result.auth.method as AuthMethod
|
||||
)
|
||||
? (result.auth.method as AuthMethod)
|
||||
: "none";
|
||||
const authStatus = {
|
||||
authenticated: result.auth.authenticated,
|
||||
method,
|
||||
hasCredentialsFile: false,
|
||||
oauthTokenValid:
|
||||
result.auth.hasStoredOAuthToken ||
|
||||
result.auth.hasEnvOAuthToken,
|
||||
apiKeyValid:
|
||||
result.auth.hasStoredApiKey || result.auth.hasEnvApiKey,
|
||||
hasEnvOAuthToken: result.auth.hasEnvOAuthToken,
|
||||
hasEnvApiKey: result.auth.hasEnvApiKey,
|
||||
};
|
||||
setAuthStatus(authStatus);
|
||||
} else {
|
||||
// Codex auth status mapping
|
||||
const mapAuthMethod = (method?: string): any => {
|
||||
switch (method) {
|
||||
case "cli_verified":
|
||||
return "cli_verified";
|
||||
case "cli_tokens":
|
||||
return "cli_tokens";
|
||||
case "auth_file":
|
||||
return "api_key";
|
||||
case "env_var":
|
||||
return "env";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
};
|
||||
|
||||
const method = mapAuthMethod(result.auth.method);
|
||||
const authStatus = {
|
||||
authenticated: result.auth.authenticated,
|
||||
method,
|
||||
apiKeyValid:
|
||||
method === "cli_verified" || method === "cli_tokens"
|
||||
? undefined
|
||||
: result.auth.authenticated,
|
||||
};
|
||||
console.log(`[${cliType} Setup] Auth Status:`, authStatus);
|
||||
setAuthStatus(authStatus);
|
||||
}
|
||||
// Validate method is one of the expected values, default to "none"
|
||||
const validMethods = [
|
||||
"oauth_token_env",
|
||||
"oauth_token",
|
||||
"api_key",
|
||||
"api_key_env",
|
||||
"credentials_file",
|
||||
"cli_authenticated",
|
||||
"none",
|
||||
] as const;
|
||||
type AuthMethod = (typeof validMethods)[number];
|
||||
const method: AuthMethod = validMethods.includes(
|
||||
result.auth.method as AuthMethod
|
||||
)
|
||||
? (result.auth.method as AuthMethod)
|
||||
: "none";
|
||||
const authStatus = {
|
||||
authenticated: result.auth.authenticated,
|
||||
method,
|
||||
hasCredentialsFile: false,
|
||||
oauthTokenValid:
|
||||
result.auth.hasStoredOAuthToken ||
|
||||
result.auth.hasEnvOAuthToken,
|
||||
apiKeyValid:
|
||||
result.auth.hasStoredApiKey || result.auth.hasEnvApiKey,
|
||||
hasEnvOAuthToken: result.auth.hasEnvOAuthToken,
|
||||
hasEnvApiKey: result.auth.hasEnvApiKey,
|
||||
};
|
||||
setAuthStatus(authStatus);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getElectronAPI } from "@/lib/electron";
|
||||
type AuthState = "idle" | "running" | "success" | "error" | "manual";
|
||||
|
||||
interface UseOAuthAuthenticationOptions {
|
||||
cliType: "claude" | "codex";
|
||||
cliType: "claude";
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -70,11 +70,8 @@ export function useOAuthAuthentication({
|
||||
}
|
||||
|
||||
try {
|
||||
// Call the appropriate auth API based on cliType
|
||||
const result =
|
||||
cliType === "claude"
|
||||
? await api.setup.authClaude()
|
||||
: await api.setup.authCodex?.();
|
||||
// Call the auth API
|
||||
const result = await api.setup.authClaude();
|
||||
|
||||
// Cleanup subscription
|
||||
if (unsubscribeRef.current) {
|
||||
|
||||
@@ -1,445 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { useSetupStore } from "@/store/setup-store";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import {
|
||||
CheckCircle2,
|
||||
Loader2,
|
||||
Terminal,
|
||||
Key,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
ExternalLink,
|
||||
Copy,
|
||||
AlertCircle,
|
||||
RefreshCw,
|
||||
Download,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { StatusBadge, TerminalOutput } from "../components";
|
||||
import {
|
||||
useCliStatus,
|
||||
useCliInstallation,
|
||||
useTokenSave,
|
||||
} from "../hooks";
|
||||
|
||||
interface CodexSetupStepProps {
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
onSkip: () => void;
|
||||
}
|
||||
|
||||
export function CodexSetupStep({
|
||||
onNext,
|
||||
onBack,
|
||||
onSkip,
|
||||
}: CodexSetupStepProps) {
|
||||
const {
|
||||
codexCliStatus,
|
||||
codexAuthStatus,
|
||||
setCodexCliStatus,
|
||||
setCodexAuthStatus,
|
||||
setCodexInstallProgress,
|
||||
} = useSetupStore();
|
||||
const { setApiKeys, apiKeys } = useAppStore();
|
||||
|
||||
const [showApiKeyInput, setShowApiKeyInput] = useState(false);
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
|
||||
// Memoize API functions to prevent infinite loops
|
||||
const statusApi = useCallback(
|
||||
() => getElectronAPI().setup?.getCodexStatus() || Promise.reject(),
|
||||
[]
|
||||
);
|
||||
|
||||
const installApi = useCallback(
|
||||
() => getElectronAPI().setup?.installCodex() || Promise.reject(),
|
||||
[]
|
||||
);
|
||||
|
||||
// Use custom hooks
|
||||
const { isChecking, checkStatus } = useCliStatus({
|
||||
cliType: "codex",
|
||||
statusApi,
|
||||
setCliStatus: setCodexCliStatus,
|
||||
setAuthStatus: setCodexAuthStatus,
|
||||
});
|
||||
|
||||
const onInstallSuccess = useCallback(() => {
|
||||
checkStatus();
|
||||
}, [checkStatus]);
|
||||
|
||||
const { isInstalling, installProgress, install } = useCliInstallation({
|
||||
cliType: "codex",
|
||||
installApi,
|
||||
onProgressEvent: getElectronAPI().setup?.onInstallProgress,
|
||||
onSuccess: onInstallSuccess,
|
||||
});
|
||||
|
||||
const { isSaving: isSavingKey, saveToken: saveApiKeyToken } = useTokenSave({
|
||||
provider: "openai",
|
||||
onSuccess: () => {
|
||||
setCodexAuthStatus({
|
||||
authenticated: true,
|
||||
method: "api_key",
|
||||
apiKeyValid: true,
|
||||
});
|
||||
setApiKeys({ ...apiKeys, openai: apiKey });
|
||||
setShowApiKeyInput(false);
|
||||
checkStatus();
|
||||
},
|
||||
});
|
||||
|
||||
// Sync install progress to store
|
||||
useEffect(() => {
|
||||
setCodexInstallProgress({
|
||||
isInstalling,
|
||||
output: installProgress.output,
|
||||
});
|
||||
}, [isInstalling, installProgress, setCodexInstallProgress]);
|
||||
|
||||
// Check status on mount
|
||||
useEffect(() => {
|
||||
checkStatus();
|
||||
}, [checkStatus]);
|
||||
|
||||
const copyCommand = (command: string) => {
|
||||
navigator.clipboard.writeText(command);
|
||||
toast.success("Command copied to clipboard");
|
||||
};
|
||||
|
||||
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)";
|
||||
return "Authenticated";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 rounded-xl bg-green-500/10 flex items-center justify-center mx-auto mb-4">
|
||||
<Terminal className="w-8 h-8 text-green-500" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-foreground mb-2">
|
||||
Codex CLI Setup
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
OpenAI's GPT-5.1 Codex for advanced code generation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Status Card */}
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">Installation Status</CardTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={checkStatus}
|
||||
disabled={isChecking}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-4 h-4 ${isChecking ? "animate-spin" : ""}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-foreground">CLI Installation</span>
|
||||
{isChecking ? (
|
||||
<StatusBadge status="checking" label="Checking..." />
|
||||
) : codexCliStatus?.installed ? (
|
||||
<StatusBadge status="installed" label="Installed" />
|
||||
) : (
|
||||
<StatusBadge status="not_installed" label="Not Installed" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{codexCliStatus?.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">
|
||||
{codexCliStatus.version}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-foreground">Authentication</span>
|
||||
{isAuthenticated ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusBadge status="authenticated" label="Authenticated" />
|
||||
{getAuthMethodLabel() && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({getAuthMethodLabel()})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<StatusBadge
|
||||
status="not_authenticated"
|
||||
label="Not Authenticated"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Installation Section */}
|
||||
{!codexCliStatus?.installed && (
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Download className="w-5 h-5" />
|
||||
Install Codex CLI
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Install via npm (Node.js required)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
npm (Global installation)
|
||||
</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">
|
||||
npm install -g @openai/codex
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => copyCommand("npm install -g @openai/codex")}
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isInstalling && (
|
||||
<TerminalOutput lines={installProgress.output} />
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={install}
|
||||
disabled={isInstalling}
|
||||
className="flex-1 bg-green-500 hover:bg-green-600 text-white"
|
||||
data-testid="install-codex-button"
|
||||
>
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Installing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Auto Install
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-yellow-500 mt-0.5" />
|
||||
<p className="text-xs text-yellow-600 dark:text-yellow-400">
|
||||
Requires Node.js to be installed. If the auto-install fails,
|
||||
try running the command manually in your terminal.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Authentication Section */}
|
||||
{!isAuthenticated && (
|
||||
<Card className="bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Key className="w-5 h-5" />
|
||||
Authentication
|
||||
</CardTitle>
|
||||
<CardDescription>Codex requires an OpenAI API key</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{codexCliStatus?.installed && (
|
||||
<div className="p-4 rounded-lg bg-muted/50 border border-border">
|
||||
<div className="flex items-start gap-3">
|
||||
<Terminal className="w-5 h-5 text-green-500 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-foreground">
|
||||
Authenticate via CLI
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Run this command in your terminal:
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="bg-muted px-3 py-1 rounded text-sm font-mono text-foreground">
|
||||
codex auth login
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => copyCommand("codex auth login")}
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t border-border" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-card px-2 text-muted-foreground">
|
||||
or enter API key
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showApiKeyInput ? (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="openai-key" className="text-foreground">
|
||||
OpenAI API Key
|
||||
</Label>
|
||||
<Input
|
||||
id="openai-key"
|
||||
type="password"
|
||||
placeholder="sk-..."
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
className="bg-input border-border text-foreground"
|
||||
data-testid="openai-api-key-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Get your API key from{" "}
|
||||
<a
|
||||
href="https://platform.openai.com/api-keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-green-500 hover:underline"
|
||||
>
|
||||
platform.openai.com
|
||||
<ExternalLink className="w-3 h-3 inline ml-1" />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowApiKeyInput(false)}
|
||||
className="border-border"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => saveApiKeyToken(apiKey)}
|
||||
disabled={isSavingKey || !apiKey.trim()}
|
||||
className="flex-1 bg-green-500 hover:bg-green-600 text-white"
|
||||
data-testid="save-openai-key-button"
|
||||
>
|
||||
{isSavingKey ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
"Save API Key"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowApiKeyInput(true)}
|
||||
className="w-full border-border"
|
||||
data-testid="use-openai-key-button"
|
||||
>
|
||||
<Key className="w-4 h-4 mr-2" />
|
||||
Enter OpenAI API Key
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Success State */}
|
||||
{isAuthenticated && (
|
||||
<Card className="bg-green-500/5 border-green-500/20">
|
||||
<CardContent className="py-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<CheckCircle2 className="w-6 h-6 text-green-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">
|
||||
Codex is ready to use!
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{getAuthMethodLabel() &&
|
||||
`Authenticated via ${getAuthMethodLabel()}. `}
|
||||
You can proceed to complete setup
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex justify-between pt-4">
|
||||
<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"
|
||||
>
|
||||
Skip for now
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onNext}
|
||||
className="bg-green-500 hover:bg-green-600 text-white"
|
||||
data-testid="codex-next-button"
|
||||
>
|
||||
Continue
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,16 +14,13 @@ interface CompleteStepProps {
|
||||
}
|
||||
|
||||
export function CompleteStep({ onFinish }: CompleteStepProps) {
|
||||
const { claudeCliStatus, claudeAuthStatus, codexCliStatus, codexAuthStatus } =
|
||||
const { claudeCliStatus, claudeAuthStatus } =
|
||||
useSetupStore();
|
||||
const { apiKeys } = useAppStore();
|
||||
|
||||
const claudeReady =
|
||||
(claudeCliStatus?.installed && claudeAuthStatus?.authenticated) ||
|
||||
apiKeys.anthropic;
|
||||
const codexReady =
|
||||
(codexCliStatus?.installed && codexAuthStatus?.authenticated) ||
|
||||
apiKeys.openai;
|
||||
|
||||
return (
|
||||
<div className="text-center space-y-6">
|
||||
@@ -41,7 +38,7 @@ export function CompleteStep({ onFinish }: CompleteStepProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-2xl mx-auto">
|
||||
<div className="max-w-md mx-auto">
|
||||
<Card
|
||||
className={`bg-card/50 border ${
|
||||
claudeReady ? "border-green-500/50" : "border-yellow-500/50"
|
||||
@@ -63,28 +60,6 @@ export function CompleteStep({ onFinish }: CompleteStepProps) {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className={`bg-card/50 border ${
|
||||
codexReady ? "border-green-500/50" : "border-yellow-500/50"
|
||||
}`}
|
||||
>
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{codexReady ? (
|
||||
<CheckCircle2 className="w-6 h-6 text-green-500" />
|
||||
) : (
|
||||
<AlertCircle className="w-6 h-6 text-yellow-500" />
|
||||
)}
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-foreground">Codex</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{codexReady ? "Ready to use" : "Configure later in settings"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50 border border-border max-w-md mx-auto">
|
||||
|
||||
@@ -2,4 +2,3 @@
|
||||
export { WelcomeStep } from "./welcome-step";
|
||||
export { CompleteStep } from "./complete-step";
|
||||
export { ClaudeSetupStep } from "./claude-setup-step";
|
||||
export { CodexSetupStep } from "./codex-setup-step";
|
||||
|
||||
@@ -24,7 +24,7 @@ export function WelcomeStep({ onNext }: WelcomeStepProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-2xl mx-auto">
|
||||
<div className="grid grid-cols-1 gap-4 max-w-md mx-auto place-items-center">
|
||||
<Card className="bg-card/50 border-border hover:border-brand-500/50 transition-colors">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
@@ -40,19 +40,6 @@ export function WelcomeStep({ onNext }: WelcomeStepProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card/50 border-border hover:border-brand-500/50 transition-colors">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Terminal className="w-5 h-5 text-green-500" />
|
||||
Codex CLI
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
OpenAI's GPT-5.1 Codex for advanced code generation tasks
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -200,7 +200,7 @@ export function WikiView() {
|
||||
{
|
||||
icon: Cpu,
|
||||
title: "Multi-Model Support",
|
||||
description: "Claude Haiku/Sonnet/Opus + OpenAI Codex models. Choose the right model for each task.",
|
||||
description: "Claude Haiku/Sonnet/Opus models. Choose the right model for each task.",
|
||||
},
|
||||
{
|
||||
icon: Brain,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Dispatch, SetStateAction } from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ApiKeys } from "@/store/app-store";
|
||||
|
||||
export type ProviderKey = "anthropic" | "google" | "openai";
|
||||
export type ProviderKey = "anthropic" | "google";
|
||||
|
||||
export interface ProviderConfig {
|
||||
key: ProviderKey;
|
||||
@@ -51,22 +51,12 @@ export interface ProviderConfigParams {
|
||||
onTest: () => Promise<void>;
|
||||
result: { success: boolean; message: string } | null;
|
||||
};
|
||||
openai: {
|
||||
value: string;
|
||||
setValue: Dispatch<SetStateAction<string>>;
|
||||
show: boolean;
|
||||
setShow: Dispatch<SetStateAction<boolean>>;
|
||||
testing: boolean;
|
||||
onTest: () => Promise<void>;
|
||||
result: { success: boolean; message: string } | null;
|
||||
};
|
||||
}
|
||||
|
||||
export const buildProviderConfigs = ({
|
||||
apiKeys,
|
||||
anthropic,
|
||||
google,
|
||||
openai,
|
||||
}: ProviderConfigParams): ProviderConfig[] => [
|
||||
{
|
||||
key: "anthropic",
|
||||
@@ -121,29 +111,4 @@ export const buildProviderConfigs = ({
|
||||
descriptionLinkHref: "https://makersuite.google.com/app/apikey",
|
||||
descriptionLinkText: "makersuite.google.com",
|
||||
},
|
||||
{
|
||||
key: "openai",
|
||||
label: "OpenAI API Key (Codex/GPT)",
|
||||
inputId: "openai-key",
|
||||
placeholder: "sk-...",
|
||||
value: openai.value,
|
||||
setValue: openai.setValue,
|
||||
showValue: openai.show,
|
||||
setShowValue: openai.setShow,
|
||||
hasStoredKey: apiKeys.openai,
|
||||
inputTestId: "openai-api-key-input",
|
||||
toggleTestId: "toggle-openai-visibility",
|
||||
testButton: {
|
||||
onClick: openai.onTest,
|
||||
disabled: !openai.value || openai.testing,
|
||||
loading: openai.testing,
|
||||
testId: "test-openai-connection",
|
||||
},
|
||||
result: openai.result,
|
||||
resultTestId: "openai-test-connection-result",
|
||||
resultMessageTestId: "openai-test-connection-message",
|
||||
descriptionPrefix: "Used for OpenAI Codex CLI and GPT models. Get your key at",
|
||||
descriptionLinkHref: "https://platform.openai.com/api-keys",
|
||||
descriptionLinkText: "platform.openai.com",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -287,22 +287,6 @@ export interface ElectronAPI {
|
||||
};
|
||||
error?: string;
|
||||
}>;
|
||||
checkCodexCli?: () => Promise<{
|
||||
success: boolean;
|
||||
status?: string;
|
||||
method?: string;
|
||||
version?: string;
|
||||
path?: string;
|
||||
hasApiKey?: boolean;
|
||||
recommendation?: string;
|
||||
installCommands?: {
|
||||
macos?: string;
|
||||
windows?: string;
|
||||
linux?: string;
|
||||
npm?: string;
|
||||
};
|
||||
error?: string;
|
||||
}>;
|
||||
model?: {
|
||||
getAvailable: () => Promise<{
|
||||
success: boolean;
|
||||
@@ -315,11 +299,6 @@ export interface ElectronAPI {
|
||||
error?: string;
|
||||
}>;
|
||||
};
|
||||
testOpenAIConnection?: (apiKey?: string) => Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
worktree?: WorktreeAPI;
|
||||
git?: GitAPI;
|
||||
suggestions?: SuggestionsAPI;
|
||||
@@ -347,32 +326,11 @@ export interface ElectronAPI {
|
||||
};
|
||||
error?: string;
|
||||
}>;
|
||||
getCodexStatus: () => Promise<{
|
||||
success: boolean;
|
||||
status?: string;
|
||||
method?: string;
|
||||
version?: string;
|
||||
path?: string;
|
||||
auth?: {
|
||||
authenticated: boolean;
|
||||
method: string; // Can be: "cli_verified", "cli_tokens", "auth_file", "env_var", "none"
|
||||
hasAuthFile: boolean;
|
||||
hasEnvKey: boolean;
|
||||
hasStoredApiKey?: boolean;
|
||||
hasEnvApiKey?: boolean;
|
||||
};
|
||||
error?: string;
|
||||
}>;
|
||||
installClaude: () => Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
installCodex: () => Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
authClaude: () => Promise<{
|
||||
success: boolean;
|
||||
token?: string;
|
||||
@@ -383,12 +341,6 @@ export interface ElectronAPI {
|
||||
message?: string;
|
||||
output?: string;
|
||||
}>;
|
||||
authCodex: (apiKey?: string) => Promise<{
|
||||
success: boolean;
|
||||
requiresManualAuth?: boolean;
|
||||
command?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
storeApiKey: (
|
||||
provider: string,
|
||||
apiKey: string
|
||||
@@ -396,12 +348,8 @@ export interface ElectronAPI {
|
||||
getApiKeys: () => Promise<{
|
||||
success: boolean;
|
||||
hasAnthropicKey: boolean;
|
||||
hasOpenAIKey: boolean;
|
||||
hasGoogleKey: boolean;
|
||||
}>;
|
||||
configureCodexMcp: (
|
||||
projectPath: string
|
||||
) => Promise<{ success: boolean; configPath?: string; error?: string }>;
|
||||
getPlatform: () => Promise<{
|
||||
success: boolean;
|
||||
platform: string;
|
||||
@@ -838,22 +786,11 @@ const getMockElectronAPI = (): ElectronAPI => {
|
||||
recommendation: "Claude CLI checks are unavailable in the web preview.",
|
||||
}),
|
||||
|
||||
checkCodexCli: async () => ({
|
||||
success: false,
|
||||
status: "not_installed",
|
||||
recommendation: "Codex CLI checks are unavailable in the web preview.",
|
||||
}),
|
||||
|
||||
model: {
|
||||
getAvailable: async () => ({ success: true, models: [] }),
|
||||
checkProviders: async () => ({ success: true, providers: {} }),
|
||||
},
|
||||
|
||||
testOpenAIConnection: async () => ({
|
||||
success: false,
|
||||
error: "OpenAI connection test is only available in the Electron app.",
|
||||
}),
|
||||
|
||||
// Mock Setup API
|
||||
setup: createMockSetupAPI(),
|
||||
|
||||
@@ -903,32 +840,11 @@ interface SetupAPI {
|
||||
};
|
||||
error?: string;
|
||||
}>;
|
||||
getCodexStatus: () => Promise<{
|
||||
success: boolean;
|
||||
status?: string;
|
||||
method?: string;
|
||||
version?: string;
|
||||
path?: string;
|
||||
auth?: {
|
||||
authenticated: boolean;
|
||||
method: string; // Can be: "cli_verified", "cli_tokens", "auth_file", "env_var", "none"
|
||||
hasAuthFile: boolean;
|
||||
hasEnvKey: boolean;
|
||||
hasStoredApiKey?: boolean;
|
||||
hasEnvApiKey?: boolean;
|
||||
};
|
||||
error?: string;
|
||||
}>;
|
||||
installClaude: () => Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
installCodex: () => Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
authClaude: () => Promise<{
|
||||
success: boolean;
|
||||
token?: string;
|
||||
@@ -939,12 +855,6 @@ interface SetupAPI {
|
||||
message?: string;
|
||||
output?: string;
|
||||
}>;
|
||||
authCodex: (apiKey?: string) => Promise<{
|
||||
success: boolean;
|
||||
requiresManualAuth?: boolean;
|
||||
command?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
storeApiKey: (
|
||||
provider: string,
|
||||
apiKey: string
|
||||
@@ -952,12 +862,8 @@ interface SetupAPI {
|
||||
getApiKeys: () => Promise<{
|
||||
success: boolean;
|
||||
hasAnthropicKey: boolean;
|
||||
hasOpenAIKey: boolean;
|
||||
hasGoogleKey: boolean;
|
||||
}>;
|
||||
configureCodexMcp: (
|
||||
projectPath: string
|
||||
) => Promise<{ success: boolean; configPath?: string; error?: string }>;
|
||||
getPlatform: () => Promise<{
|
||||
success: boolean;
|
||||
platform: string;
|
||||
@@ -991,20 +897,6 @@ function createMockSetupAPI(): SetupAPI {
|
||||
};
|
||||
},
|
||||
|
||||
getCodexStatus: async () => {
|
||||
console.log("[Mock] Getting Codex status");
|
||||
return {
|
||||
success: true,
|
||||
status: "not_installed",
|
||||
auth: {
|
||||
authenticated: false,
|
||||
method: "none",
|
||||
hasAuthFile: false,
|
||||
hasEnvKey: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
installClaude: async () => {
|
||||
console.log("[Mock] Installing Claude CLI");
|
||||
// Simulate installation delay
|
||||
@@ -1016,16 +908,6 @@ function createMockSetupAPI(): SetupAPI {
|
||||
};
|
||||
},
|
||||
|
||||
installCodex: async () => {
|
||||
console.log("[Mock] Installing Codex CLI");
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"CLI installation is only available in the Electron app. Please run the command manually.",
|
||||
};
|
||||
},
|
||||
|
||||
authClaude: async () => {
|
||||
console.log("[Mock] Auth Claude CLI");
|
||||
return {
|
||||
@@ -1035,18 +917,6 @@ function createMockSetupAPI(): SetupAPI {
|
||||
};
|
||||
},
|
||||
|
||||
authCodex: async (apiKey?: string) => {
|
||||
console.log("[Mock] Auth Codex CLI", { hasApiKey: !!apiKey });
|
||||
if (apiKey) {
|
||||
return { success: true };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
requiresManualAuth: true,
|
||||
command: "codex auth login",
|
||||
};
|
||||
},
|
||||
|
||||
storeApiKey: async (provider: string, apiKey: string) => {
|
||||
console.log("[Mock] Storing API key for:", provider);
|
||||
// In mock mode, we just pretend to store it (it's already in the app store)
|
||||
@@ -1058,19 +928,10 @@ function createMockSetupAPI(): SetupAPI {
|
||||
return {
|
||||
success: true,
|
||||
hasAnthropicKey: false,
|
||||
hasOpenAIKey: false,
|
||||
hasGoogleKey: false,
|
||||
};
|
||||
},
|
||||
|
||||
configureCodexMcp: async (projectPath: string) => {
|
||||
console.log("[Mock] Configuring Codex MCP for:", projectPath);
|
||||
return {
|
||||
success: true,
|
||||
configPath: `${projectPath}/.codex/config.toml`,
|
||||
};
|
||||
},
|
||||
|
||||
getPlatform: async () => {
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -371,25 +371,6 @@ export class HttpApiClient implements ElectronAPI {
|
||||
return this.get("/api/setup/claude-status");
|
||||
}
|
||||
|
||||
async checkCodexCli(): Promise<{
|
||||
success: boolean;
|
||||
status?: string;
|
||||
method?: string;
|
||||
version?: string;
|
||||
path?: string;
|
||||
hasApiKey?: boolean;
|
||||
recommendation?: string;
|
||||
installCommands?: {
|
||||
macos?: string;
|
||||
windows?: string;
|
||||
linux?: string;
|
||||
npm?: string;
|
||||
};
|
||||
error?: string;
|
||||
}> {
|
||||
return this.get("/api/setup/codex-status");
|
||||
}
|
||||
|
||||
// Model API
|
||||
model = {
|
||||
getAvailable: async (): Promise<{
|
||||
@@ -408,14 +389,6 @@ export class HttpApiClient implements ElectronAPI {
|
||||
},
|
||||
};
|
||||
|
||||
async testOpenAIConnection(apiKey?: string): Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}> {
|
||||
return this.post("/api/setup/test-openai", { apiKey });
|
||||
}
|
||||
|
||||
// Setup API
|
||||
setup = {
|
||||
getClaudeStatus: (): Promise<{
|
||||
@@ -440,35 +413,12 @@ export class HttpApiClient implements ElectronAPI {
|
||||
error?: string;
|
||||
}> => this.get("/api/setup/claude-status"),
|
||||
|
||||
getCodexStatus: (): Promise<{
|
||||
success: boolean;
|
||||
status?: string;
|
||||
method?: string;
|
||||
version?: string;
|
||||
path?: string;
|
||||
auth?: {
|
||||
authenticated: boolean;
|
||||
method: string;
|
||||
hasAuthFile: boolean;
|
||||
hasEnvKey: boolean;
|
||||
hasStoredApiKey?: boolean;
|
||||
hasEnvApiKey?: boolean;
|
||||
};
|
||||
error?: string;
|
||||
}> => this.get("/api/setup/codex-status"),
|
||||
|
||||
installClaude: (): Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}> => this.post("/api/setup/install-claude"),
|
||||
|
||||
installCodex: (): Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}> => this.post("/api/setup/install-codex"),
|
||||
|
||||
authClaude: (): Promise<{
|
||||
success: boolean;
|
||||
token?: string;
|
||||
@@ -480,15 +430,6 @@ export class HttpApiClient implements ElectronAPI {
|
||||
output?: string;
|
||||
}> => this.post("/api/setup/auth-claude"),
|
||||
|
||||
authCodex: (
|
||||
apiKey?: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
requiresManualAuth?: boolean;
|
||||
command?: string;
|
||||
error?: string;
|
||||
}> => this.post("/api/setup/auth-codex", { apiKey }),
|
||||
|
||||
storeApiKey: (
|
||||
provider: string,
|
||||
apiKey: string
|
||||
@@ -500,18 +441,9 @@ export class HttpApiClient implements ElectronAPI {
|
||||
getApiKeys: (): Promise<{
|
||||
success: boolean;
|
||||
hasAnthropicKey: boolean;
|
||||
hasOpenAIKey: boolean;
|
||||
hasGoogleKey: boolean;
|
||||
}> => this.get("/api/setup/api-keys"),
|
||||
|
||||
configureCodexMcp: (
|
||||
projectPath: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
configPath?: string;
|
||||
error?: string;
|
||||
}> => this.post("/api/setup/configure-codex-mcp", { projectPath }),
|
||||
|
||||
getPlatform: (): Promise<{
|
||||
success: boolean;
|
||||
platform: string;
|
||||
|
||||
@@ -6,26 +6,12 @@ export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a model is a Codex/OpenAI model (doesn't support thinking)
|
||||
*/
|
||||
export function isCodexModel(model?: AgentModel | string): boolean {
|
||||
if (!model) return false;
|
||||
const codexModels: string[] = [
|
||||
"gpt-5.1-codex-max",
|
||||
"gpt-5.1-codex",
|
||||
"gpt-5.1-codex-mini",
|
||||
"gpt-5.1",
|
||||
];
|
||||
return codexModels.includes(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current model supports extended thinking controls
|
||||
*/
|
||||
export function modelSupportsThinking(model?: AgentModel | string): boolean {
|
||||
if (!model) return true;
|
||||
return !isCodexModel(model);
|
||||
// All Claude models support thinking
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,10 +22,6 @@ export function getModelDisplayName(model: AgentModel | string): string {
|
||||
haiku: "Claude Haiku",
|
||||
sonnet: "Claude Sonnet",
|
||||
opus: "Claude Opus",
|
||||
"gpt-5.1-codex-max": "GPT-5.1 Codex Max",
|
||||
"gpt-5.1-codex": "GPT-5.1 Codex",
|
||||
"gpt-5.1-codex-mini": "GPT-5.1 Codex Mini",
|
||||
"gpt-5.1": "GPT-5.1",
|
||||
};
|
||||
return displayNames[model] || model;
|
||||
}
|
||||
|
||||
@@ -246,19 +246,11 @@ export interface FeatureImagePath {
|
||||
}
|
||||
|
||||
// Available models for feature execution
|
||||
// Claude models
|
||||
export type ClaudeModel = "opus" | "sonnet" | "haiku";
|
||||
// OpenAI/Codex models
|
||||
export type OpenAIModel =
|
||||
| "gpt-5.1-codex-max"
|
||||
| "gpt-5.1-codex"
|
||||
| "gpt-5.1-codex-mini"
|
||||
| "gpt-5.1";
|
||||
// Combined model type
|
||||
export type AgentModel = ClaudeModel | OpenAIModel;
|
||||
export type AgentModel = ClaudeModel;
|
||||
|
||||
// Model provider type
|
||||
export type ModelProvider = "claude" | "codex";
|
||||
export type ModelProvider = "claude";
|
||||
|
||||
// Thinking level (budget_tokens) options
|
||||
export type ThinkingLevel = "none" | "low" | "medium" | "high" | "ultrathink";
|
||||
@@ -570,6 +562,7 @@ export interface AppActions {
|
||||
updateAIProfile: (id: string, updates: Partial<AIProfile>) => void;
|
||||
removeAIProfile: (id: string) => void;
|
||||
reorderAIProfiles: (oldIndex: number, newIndex: number) => void;
|
||||
resetAIProfiles: () => void;
|
||||
|
||||
// Project Analysis actions
|
||||
setProjectAnalysis: (analysis: ProjectAnalysis | null) => void;
|
||||
@@ -657,26 +650,6 @@ const DEFAULT_AI_PROFILES: AIProfile[] = [
|
||||
isBuiltIn: true,
|
||||
icon: "Zap",
|
||||
},
|
||||
{
|
||||
id: "profile-codex-power",
|
||||
name: "Codex Power",
|
||||
description: "GPT-5.1 Codex Max for deep coding tasks via OpenAI CLI.",
|
||||
model: "gpt-5.1-codex-max",
|
||||
thinkingLevel: "none",
|
||||
provider: "codex",
|
||||
isBuiltIn: true,
|
||||
icon: "Cpu",
|
||||
},
|
||||
{
|
||||
id: "profile-codex-fast",
|
||||
name: "Codex Fast",
|
||||
description: "GPT-5.1 Codex Mini for lightweight and quick edits.",
|
||||
model: "gpt-5.1-codex-mini",
|
||||
thinkingLevel: "none",
|
||||
provider: "codex",
|
||||
isBuiltIn: true,
|
||||
icon: "Rocket",
|
||||
},
|
||||
];
|
||||
|
||||
const initialState: AppState = {
|
||||
@@ -1356,6 +1329,13 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
set({ aiProfiles: profiles });
|
||||
},
|
||||
|
||||
resetAIProfiles: () => {
|
||||
// Merge: keep user-created profiles, but refresh all built-in profiles to latest defaults
|
||||
const defaultProfileIds = new Set(DEFAULT_AI_PROFILES.map(p => p.id));
|
||||
const userProfiles = get().aiProfiles.filter(p => !p.isBuiltIn && !defaultProfileIds.has(p.id));
|
||||
set({ aiProfiles: [...DEFAULT_AI_PROFILES, ...userProfiles] });
|
||||
},
|
||||
|
||||
// Project Analysis actions
|
||||
setProjectAnalysis: (analysis) => set({ projectAnalysis: analysis }),
|
||||
setIsAnalyzing: (analyzing) => set({ isAnalyzing: analyzing }),
|
||||
|
||||
@@ -32,26 +32,6 @@ export interface ClaudeAuthStatus {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Codex Auth Method - all possible authentication sources
|
||||
export type CodexAuthMethod =
|
||||
| "subscription" // Codex/OpenAI Plus or Team subscription
|
||||
| "cli_verified" // CLI logged in with OpenAI account
|
||||
| "cli_tokens" // CLI with stored access tokens
|
||||
| "api_key" // Manually stored API key
|
||||
| "env" // OPENAI_API_KEY environment variable
|
||||
| "none";
|
||||
|
||||
// Codex Auth Status
|
||||
export interface CodexAuthStatus {
|
||||
authenticated: boolean;
|
||||
method: CodexAuthMethod;
|
||||
apiKeyValid?: boolean;
|
||||
mcpConfigured?: boolean;
|
||||
hasSubscription?: boolean;
|
||||
cliLoggedIn?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Installation Progress
|
||||
export interface InstallProgress {
|
||||
isInstalling: boolean;
|
||||
@@ -65,8 +45,6 @@ export type SetupStep =
|
||||
| "welcome"
|
||||
| "claude_detect"
|
||||
| "claude_auth"
|
||||
| "codex_detect"
|
||||
| "codex_auth"
|
||||
| "complete";
|
||||
|
||||
export interface SetupState {
|
||||
@@ -80,14 +58,8 @@ export interface SetupState {
|
||||
claudeAuthStatus: ClaudeAuthStatus | null;
|
||||
claudeInstallProgress: InstallProgress;
|
||||
|
||||
// Codex CLI state
|
||||
codexCliStatus: CliStatus | null;
|
||||
codexAuthStatus: CodexAuthStatus | null;
|
||||
codexInstallProgress: InstallProgress;
|
||||
|
||||
// Setup preferences
|
||||
skipClaudeSetup: boolean;
|
||||
skipCodexSetup: boolean;
|
||||
}
|
||||
|
||||
export interface SetupActions {
|
||||
@@ -103,15 +75,8 @@ export interface SetupActions {
|
||||
setClaudeInstallProgress: (progress: Partial<InstallProgress>) => void;
|
||||
resetClaudeInstallProgress: () => void;
|
||||
|
||||
// Codex CLI
|
||||
setCodexCliStatus: (status: CliStatus | null) => void;
|
||||
setCodexAuthStatus: (status: CodexAuthStatus | null) => void;
|
||||
setCodexInstallProgress: (progress: Partial<InstallProgress>) => void;
|
||||
resetCodexInstallProgress: () => void;
|
||||
|
||||
// Preferences
|
||||
setSkipClaudeSetup: (skip: boolean) => void;
|
||||
setSkipCodexSetup: (skip: boolean) => void;
|
||||
}
|
||||
|
||||
const initialInstallProgress: InstallProgress = {
|
||||
@@ -130,12 +95,7 @@ const initialState: SetupState = {
|
||||
claudeAuthStatus: null,
|
||||
claudeInstallProgress: { ...initialInstallProgress },
|
||||
|
||||
codexCliStatus: null,
|
||||
codexAuthStatus: null,
|
||||
codexInstallProgress: { ...initialInstallProgress },
|
||||
|
||||
skipClaudeSetup: false,
|
||||
skipCodexSetup: false,
|
||||
};
|
||||
|
||||
export const useSetupStore = create<SetupState & SetupActions>()(
|
||||
@@ -171,26 +131,8 @@ export const useSetupStore = create<SetupState & SetupActions>()(
|
||||
claudeInstallProgress: { ...initialInstallProgress },
|
||||
}),
|
||||
|
||||
// Codex CLI
|
||||
setCodexCliStatus: (status) => set({ codexCliStatus: status }),
|
||||
|
||||
setCodexAuthStatus: (status) => set({ codexAuthStatus: status }),
|
||||
|
||||
setCodexInstallProgress: (progress) => set({
|
||||
codexInstallProgress: {
|
||||
...get().codexInstallProgress,
|
||||
...progress,
|
||||
},
|
||||
}),
|
||||
|
||||
resetCodexInstallProgress: () => set({
|
||||
codexInstallProgress: { ...initialInstallProgress },
|
||||
}),
|
||||
|
||||
// Preferences
|
||||
setSkipClaudeSetup: (skip) => set({ skipClaudeSetup: skip }),
|
||||
|
||||
setSkipCodexSetup: (skip) => set({ skipCodexSetup: skip }),
|
||||
}),
|
||||
{
|
||||
name: "automaker-setup",
|
||||
@@ -198,7 +140,6 @@ export const useSetupStore = create<SetupState & SetupActions>()(
|
||||
isFirstRun: state.isFirstRun,
|
||||
setupComplete: state.setupComplete,
|
||||
skipClaudeSetup: state.skipClaudeSetup,
|
||||
skipCodexSetup: state.skipCodexSetup,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
20
apps/app/src/types/electron.d.ts
vendored
20
apps/app/src/types/electron.d.ts
vendored
@@ -471,24 +471,6 @@ export interface ElectronAPI {
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
// Codex CLI Detection API
|
||||
checkCodexCli: () => Promise<{
|
||||
success: boolean;
|
||||
status?: string;
|
||||
method?: string;
|
||||
version?: string;
|
||||
path?: string;
|
||||
hasApiKey?: boolean;
|
||||
recommendation?: string;
|
||||
installCommands?: {
|
||||
macos?: string;
|
||||
windows?: string;
|
||||
linux?: string;
|
||||
npm?: string;
|
||||
};
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
// Model Management APIs
|
||||
model: {
|
||||
// Get all available models from all providers
|
||||
@@ -641,7 +623,7 @@ export interface ModelDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
modelString: string;
|
||||
provider: "claude" | "codex";
|
||||
provider: "claude";
|
||||
description?: string;
|
||||
tier?: "basic" | "standard" | "premium";
|
||||
default?: boolean;
|
||||
|
||||
@@ -2437,16 +2437,7 @@ export async function setupFirstRun(page: Page): Promise<void> {
|
||||
progress: 0,
|
||||
output: [],
|
||||
},
|
||||
codexCliStatus: null,
|
||||
codexAuthStatus: null,
|
||||
codexInstallProgress: {
|
||||
isInstalling: false,
|
||||
currentStep: "",
|
||||
progress: 0,
|
||||
output: [],
|
||||
},
|
||||
skipClaudeSetup: false,
|
||||
skipCodexSetup: false,
|
||||
},
|
||||
version: 0,
|
||||
};
|
||||
@@ -2460,7 +2451,7 @@ export async function setupFirstRun(page: Page): Promise<void> {
|
||||
currentProject: null,
|
||||
theme: "dark",
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "", openai: "" },
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
@@ -2488,7 +2479,6 @@ export async function setupComplete(page: Page): Promise<void> {
|
||||
setupComplete: true,
|
||||
currentStep: "complete",
|
||||
skipClaudeSetup: false,
|
||||
skipCodexSetup: false,
|
||||
},
|
||||
version: 0,
|
||||
};
|
||||
@@ -2530,14 +2520,6 @@ export async function clickClaudeContinue(page: Page): Promise<void> {
|
||||
await button.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click continue on Codex setup step
|
||||
*/
|
||||
export async function clickCodexContinue(page: Page): Promise<void> {
|
||||
const button = await getByTestId(page, "codex-next-button");
|
||||
await button.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click finish on setup complete step
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user