mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
refactor: streamline Electron API integration and enhance UI components
- Removed unused Electron API methods and simplified the main process. - Introduced a new workspace picker modal for improved project selection. - Enhanced error handling for authentication issues across various components. - Updated UI styles for dark mode support and added new CSS variables. - Refactored session management to utilize a centralized API access method. - Added server routes for workspace management, including directory listing and configuration checks.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
// Type definitions for Electron IPC API
|
||||
import type { SessionListItem, Message } from "@/types/electron";
|
||||
|
||||
export interface FileEntry {
|
||||
name: string;
|
||||
@@ -413,6 +414,59 @@ export interface ElectronAPI {
|
||||
onInstallProgress?: (callback: (progress: any) => void) => () => void;
|
||||
onAuthProgress?: (callback: (progress: any) => void) => () => void;
|
||||
};
|
||||
agent?: {
|
||||
start: (sessionId: string, workingDirectory?: string) => Promise<{
|
||||
success: boolean;
|
||||
messages?: Message[];
|
||||
error?: string;
|
||||
}>;
|
||||
send: (
|
||||
sessionId: string,
|
||||
message: string,
|
||||
workingDirectory?: string,
|
||||
imagePaths?: string[]
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
getHistory: (sessionId: string) => Promise<{
|
||||
success: boolean;
|
||||
messages?: Message[];
|
||||
isRunning?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
stop: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
clear: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
onStream: (callback: (data: unknown) => void) => () => void;
|
||||
};
|
||||
sessions?: {
|
||||
list: (includeArchived?: boolean) => Promise<{
|
||||
success: boolean;
|
||||
sessions?: SessionListItem[];
|
||||
error?: string;
|
||||
}>;
|
||||
create: (
|
||||
name: string,
|
||||
projectPath: string,
|
||||
workingDirectory?: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
session?: {
|
||||
id: string;
|
||||
name: string;
|
||||
projectPath: string;
|
||||
workingDirectory?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
error?: string;
|
||||
}>;
|
||||
update: (
|
||||
sessionId: string,
|
||||
name?: string,
|
||||
tags?: string[]
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
archive: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
unarchive: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
delete: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
// Note: Window interface is declared in @/types/electron.d.ts
|
||||
@@ -438,7 +492,7 @@ const STORAGE_KEYS = {
|
||||
// Mock file system using localStorage
|
||||
const mockFileSystem: Record<string, string> = {};
|
||||
|
||||
// Check if we're in Electron
|
||||
// Check if we're in Electron (for UI indicators only)
|
||||
export const isElectron = (): boolean => {
|
||||
return typeof window !== "undefined" && window.isElectron === true;
|
||||
};
|
||||
@@ -478,72 +532,50 @@ export const resetServerCheck = (): void => {
|
||||
// Cached HTTP client instance
|
||||
let httpClientInstance: ElectronAPI | null = null;
|
||||
|
||||
// Check if we're in simplified Electron mode (HTTP backend instead of IPC)
|
||||
const isSimplifiedElectronMode = (): boolean => {
|
||||
if (typeof window === "undefined") return false;
|
||||
const api = window.electronAPI as any;
|
||||
// Simplified mode has isElectron flag and getServerUrl but NOT readFile
|
||||
return api?.isElectron === true &&
|
||||
typeof api?.getServerUrl === "function" &&
|
||||
typeof api?.readFile !== "function";
|
||||
};
|
||||
|
||||
// Get the Electron API or HTTP client for web mode
|
||||
// In simplified Electron mode and web mode, uses HTTP client
|
||||
/**
|
||||
* Get the HTTP API client
|
||||
*
|
||||
* All API calls go through HTTP to the backend server.
|
||||
* This is the only transport mode supported.
|
||||
*/
|
||||
export const getElectronAPI = (): ElectronAPI => {
|
||||
// Check if we're in simplified Electron mode (uses HTTP backend)
|
||||
if (isSimplifiedElectronMode()) {
|
||||
if (typeof window !== "undefined" && !httpClientInstance) {
|
||||
const { getHttpApiClient } = require("./http-api-client");
|
||||
httpClientInstance = getHttpApiClient();
|
||||
}
|
||||
return httpClientInstance!;
|
||||
if (typeof window === "undefined") {
|
||||
throw new Error("Cannot get API during SSR");
|
||||
}
|
||||
|
||||
// Full Electron API with IPC
|
||||
if (isElectron() && window.electronAPI) {
|
||||
return window.electronAPI;
|
||||
if (!httpClientInstance) {
|
||||
const { getHttpApiClient } = require("./http-api-client");
|
||||
httpClientInstance = getHttpApiClient();
|
||||
}
|
||||
|
||||
// Web mode: use HTTP API client
|
||||
if (typeof window !== "undefined") {
|
||||
if (!httpClientInstance) {
|
||||
const { getHttpApiClient } = require("./http-api-client");
|
||||
httpClientInstance = getHttpApiClient();
|
||||
}
|
||||
return httpClientInstance!;
|
||||
}
|
||||
|
||||
// SSR fallback - this shouldn't be called during actual operation
|
||||
throw new Error("Cannot get Electron API during SSR");
|
||||
return httpClientInstance!;
|
||||
};
|
||||
|
||||
// Async version that checks server availability first
|
||||
// Async version (same as sync since HTTP client is synchronously instantiated)
|
||||
export const getElectronAPIAsync = async (): Promise<ElectronAPI> => {
|
||||
// Simplified Electron mode or web mode: use HTTP client
|
||||
if (isSimplifiedElectronMode() || !isElectron()) {
|
||||
if (typeof window !== "undefined") {
|
||||
const { getHttpApiClient } = await import("./http-api-client");
|
||||
return getHttpApiClient();
|
||||
}
|
||||
}
|
||||
|
||||
// Full Electron API with IPC
|
||||
if (isElectron() && window.electronAPI) {
|
||||
return window.electronAPI;
|
||||
}
|
||||
|
||||
throw new Error("Cannot get Electron API during SSR");
|
||||
return getElectronAPI();
|
||||
};
|
||||
|
||||
// Check if backend is connected (for showing connection status in UI)
|
||||
export const isBackendConnected = async (): Promise<boolean> => {
|
||||
// Full Electron mode: backend is built-in
|
||||
if (isElectron() && !isSimplifiedElectronMode()) return true;
|
||||
// Simplified Electron or web mode: check server availability
|
||||
return await checkServerAvailable();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current API mode being used
|
||||
* Always returns "http" since that's the only mode now
|
||||
*/
|
||||
export const getCurrentApiMode = (): "http" => {
|
||||
return "http";
|
||||
};
|
||||
|
||||
// Debug helpers
|
||||
if (typeof window !== "undefined") {
|
||||
(window as any).__checkApiMode = () => {
|
||||
console.log("Current API mode:", getCurrentApiMode());
|
||||
console.log("isElectron():", isElectron());
|
||||
};
|
||||
}
|
||||
|
||||
// Mock API for development/fallback when no backend is available
|
||||
const getMockElectronAPI = (): ElectronAPI => {
|
||||
return {
|
||||
@@ -1962,7 +1994,9 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
|
||||
}
|
||||
|
||||
mockSpecRegenerationRunning = true;
|
||||
console.log(`[Mock] Generating features from existing spec for: ${projectPath}`);
|
||||
console.log(
|
||||
`[Mock] Generating features from existing spec for: ${projectPath}`
|
||||
);
|
||||
|
||||
// Simulate async feature generation
|
||||
simulateFeatureGeneration(projectPath);
|
||||
@@ -2149,7 +2183,8 @@ async function simulateFeatureGeneration(projectPath: string) {
|
||||
mockSpecRegenerationPhase = "initialization";
|
||||
emitSpecRegenerationEvent({
|
||||
type: "spec_regeneration_progress",
|
||||
content: "[Phase: initialization] Starting feature generation from existing app_spec.txt...\n",
|
||||
content:
|
||||
"[Phase: initialization] Starting feature generation from existing app_spec.txt...\n",
|
||||
});
|
||||
|
||||
await new Promise((resolve) => {
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
FeatureSuggestion,
|
||||
SuggestionType,
|
||||
} from "./electron";
|
||||
import type { Message, SessionListItem } from "@/types/electron";
|
||||
import type { Feature } from "@/store/app-store";
|
||||
import type {
|
||||
WorktreeAPI,
|
||||
@@ -31,46 +32,9 @@ import type {
|
||||
ProviderStatus,
|
||||
} from "@/types/electron";
|
||||
|
||||
// Check if we're in simplified Electron mode (Electron with HTTP backend)
|
||||
const isSimplifiedElectronMode = (): boolean => {
|
||||
if (typeof window === "undefined") return false;
|
||||
const api = (window as any).electronAPI;
|
||||
// Simplified mode has isElectron flag but limited methods
|
||||
return api?.isElectron === true && typeof api?.getServerUrl === "function";
|
||||
};
|
||||
|
||||
// Check if native Electron dialogs are available
|
||||
const hasNativeDialogs = (): boolean => {
|
||||
if (typeof window === "undefined") return false;
|
||||
const api = (window as any).electronAPI;
|
||||
return typeof api?.openDirectory === "function";
|
||||
};
|
||||
|
||||
// Server URL - configurable via environment variable or Electron
|
||||
const getServerUrl = async (): Promise<string> => {
|
||||
if (typeof window !== "undefined") {
|
||||
// In simplified Electron mode, get URL from main process
|
||||
const api = (window as any).electronAPI;
|
||||
if (api?.getServerUrl) {
|
||||
try {
|
||||
return await api.getServerUrl();
|
||||
} catch {
|
||||
// Fall through to defaults
|
||||
}
|
||||
}
|
||||
|
||||
// Check for environment variable
|
||||
const envUrl = process.env.NEXT_PUBLIC_SERVER_URL;
|
||||
if (envUrl) return envUrl;
|
||||
|
||||
// Default to localhost for development
|
||||
return "http://localhost:3008";
|
||||
}
|
||||
return "http://localhost:3008";
|
||||
};
|
||||
|
||||
// Synchronous version for constructor (uses default, then updates)
|
||||
const getServerUrlSync = (): string => {
|
||||
// Server URL - configurable via environment variable
|
||||
const getServerUrl = (): string => {
|
||||
if (typeof window !== "undefined") {
|
||||
const envUrl = process.env.NEXT_PUBLIC_SERVER_URL;
|
||||
if (envUrl) return envUrl;
|
||||
@@ -78,6 +42,7 @@ const getServerUrlSync = (): string => {
|
||||
return "http://localhost:3008";
|
||||
};
|
||||
|
||||
|
||||
// Get API key from environment variable
|
||||
const getApiKey = (): string | null => {
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -105,25 +70,10 @@ export class HttpApiClient implements ElectronAPI {
|
||||
private isConnecting = false;
|
||||
|
||||
constructor() {
|
||||
this.serverUrl = getServerUrlSync();
|
||||
// Update server URL asynchronously if in Electron
|
||||
this.initServerUrl();
|
||||
this.serverUrl = getServerUrl();
|
||||
this.connectWebSocket();
|
||||
}
|
||||
|
||||
private async initServerUrl(): Promise<void> {
|
||||
const url = await getServerUrl();
|
||||
if (url !== this.serverUrl) {
|
||||
this.serverUrl = url;
|
||||
// Reconnect WebSocket with new URL
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
this.connectWebSocket();
|
||||
}
|
||||
}
|
||||
|
||||
private connectWebSocket(): void {
|
||||
if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
|
||||
return;
|
||||
@@ -222,6 +172,23 @@ export class HttpApiClient implements ElectronAPI {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
private async put<T>(endpoint: string, body?: unknown): Promise<T> {
|
||||
const response = await fetch(`${this.serverUrl}${endpoint}`, {
|
||||
method: "PUT",
|
||||
headers: this.getHeaders(),
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
private async httpDelete<T>(endpoint: string): Promise<T> {
|
||||
const response = await fetch(`${this.serverUrl}${endpoint}`, {
|
||||
method: "DELETE",
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Basic operations
|
||||
async ping(): Promise<string> {
|
||||
const result = await this.get<{ status: string }>("/api/health");
|
||||
@@ -229,27 +196,13 @@ export class HttpApiClient implements ElectronAPI {
|
||||
}
|
||||
|
||||
async openExternalLink(url: string): Promise<{ success: boolean; error?: string }> {
|
||||
// Use native Electron shell if available (better UX)
|
||||
if (hasNativeDialogs()) {
|
||||
const api = (window as any).electronAPI;
|
||||
if (api.openExternalLink) {
|
||||
return api.openExternalLink(url);
|
||||
}
|
||||
}
|
||||
// Web mode: open in new tab
|
||||
// Open in new tab
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// File picker - uses native Electron dialogs when available, otherwise prompt
|
||||
// File picker - uses prompt for path input
|
||||
async openDirectory(): Promise<DialogResult> {
|
||||
// Use native Electron dialog if available
|
||||
if (hasNativeDialogs()) {
|
||||
const api = (window as any).electronAPI;
|
||||
return api.openDirectory();
|
||||
}
|
||||
|
||||
// Web mode: show a modal to let user type/paste path
|
||||
const path = prompt("Enter project directory path:");
|
||||
if (!path) {
|
||||
return { canceled: true, filePaths: [] };
|
||||
@@ -271,13 +224,7 @@ export class HttpApiClient implements ElectronAPI {
|
||||
}
|
||||
|
||||
async openFile(options?: object): Promise<DialogResult> {
|
||||
// Use native Electron dialog if available
|
||||
if (hasNativeDialogs()) {
|
||||
const api = (window as any).electronAPI;
|
||||
return api.openFile(options);
|
||||
}
|
||||
|
||||
// Web mode: prompt for file path
|
||||
// Prompt for file path
|
||||
const path = prompt("Enter file path:");
|
||||
if (!path) {
|
||||
return { canceled: true, filePaths: [] };
|
||||
@@ -651,6 +598,98 @@ export class HttpApiClient implements ElectronAPI {
|
||||
error?: string;
|
||||
}> => this.get("/api/running-agents"),
|
||||
};
|
||||
|
||||
// Workspace API
|
||||
workspace = {
|
||||
getConfig: (): Promise<{
|
||||
success: boolean;
|
||||
configured: boolean;
|
||||
workspaceDir?: string;
|
||||
error?: string;
|
||||
}> => this.get("/api/workspace/config"),
|
||||
|
||||
getDirectories: (): Promise<{
|
||||
success: boolean;
|
||||
directories?: Array<{ name: string; path: string }>;
|
||||
error?: string;
|
||||
}> => this.get("/api/workspace/directories"),
|
||||
};
|
||||
|
||||
// Agent API
|
||||
agent = {
|
||||
start: (sessionId: string, workingDirectory?: string): Promise<{
|
||||
success: boolean;
|
||||
messages?: Message[];
|
||||
error?: string;
|
||||
}> => this.post("/api/agent/start", { sessionId, workingDirectory }),
|
||||
|
||||
send: (
|
||||
sessionId: string,
|
||||
message: string,
|
||||
workingDirectory?: string,
|
||||
imagePaths?: string[]
|
||||
): Promise<{ success: boolean; error?: string }> =>
|
||||
this.post("/api/agent/send", { sessionId, message, workingDirectory, imagePaths }),
|
||||
|
||||
getHistory: (sessionId: string): Promise<{
|
||||
success: boolean;
|
||||
messages?: Message[];
|
||||
isRunning?: boolean;
|
||||
error?: string;
|
||||
}> => this.post("/api/agent/history", { sessionId }),
|
||||
|
||||
stop: (sessionId: string): Promise<{ success: boolean; error?: string }> =>
|
||||
this.post("/api/agent/stop", { sessionId }),
|
||||
|
||||
clear: (sessionId: string): Promise<{ success: boolean; error?: string }> =>
|
||||
this.post("/api/agent/clear", { sessionId }),
|
||||
|
||||
onStream: (callback: (data: unknown) => void): (() => void) => {
|
||||
return this.subscribeToEvent("agent:stream", callback as EventCallback);
|
||||
},
|
||||
};
|
||||
|
||||
// Sessions API
|
||||
sessions = {
|
||||
list: (includeArchived?: boolean): Promise<{
|
||||
success: boolean;
|
||||
sessions?: SessionListItem[];
|
||||
error?: string;
|
||||
}> => this.get(`/api/sessions?includeArchived=${includeArchived || false}`),
|
||||
|
||||
create: (
|
||||
name: string,
|
||||
projectPath: string,
|
||||
workingDirectory?: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
session?: {
|
||||
id: string;
|
||||
name: string;
|
||||
projectPath: string;
|
||||
workingDirectory?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
error?: string;
|
||||
}> => this.post("/api/sessions", { name, projectPath, workingDirectory }),
|
||||
|
||||
update: (
|
||||
sessionId: string,
|
||||
name?: string,
|
||||
tags?: string[]
|
||||
): Promise<{ success: boolean; error?: string }> =>
|
||||
this.put(`/api/sessions/${sessionId}`, { name, tags }),
|
||||
|
||||
archive: (sessionId: string): Promise<{ success: boolean; error?: string }> =>
|
||||
this.post(`/api/sessions/${sessionId}/archive`, {}),
|
||||
|
||||
unarchive: (sessionId: string): Promise<{ success: boolean; error?: string }> =>
|
||||
this.post(`/api/sessions/${sessionId}/unarchive`, {}),
|
||||
|
||||
delete: (sessionId: string): Promise<{ success: boolean; error?: string }> =>
|
||||
this.httpDelete(`/api/sessions/${sessionId}`),
|
||||
};
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
|
||||
Reference in New Issue
Block a user