Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/App.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ function setupMockAPI(options: {
const mockWorkspaces = options.workspaces ?? [];

const mockApi: IPCApi = {
dialog: {
selectDirectory: () => Promise.resolve(null),
},
providers: {
setProviderConfig: () => Promise.resolve({ success: true, data: undefined }),
list: () => Promise.resolve([]),
Expand Down Expand Up @@ -64,7 +61,11 @@ function setupMockAPI(options: {
},
projects: {
list: () => Promise.resolve(Array.from(mockProjects.entries())),
create: () => Promise.resolve({ success: true, data: { workspaces: [] } }),
create: () =>
Promise.resolve({
success: true,
data: { projectConfig: { workspaces: [] }, normalizedPath: "/mock/project/path" },
}),
remove: () => Promise.resolve({ success: true, data: undefined }),
listBranches: () =>
Promise.resolve({
Expand Down
17 changes: 11 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { WorkspaceSelection } from "./components/ProjectSidebar";
import type { FrontendWorkspaceMetadata } from "./types/workspace";
import { LeftSidebar } from "./components/LeftSidebar";
import NewWorkspaceModal from "./components/NewWorkspaceModal";
import { DirectorySelectModal } from "./components/DirectorySelectModal";
import { ProjectCreateModal } from "./components/ProjectCreateModal";
import { AIView } from "./components/AIView";
import { ErrorBoundary } from "./components/ErrorBoundary";
import { usePersistedState, updatePersistedState } from "./hooks/usePersistedState";
Expand Down Expand Up @@ -46,6 +46,7 @@ function AppInner() {
selectedWorkspace,
setSelectedWorkspace,
} = useApp();
const [projectCreateModalOpen, setProjectCreateModalOpen] = useState(false);
const [workspaceModalOpen, setWorkspaceModalOpen] = useState(false);
const [workspaceModalProject, setWorkspaceModalProject] = useState<string | null>(null);
const [workspaceModalProjectName, setWorkspaceModalProjectName] = useState<string>("");
Expand Down Expand Up @@ -218,8 +219,8 @@ function AppInner() {

// Memoize callbacks to prevent LeftSidebar/ProjectSidebar re-renders
const handleAddProjectCallback = useCallback(() => {
void addProject();
}, [addProject]);
setProjectCreateModalOpen(true);
}, []);

const handleAddWorkspaceCallback = useCallback(
(projectPath: string) => {
Expand Down Expand Up @@ -473,8 +474,8 @@ function AppInner() {
);

const addProjectFromPalette = useCallback(() => {
void addProject();
}, [addProject]);
setProjectCreateModalOpen(true);
}, []);

const removeProjectFromPalette = useCallback(
(path: string) => {
Expand Down Expand Up @@ -694,7 +695,11 @@ function AppInner() {
onAdd={handleCreateWorkspace}
/>
)}
<DirectorySelectModal />
<ProjectCreateModal
isOpen={projectCreateModalOpen}
onClose={() => setProjectCreateModalOpen(false)}
onSuccess={addProject}
/>
</div>
</>
);
Expand Down
19 changes: 0 additions & 19 deletions src/browser/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,27 +184,8 @@ class WebSocketManager {

const wsManager = new WebSocketManager();

// Directory selection via custom event (for browser mode)
interface DirectorySelectEvent extends CustomEvent {
detail: {
resolve: (path: string | null) => void;
};
}

function requestDirectorySelection(): Promise<string | null> {
return new Promise((resolve) => {
const event = new CustomEvent("directory-select-request", {
detail: { resolve },
}) as DirectorySelectEvent;
window.dispatchEvent(event);
});
}

// Create the Web API implementation
const webApi: IPCApi = {
dialog: {
selectDirectory: requestDirectorySelection,
},
providers: {
setProviderConfig: (provider, keyPath, value) =>
invokeIPC(IPC_CHANNELS.PROVIDERS_SET_CONFIG, provider, keyPath, value),
Expand Down
94 changes: 0 additions & 94 deletions src/components/DirectorySelectModal.tsx

This file was deleted.

123 changes: 123 additions & 0 deletions src/components/ProjectCreateModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React, { useState, useCallback } from "react";
import { Modal, ModalActions, CancelButton, PrimaryButton } from "./Modal";
import type { ProjectConfig } from "@/config";

interface ProjectCreateModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: (normalizedPath: string, projectConfig: ProjectConfig) => void;
}

/**
* Project creation modal that handles the full flow from path input to backend validation.
*
* Displays a modal for path input, calls the backend to create the project, and shows
* validation errors inline. Modal stays open until project is successfully created or user cancels.
*/
export const ProjectCreateModal: React.FC<ProjectCreateModalProps> = ({
isOpen,
onClose,
onSuccess,
}) => {
const [path, setPath] = useState("");
const [error, setError] = useState("");
const [isCreating, setIsCreating] = useState(false);

const handleCancel = useCallback(() => {
setPath("");
setError("");
onClose();
}, [onClose]);

const handleSelect = useCallback(async () => {
const trimmedPath = path.trim();
if (!trimmedPath) {
setError("Please enter a directory path");
return;
}

setError("");
setIsCreating(true);

try {
// First check if project already exists
const existingProjects = await window.api.projects.list();
const existingPaths = new Map(existingProjects);

// Try to create the project
const result = await window.api.projects.create(trimmedPath);

if (result.success) {
// Check if duplicate (backend may normalize the path)
const { normalizedPath, projectConfig } = result.data as {
normalizedPath: string;
projectConfig: ProjectConfig;
};
if (existingPaths.has(normalizedPath)) {
setError("This project has already been added.");
return;
}

// Success - notify parent and close
onSuccess(normalizedPath, projectConfig);
setPath("");
setError("");
onClose();
} else {
// Backend validation error - show inline, keep modal open
const errorMessage =
typeof result.error === "string" ? result.error : "Failed to add project";
setError(errorMessage);
}
} catch (err) {
// Unexpected error
const errorMessage = err instanceof Error ? err.message : "An unexpected error occurred";
setError(`Failed to add project: ${errorMessage}`);
} finally {
setIsCreating(false);
}
}, [path, onSuccess, onClose]);

const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
void handleSelect();
}
},
[handleSelect]
);

return (
<Modal
isOpen={isOpen}
title="Add Project"
subtitle="Enter the path to your project directory"
onClose={handleCancel}
isLoading={isCreating}
>
<input
type="text"
value={path}
onChange={(e) => {
setPath(e.target.value);
setError("");
}}
onKeyDown={handleKeyDown}
placeholder="/home/user/projects/my-project"
autoFocus
disabled={isCreating}
className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-muted mb-5 w-full rounded border px-3 py-2 font-mono text-sm text-white focus:outline-none disabled:opacity-50"
/>
{error && <div className="text-error -mt-3 mb-3 text-xs">{error}</div>}
<ModalActions>
<CancelButton onClick={handleCancel} disabled={isCreating}>
Cancel
</CancelButton>
<PrimaryButton onClick={() => void handleSelect()} disabled={isCreating}>
{isCreating ? "Adding..." : "Add Project"}
</PrimaryButton>
</ModalActions>
</Modal>
);
};
3 changes: 0 additions & 3 deletions src/constants/ipc-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
*/

export const IPC_CHANNELS = {
// Dialog channels
DIALOG_SELECT_DIR: "dialog:selectDirectory",

// Provider channels
PROVIDERS_SET_CONFIG: "providers:setConfig",
PROVIDERS_LIST: "providers:list",
Expand Down
2 changes: 1 addition & 1 deletion src/contexts/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface AppContextType {
// Projects
projects: Map<string, ProjectConfig>;
setProjects: Dispatch<SetStateAction<Map<string, ProjectConfig>>>;
addProject: () => Promise<void>;
addProject: (normalizedPath: string, projectConfig: ProjectConfig) => void;
removeProject: (path: string) => Promise<void>;

// Workspaces
Expand Down
Loading