From 162521dc23acfa0101ccbbced9942130f34a0379 Mon Sep 17 00:00:00 2001 From: jiajia Date: Mon, 27 Apr 2026 10:34:44 +0800 Subject: [PATCH] Let browser users choose local workflow folders Server-side native folder pickers cannot select a Windows user's local directory when the app is deployed remotely. This adds a browser File System Access path for supported browsers so the user can choose a local folder, create the project subfolder, and save the workflow JSON directly from the client. Constraint: File System Access handles are browser-only and cannot be passed to server filesystem APIs Constraint: External media folder storage still depends on server filesystem endpoints, so browser-local projects keep media embedded or remote in the workflow JSON Rejected: Make /api/browse-directory choose the user's Windows folder | that endpoint runs on the server, not in the browser Confidence: medium Scope-risk: moderate Directive: Extend the browser filesystem utility for local media inputs/generations before enabling external media storage for browserfs paths Tested: npx vitest run src/components/__tests__/ProjectSetupModal.test.tsx src/components/__tests__/Header.test.tsx Tested: npm run build Not-tested: Manual Chrome/Edge File System Access flow on Windows test server --- src/components/Header.tsx | 7 +- src/components/ProjectSetupModal.tsx | 29 ++++- src/store/workflowStore.ts | 35 +++--- src/utils/browserFileSystem.ts | 164 +++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 17 deletions(-) create mode 100644 src/utils/browserFileSystem.ts diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 488285fb..68b53dea 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -8,6 +8,7 @@ import { CostIndicator } from "./CostIndicator"; import { KeyboardShortcutsDialog } from "./KeyboardShortcutsDialog"; import { WorkflowBrowserModal } from "./WorkflowBrowserModal"; import { LANGUAGE_OPTIONS, useI18n } from "@/i18n"; +import { isBrowserFileSystemPath } from "@/utils/browserFileSystem"; function CommentsNavigationIcon() { const { t } = useI18n(); @@ -137,6 +138,10 @@ export function Header() { const handleOpenDirectory = async () => { if (!saveDirectoryPath) return; + if (isBrowserFileSystemPath(saveDirectoryPath)) { + alert(t("header.openFolderFailed", { error: "Browser-selected folders cannot be opened from the server." })); + return; + } try { const response = await fetch("/api/open-directory", { @@ -261,7 +266,7 @@ export function Header() { )} - {saveDirectoryPath && ( + {saveDirectoryPath && !isBrowserFileSystemPath(saveDirectoryPath) && (