mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 20:03:37 +00:00
Merge branch 'main' into feature/shared-packages
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import {
|
||||
FolderOpen,
|
||||
@@ -21,6 +20,11 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { getJSON, setJSON } from "@/lib/storage";
|
||||
import {
|
||||
getDefaultWorkspaceDirectory,
|
||||
saveLastProjectDirectory,
|
||||
} from "@/lib/workspace-config";
|
||||
|
||||
interface DirectoryEntry {
|
||||
name: string;
|
||||
@@ -50,38 +54,22 @@ const RECENT_FOLDERS_KEY = "file-browser-recent-folders";
|
||||
const MAX_RECENT_FOLDERS = 5;
|
||||
|
||||
function getRecentFolders(): string[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
const stored = localStorage.getItem(RECENT_FOLDERS_KEY);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return getJSON<string[]>(RECENT_FOLDERS_KEY) ?? [];
|
||||
}
|
||||
|
||||
function addRecentFolder(path: string): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
const recent = getRecentFolders();
|
||||
// Remove if already exists, then add to front
|
||||
const filtered = recent.filter((p) => p !== path);
|
||||
const updated = [path, ...filtered].slice(0, MAX_RECENT_FOLDERS);
|
||||
localStorage.setItem(RECENT_FOLDERS_KEY, JSON.stringify(updated));
|
||||
} catch {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
const recent = getRecentFolders();
|
||||
// Remove if already exists, then add to front
|
||||
const filtered = recent.filter((p) => p !== path);
|
||||
const updated = [path, ...filtered].slice(0, MAX_RECENT_FOLDERS);
|
||||
setJSON(RECENT_FOLDERS_KEY, updated);
|
||||
}
|
||||
|
||||
function removeRecentFolder(path: string): string[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
const recent = getRecentFolders();
|
||||
const updated = recent.filter((p) => p !== path);
|
||||
localStorage.setItem(RECENT_FOLDERS_KEY, JSON.stringify(updated));
|
||||
return updated;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const recent = getRecentFolders();
|
||||
const updated = recent.filter((p) => p !== path);
|
||||
setJSON(RECENT_FOLDERS_KEY, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function FileBrowserDialog({
|
||||
@@ -110,17 +98,16 @@ export function FileBrowserDialog({
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleRemoveRecent = useCallback((e: React.MouseEvent, path: string) => {
|
||||
e.stopPropagation();
|
||||
const updated = removeRecentFolder(path);
|
||||
setRecentFolders(updated);
|
||||
}, []);
|
||||
const handleRemoveRecent = useCallback(
|
||||
(e: React.MouseEvent, path: string) => {
|
||||
e.stopPropagation();
|
||||
const updated = removeRecentFolder(path);
|
||||
setRecentFolders(updated);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSelectRecent = useCallback((path: string) => {
|
||||
browseDirectory(path);
|
||||
}, []);
|
||||
|
||||
const browseDirectory = async (dirPath?: string) => {
|
||||
const browseDirectory = useCallback(async (dirPath?: string) => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
setWarning("");
|
||||
@@ -155,7 +142,14 @@ export function FileBrowserDialog({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSelectRecent = useCallback(
|
||||
(path: string) => {
|
||||
browseDirectory(path);
|
||||
},
|
||||
[browseDirectory]
|
||||
);
|
||||
|
||||
// Reset current path when dialog closes
|
||||
useEffect(() => {
|
||||
@@ -169,12 +163,46 @@ export function FileBrowserDialog({
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Load initial path or home directory when dialog opens
|
||||
// Load initial path or workspace directory when dialog opens
|
||||
useEffect(() => {
|
||||
if (open && !currentPath) {
|
||||
browseDirectory(initialPath);
|
||||
// Priority order:
|
||||
// 1. Last selected directory from this file browser (from localStorage)
|
||||
// 2. initialPath prop (from parent component)
|
||||
// 3. Default workspace directory
|
||||
// 4. Home directory
|
||||
const loadInitialPath = async () => {
|
||||
try {
|
||||
// First, check for last selected directory from getDefaultWorkspaceDirectory
|
||||
// which already implements the priority: last used > Documents/Automaker > DATA_DIR
|
||||
const defaultDir = await getDefaultWorkspaceDirectory();
|
||||
|
||||
// If we have a default directory, use it (unless initialPath is explicitly provided and different)
|
||||
const pathToUse = initialPath || defaultDir;
|
||||
|
||||
if (pathToUse) {
|
||||
// Pre-fill the path input immediately
|
||||
setPathInput(pathToUse);
|
||||
// Then browse to that directory
|
||||
browseDirectory(pathToUse);
|
||||
} else {
|
||||
// No default directory, browse home directory
|
||||
browseDirectory();
|
||||
}
|
||||
} catch (err) {
|
||||
// If config fetch fails, try initialPath or fall back to home directory
|
||||
if (initialPath) {
|
||||
setPathInput(initialPath);
|
||||
browseDirectory(initialPath);
|
||||
} else {
|
||||
browseDirectory();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadInitialPath();
|
||||
}
|
||||
}, [open, initialPath]);
|
||||
}, [open, initialPath, currentPath, browseDirectory]);
|
||||
|
||||
const handleSelectDirectory = (dir: DirectoryEntry) => {
|
||||
browseDirectory(dir.path);
|
||||
@@ -211,6 +239,8 @@ export function FileBrowserDialog({
|
||||
const handleSelect = useCallback(() => {
|
||||
if (currentPath) {
|
||||
addRecentFolder(currentPath);
|
||||
// Save to last project directory so it's used as default next time
|
||||
saveLastProjectDirectory(currentPath);
|
||||
onSelect(currentPath);
|
||||
onOpenChange(false);
|
||||
}
|
||||
@@ -296,7 +326,9 @@ export function FileBrowserDialog({
|
||||
title={folder}
|
||||
>
|
||||
<Folder className="w-3 h-3 text-brand-500 shrink-0" />
|
||||
<span className="truncate max-w-[120px]">{getFolderName(folder)}</span>
|
||||
<span className="truncate max-w-[120px]">
|
||||
{getFolderName(folder)}
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => handleRemoveRecent(e, folder)}
|
||||
className="ml-0.5 opacity-0 group-hover:opacity-100 hover:text-destructive transition-opacity"
|
||||
@@ -417,11 +449,20 @@ export function FileBrowserDialog({
|
||||
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSelect} disabled={!currentPath || loading} title="Select current folder (Cmd+Enter / Ctrl+Enter)">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSelect}
|
||||
disabled={!currentPath || loading}
|
||||
title="Select current folder (Cmd+Enter / Ctrl+Enter)"
|
||||
>
|
||||
<FolderOpen className="w-3.5 h-3.5 mr-1.5" />
|
||||
Select Current Folder
|
||||
<kbd className="ml-2 px-1.5 py-0.5 text-[10px] bg-background/50 rounded border border-border">
|
||||
{typeof navigator !== "undefined" && navigator.platform?.includes("Mac") ? "⌘" : "Ctrl"}+↵
|
||||
{typeof navigator !== "undefined" &&
|
||||
navigator.platform?.includes("Mac")
|
||||
? "⌘"
|
||||
: "Ctrl"}
|
||||
+↵
|
||||
</kbd>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -26,11 +25,12 @@ import {
|
||||
} from "lucide-react";
|
||||
import { starterTemplates, type StarterTemplate } from "@/lib/templates";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { getHttpApiClient } from "@/lib/http-api-client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useFileBrowser } from "@/contexts/file-browser-context";
|
||||
|
||||
const LAST_PROJECT_DIR_KEY = "automaker:lastProjectDir";
|
||||
import {
|
||||
getDefaultWorkspaceDirectory,
|
||||
saveLastProjectDirectory,
|
||||
} from "@/lib/workspace-config";
|
||||
|
||||
interface ValidationErrors {
|
||||
projectName?: boolean;
|
||||
@@ -81,25 +81,15 @@ export function NewProjectModal({
|
||||
// Fetch workspace directory when modal opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// First, check localStorage for last used directory
|
||||
const lastUsedDir = localStorage.getItem(LAST_PROJECT_DIR_KEY);
|
||||
if (lastUsedDir) {
|
||||
setWorkspaceDir(lastUsedDir);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to server config if no saved directory
|
||||
setIsLoadingWorkspace(true);
|
||||
const httpClient = getHttpApiClient();
|
||||
httpClient.workspace
|
||||
.getConfig()
|
||||
.then((result) => {
|
||||
if (result.success && result.workspaceDir) {
|
||||
setWorkspaceDir(result.workspaceDir);
|
||||
getDefaultWorkspaceDirectory()
|
||||
.then((defaultDir) => {
|
||||
if (defaultDir) {
|
||||
setWorkspaceDir(defaultDir);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to get workspace config:", error);
|
||||
console.error("Failed to get default workspace directory:", error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingWorkspace(false);
|
||||
@@ -211,7 +201,7 @@ export function NewProjectModal({
|
||||
if (selectedPath) {
|
||||
setWorkspaceDir(selectedPath);
|
||||
// Save to localStorage for next time
|
||||
localStorage.setItem(LAST_PROJECT_DIR_KEY, selectedPath);
|
||||
saveLastProjectDirectory(selectedPath);
|
||||
// Clear any workspace error when a valid directory is selected
|
||||
if (errors.workspaceDir) {
|
||||
setErrors((prev) => ({ ...prev, workspaceDir: false }));
|
||||
@@ -296,9 +286,7 @@ export function NewProjectModal({
|
||||
{projectPath || workspaceDir}
|
||||
</code>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-red-500">No workspace configured</span>
|
||||
)}
|
||||
) : null}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -191,7 +191,9 @@ export function WorktreeTab({
|
||||
)}
|
||||
onClick={() => onSelectWorktree(worktree)}
|
||||
disabled={isActivating}
|
||||
title="Click to preview main"
|
||||
title={`Click to preview ${worktree.branch}`}
|
||||
aria-label={worktree.branch}
|
||||
data-testid={`worktree-branch-${worktree.branch}`}
|
||||
>
|
||||
{isRunning && <Loader2 className="w-3 h-3 animate-spin" />}
|
||||
{isActivating && !isRunning && (
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
PanelLeftClose,
|
||||
} from "lucide-react";
|
||||
import { cn, pathsEqual } from "@/lib/utils";
|
||||
import { getItem, setItem } from "@/lib/storage";
|
||||
import type { WorktreePanelProps, WorktreeInfo } from "./types";
|
||||
import {
|
||||
useWorktrees,
|
||||
@@ -91,13 +92,12 @@ export function WorktreePanel({
|
||||
|
||||
// Collapse state with localStorage persistence
|
||||
const [isCollapsed, setIsCollapsed] = useState(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
const saved = localStorage.getItem(WORKTREE_PANEL_COLLAPSED_KEY);
|
||||
const saved = getItem(WORKTREE_PANEL_COLLAPSED_KEY);
|
||||
return saved === "true";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(WORKTREE_PANEL_COLLAPSED_KEY, String(isCollapsed));
|
||||
setItem(WORKTREE_PANEL_COLLAPSED_KEY, String(isCollapsed));
|
||||
}, [isCollapsed]);
|
||||
|
||||
const toggleCollapsed = () => setIsCollapsed((prev) => !prev);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { useAppStore, Feature } from "@/store/app-store";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
@@ -20,6 +19,10 @@ import { Markdown } from "@/components/ui/markdown";
|
||||
import { useFileBrowser } from "@/contexts/file-browser-context";
|
||||
import { toast } from "sonner";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
getDefaultWorkspaceDirectory,
|
||||
saveLastProjectDirectory,
|
||||
} from "@/lib/workspace-config";
|
||||
|
||||
interface InterviewMessage {
|
||||
id: string;
|
||||
@@ -65,8 +68,7 @@ const INTERVIEW_QUESTIONS = [
|
||||
];
|
||||
|
||||
export function InterviewView() {
|
||||
const { addProject, setCurrentProject, setAppSpec } =
|
||||
useAppStore();
|
||||
const { addProject, setCurrentProject, setAppSpec } = useAppStore();
|
||||
const { openFileBrowser } = useFileBrowser();
|
||||
const navigate = useNavigate();
|
||||
const [input, setInput] = useState("");
|
||||
@@ -89,6 +91,35 @@ export function InterviewView() {
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Default parent directory using workspace config utility
|
||||
useEffect(() => {
|
||||
if (projectPath) return;
|
||||
|
||||
let isMounted = true;
|
||||
|
||||
const loadWorkspaceDir = async () => {
|
||||
try {
|
||||
const defaultDir = await getDefaultWorkspaceDirectory();
|
||||
|
||||
if (!isMounted || projectPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (defaultDir) {
|
||||
setProjectPath(defaultDir);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load default workspace directory:", error);
|
||||
}
|
||||
};
|
||||
|
||||
loadWorkspaceDir();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [projectPath]);
|
||||
|
||||
// Initialize with first question
|
||||
useEffect(() => {
|
||||
if (messages.length === 0) {
|
||||
@@ -295,10 +326,12 @@ export function InterviewView() {
|
||||
title: "Select Base Directory",
|
||||
description:
|
||||
"Choose the parent directory where your new project will be created",
|
||||
initialPath: projectPath || undefined,
|
||||
});
|
||||
|
||||
if (selectedPath) {
|
||||
setProjectPath(selectedPath);
|
||||
saveLastProjectDirectory(selectedPath);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -308,6 +341,7 @@ export function InterviewView() {
|
||||
setIsGenerating(true);
|
||||
|
||||
try {
|
||||
saveLastProjectDirectory(projectPath);
|
||||
const api = getElectronAPI();
|
||||
// Use platform-specific path separator
|
||||
const pathSep =
|
||||
@@ -423,8 +457,8 @@ export function InterviewView() {
|
||||
index < currentQuestionIndex
|
||||
? "bg-green-500"
|
||||
: index === currentQuestionIndex
|
||||
? "bg-primary"
|
||||
: "bg-zinc-700"
|
||||
? "bg-primary"
|
||||
: "bg-zinc-700"
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -239,6 +239,24 @@ export function WelcomeView() {
|
||||
const api = getElectronAPI();
|
||||
const projectPath = `${parentDir}/${projectName}`;
|
||||
|
||||
// Validate that parent directory exists
|
||||
const parentExists = await api.exists(parentDir);
|
||||
if (!parentExists) {
|
||||
toast.error("Parent directory does not exist", {
|
||||
description: `Cannot create project in non-existent directory: ${parentDir}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify parent is actually a directory
|
||||
const parentStat = await api.stat(parentDir);
|
||||
if (parentStat && !parentStat.isDirectory) {
|
||||
toast.error("Parent path is not a directory", {
|
||||
description: `${parentDir} is not a directory`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create project directory
|
||||
const mkdirResult = await api.mkdir(projectPath);
|
||||
if (!mkdirResult.success) {
|
||||
|
||||
Reference in New Issue
Block a user