From 6365b511d6167198542255b27c927733a8f472a5 Mon Sep 17 00:00:00 2001 From: jiajia Date: Mon, 11 May 2026 14:53:57 +0800 Subject: [PATCH] Keep web projects on the operator device The hosted app should not save each operator's project into the shared test server filesystem when the browser can grant local folder access. Project creation now requires a browser-selected local folder in supported browsers, remembers that folder as the project browser root, and the workflow browser can list and load projects from File System Access handles. Server filesystem APIs remain as a fallback for unsupported browsers. Constraint: Hosted users need per-operator local project storage instead of shared server paths. Rejected: Keep manual absolute path entry on supported browsers | it can silently target the server filesystem in the deployed app. Confidence: high Scope-risk: moderate Directive: Do not re-enable server path entry for hosted browsers without adding per-user isolation and auth boundaries. Tested: npm test -- --run src/components/__tests__/ProjectSetupModal.test.tsx src/components/__tests__/QuickstartTemplatesView.test.tsx src/store/utils/__tests__/localStorage.test.ts Tested: npx tsc --noEmit --pretty false filtered to changed files --- src/components/ProjectSetupModal.tsx | 12 +- .../quickstart/WorkflowBrowserView.tsx | 44 ++++++ src/i18n/index.tsx | 8 + src/utils/browserFileSystem.ts | 138 ++++++++++++++++++ 4 files changed, 200 insertions(+), 2 deletions(-) diff --git a/src/components/ProjectSetupModal.tsx b/src/components/ProjectSetupModal.tsx index 1281bf99..a234fe8a 100644 --- a/src/components/ProjectSetupModal.tsx +++ b/src/components/ProjectSetupModal.tsx @@ -5,7 +5,7 @@ import { generateWorkflowId, useWorkflowStore } from "@/store/workflowStore"; import { ProviderType, ProviderSettings, NodeDefaultsConfig, LLMProvider, LLMModelType } from "@/types"; import { CanvasNavigationSettings, PanMode, ZoomMode, SelectionMode } from "@/types/canvas"; import { EnvStatusResponse } from "@/app/api/env-status/route"; -import { loadNodeDefaults, saveNodeDefaults, getLastProjectBaseDir, setLastProjectBaseDir } from "@/store/utils/localStorage"; +import { loadNodeDefaults, saveNodeDefaults, getLastProjectBaseDir, setLastProjectBaseDir, setWorkflowsDirectory } from "@/store/utils/localStorage"; import { ProviderModel } from "@/lib/providers/types"; import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog"; import { useInlineParameters } from "@/hooks/useInlineParameters"; @@ -154,6 +154,7 @@ export function ProjectSetupModal({ const [isValidating, setIsValidating] = useState(false); const [isBrowsing, setIsBrowsing] = useState(false); const [error, setError] = useState(null); + const browserLocalProjectsEnabled = isBrowserDirectoryPickerSupported(); // Provider tab state const [localProviders, setLocalProviders] = useState(providerSettings); @@ -288,6 +289,11 @@ export function ProjectSetupModal({ const fullProjectPath = ensureProjectSubfolderPath(directoryPath, name); const isBrowserPath = isBrowserFileSystemPath(fullProjectPath); + if (browserLocalProjectsEnabled && !isBrowserPath) { + setError(t("project.errorChooseLocalFolder")); + return; + } + if (!isBrowserPath && !(fullProjectPath.startsWith("/") || /^[A-Za-z]:[\\\/]/.test(fullProjectPath) || fullProjectPath.startsWith("\\\\"))) { setError(t("project.errorAbsolutePath")); return; @@ -301,6 +307,7 @@ export function ProjectSetupModal({ const id = mode === "new" ? generateWorkflowId() : useWorkflowStore.getState().workflowId || generateWorkflowId(); setUseExternalImageStorage(false); setLastProjectBaseDir(directoryPath); + setWorkflowsDirectory(directoryPath); onSave(id, name.trim(), fullProjectPath); setIsValidating(false); return; @@ -482,7 +489,8 @@ export function ProjectSetupModal({ type="text" value={directoryPath} onChange={(e) => setDirectoryPath(e.target.value)} - placeholder="/Users/username/projects/my-project" + readOnly={browserLocalProjectsEnabled} + placeholder={browserLocalProjectsEnabled ? t("project.localFolderPlaceholder") : "/Users/username/projects/my-project"} className="flex-1 px-3 py-2 bg-neutral-900 border border-neutral-600 rounded-lg text-neutral-100 text-sm focus:outline-none focus:border-neutral-500" />