Skip to content
6 changes: 4 additions & 2 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ function AppInner() {
selectedWorkspace,
setSelectedWorkspace,
pendingNewWorkspaceProject,
pendingNewWorkspaceSectionId,
pendingNewWorkspaceSubProjectPath,
pendingNewWorkspaceDraftId,
beginWorkspaceCreation,
} = useWorkspaceContext();
Expand Down Expand Up @@ -169,6 +169,7 @@ function AppInner() {
refreshProjects,
removeProject,
openProjectCreateModal,
projectCreateInitialPath,
isProjectCreateModalOpen,
closeProjectCreateModal,
addProject,
Expand Down Expand Up @@ -1154,7 +1155,7 @@ function AppInner() {
projectName={projectName}
leftSidebarCollapsed={sidebarCollapsed}
onToggleLeftSidebarCollapsed={handleToggleSidebar}
pendingSectionId={pendingNewWorkspaceSectionId}
pendingSubProjectPath={pendingNewWorkspaceSubProjectPath}
pendingDraftId={pendingNewWorkspaceDraftId}
onWorkspaceCreated={(metadata, options) => {
// IMPORTANT: Add workspace to store FIRST (synchronous) to ensure
Expand Down Expand Up @@ -1217,6 +1218,7 @@ function AppInner() {
</div>
<CommandPalette getSlashContext={() => ({ workspaceId: selectedWorkspace?.workspaceId })} />
<ProjectCreateModal
initialPath={projectCreateInitialPath}
isOpen={isProjectCreateModalOpen}
onClose={closeProjectCreateModal}
onSuccess={(normalizedPath, projectConfig) => {
Expand Down
20 changes: 17 additions & 3 deletions src/browser/components/ProjectCreateModal/ProjectCreateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ function useDirectoryPicker(params: {
const browse = useCallback(async () => {
if (isDesktop) {
try {
const selectedPath = await api?.projects.pickDirectory();
// Seed the native directory picker with the user's current input so
// Browse opens at that path when it's already an existing directory.
const selectedPath = await api?.projects.pickDirectory({ initialPath });
if (selectedPath) {
onSelectPath(selectedPath);
}
Expand All @@ -53,7 +55,7 @@ function useDirectoryPicker(params: {
if (hasWebFsPicker) {
setIsDirPickerOpen(true);
}
}, [api, errorLabel, hasWebFsPicker, isDesktop, onSelectPath]);
}, [api, errorLabel, hasWebFsPicker, initialPath, isDesktop, onSelectPath]);

const directoryPickerModal = hasWebFsPicker ? (
<DirectoryPickerModal
Expand All @@ -68,13 +70,16 @@ function useDirectoryPicker(params: {
}

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

interface ProjectCreateFormProps {
onSuccess: (normalizedPath: string, projectConfig: ProjectConfig) => void;
/** Optional initial path for parent-scoped sub-project creation. */
initialPath?: string;
/**
* Optional close handler for modal-style usage.
* When provided, the form will call it on cancel and after a successful add.
Expand Down Expand Up @@ -102,6 +107,7 @@ export interface ProjectCreateFormHandle {
export const ProjectCreateForm = React.forwardRef<ProjectCreateFormHandle, ProjectCreateFormProps>(
function ProjectCreateForm(
{
initialPath,
onSuccess,
onClose,
showCancelButton = false,
Expand All @@ -116,10 +122,14 @@ export const ProjectCreateForm = React.forwardRef<ProjectCreateFormHandle, Proje
ref
) {
const { api } = useAPI();
const [path, setPath] = useState("");
const [path, setPath] = useState(initialPath ?? "");
const [error, setError] = useState("");
const [isCreating, setIsCreating] = useState(false);

useEffect(() => {
setPath(initialPath ?? "");
}, [initialPath]);

const setCreating = useCallback(
(next: boolean) => {
setIsCreating(next);
Expand Down Expand Up @@ -776,6 +786,7 @@ export interface ProjectAddFormHandle {
}

interface ProjectAddFormProps {
initialPath?: string;
onSuccess: (normalizedPath: string, projectConfig: ProjectConfig) => void;
onClose?: () => void;
isOpen: boolean;
Expand Down Expand Up @@ -912,6 +923,7 @@ export const ProjectAddForm = React.forwardRef<ProjectAddFormHandle, ProjectAddF

{mode === "pick-folder" ? (
<ProjectCreateForm
initialPath={props.initialPath}
ref={projectCreateFormRef}
onSuccess={props.onSuccess}
onClose={props.onClose}
Expand Down Expand Up @@ -960,6 +972,7 @@ ProjectAddForm.displayName = "ProjectAddForm";
* validation errors inline. Modal stays open until project is successfully created or user cancels.
*/
export const ProjectCreateModal: React.FC<ProjectCreateModalProps> = ({
initialPath,
isOpen,
onClose,
onSuccess,
Expand All @@ -984,6 +997,7 @@ export const ProjectCreateModal: React.FC<ProjectCreateModalProps> = ({
</DialogHeader>

<ProjectAddForm
initialPath={initialPath}
isOpen={isOpen}
onSuccess={onSuccess}
onClose={onClose}
Expand Down
66 changes: 31 additions & 35 deletions src/browser/components/ProjectPage/ProjectPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,8 @@ export const CreateWorkspaceMultipleProjects: AppStory = {
};

/**
* Creation view with project sections configured.
* On desktop the section selector is inline / right-aligned in the header row.
* On mobile it drops to its own row below the header.
* Creation view with sub-projects configured.
* Sub-projects are shown in the sidebar rather than in a creation-time section selector.
*
* Includes mobile chromatic modes: the sidebar starts expanded via
* localStorage so the play function can click the project row, then
Expand All @@ -161,15 +160,32 @@ export const CreateWorkspaceWithSections: AppStory = {
localStorage.setItem(LEFT_SIDEBAR_COLLAPSED_KEY, JSON.stringify(false));
return createMockORPCClient({
projects: new Map([
["/Users/dev/my-project", { workspaces: [] }],
[
"/Users/dev/my-project",
"/Users/dev/my-project/frontend",
{
displayName: "Frontend",
color: "#4f8cf7",
parentProjectPath: "/Users/dev/my-project",
workspaces: [],
},
],
[
"/Users/dev/my-project/backend",
{
displayName: "Backend",
color: "#f76b4f",
parentProjectPath: "/Users/dev/my-project",
workspaces: [],
},
],
[
"/Users/dev/my-project/infra",
{
displayName: "Infra",
color: "#8b5cf6",
parentProjectPath: "/Users/dev/my-project",
workspaces: [],
sections: [
{ id: "sec_0001", name: "Frontend", color: "#4f8cf7", nextId: "sec_0002" },
{ id: "sec_0002", name: "Backend", color: "#f76b4f", nextId: "sec_0003" },
{ id: "sec_0003", name: "Infra", color: "#8b5cf6", nextId: null },
],
},
],
]),
Expand Down Expand Up @@ -198,35 +214,15 @@ export const CreateWorkspaceWithSections: AppStory = {
}
}

// Wait for the section selector to be visible. Two instances exist in
// the DOM (one for desktop inline, one for mobile own-row via
// hidden/md:hidden). Find the one that's actually rendered.
// The creation form should stay focused on workspace setup; sub-project selection
// now happens by navigating to the nested project in the sidebar.
await waitFor(
() => {
const allSelectors = storyRoot.querySelectorAll<HTMLElement>(
"[data-testid='section-selector']"
const workspaceNameGroup = storyRoot.querySelector(
"[data-component='WorkspaceNameGroup']"
);
const sectionSelector = Array.from(allSelectors).find(
(el) => el.offsetWidth > 0 && el.offsetHeight > 0
);
if (!sectionSelector) {
throw new Error("Section selector not visible");
}

// On narrow viewports, verify the section selector sits below
// the header row (mobile-only own-row layout).
if (window.innerWidth < 768) {
const headerRow = storyRoot.querySelector("[data-component='WorkspaceNameGroup']");
if (!headerRow) {
throw new Error("Workspace name header row not found");
}
const headerBottom = headerRow.getBoundingClientRect().bottom;
const sectionTop = sectionSelector.getBoundingClientRect().top;
if (sectionTop < headerBottom) {
throw new Error(
`Section selector overlaps header row (section top=${sectionTop}, header bottom=${headerBottom})`
);
}
if (!workspaceNameGroup) {
throw new Error("Workspace name header row not found");
}
},
{ timeout: 10000 }
Expand Down
10 changes: 5 additions & 5 deletions src/browser/components/ProjectPage/ProjectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ interface ProjectPageProps {
projectName: string;
leftSidebarCollapsed: boolean;
onToggleLeftSidebarCollapsed: () => void;
/** Sub-project path for parent-owned draft creation. */
pendingSubProjectPath?: string | null;
/** Draft ID for UI-only workspace creation drafts (from URL) */
pendingDraftId?: string | null;
/** Section ID to pre-select when creating (from sidebar section "+" button) */
pendingSectionId?: string | null;
onWorkspaceCreated: (
metadata: FrontendWorkspaceMetadata,
options?: WorkspaceCreatedOptions
Expand Down Expand Up @@ -74,8 +74,8 @@ export const ProjectPage: React.FC<ProjectPageProps> = ({
projectName,
leftSidebarCollapsed,
onToggleLeftSidebarCollapsed,
pendingSubProjectPath,
pendingDraftId,
pendingSectionId,
onWorkspaceCreated,
}) => {
const { api } = useAPI();
Expand Down Expand Up @@ -319,15 +319,15 @@ export const ProjectPage: React.FC<ProjectPageProps> = ({
<ConfiguredProvidersBar providersConfig={providersConfig} />
)
)}
{/* ChatInput for workspace creation - includes section selector */}
{/* ChatInput for workspace creation. */}
<ChatInput
// Key by project + draft so project navigation and draft switches both remount
// creation-local state (including any in-flight creation overlays).
key={`${projectPath}:${pendingDraftId ?? "__pending__"}`}
variant="creation"
projectPath={projectPath}
projectName={projectName}
pendingSectionId={pendingSectionId}
pendingSubProjectPath={pendingSubProjectPath}
pendingDraftId={pendingDraftId}
onReady={handleChatReady}
onWorkspaceCreated={onWorkspaceCreated}
Expand Down
Loading
Loading