From 91737b183114a975c51628f586fae943d1f1d4c9 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 2 Jul 2026 11:04:49 -0700 Subject: [PATCH] FN-7433: add explicit onboarding git setup modes Expose explicit git setup choices in first-run onboarding and project registration. - Add setup wizard modes for existing repositories, new git initialization, and remote cloning with mode-specific hints and validation. - Extend project registration to accept gitSetupMode while preserving legacy cloneUrl-only behavior. - Cover init and clone validation paths with dashboard tests, localized copy, docs, and a published package changeset. Files changed: .changeset/fn-7433-onboarding-git-setup.md | 7 + docs/dashboard-guide.md | 10 ++ docs/getting-started.md | 10 +- packages/dashboard/app/api/legacy.ts | 1 + .../dashboard/app/components/SetupWizardModal.css | 77 +++++++++- .../dashboard/app/components/SetupWizardModal.tsx | 159 ++++++++++++++------- .../components/__tests__/SetupWizardModal.test.tsx | 107 +++++++++++++- .../dashboard/src/__tests__/project-routes.test.ts | 98 +++++++++++++ .../src/routes/register-project-routes.ts | 25 +++- packages/i18n/locales/en/app.json | 23 ++- packages/i18n/locales/es/app.json | 25 +++- packages/i18n/locales/fr/app.json | 25 +++- packages/i18n/locales/ko/app.json | 25 +++- packages/i18n/locales/zh-CN/app.json | 25 +++- packages/i18n/locales/zh-TW/app.json | 25 +++- 15 files changed, 538 insertions(+), 104 deletions(-) Fusion-Task-Id: FN-7433 Fusion-Task-Lineage: 022c25c2-3c2d-4e47-aa8c-02525caad672 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7433-onboarding-git-setup.md | 7 + docs/dashboard-guide.md | 10 ++ docs/getting-started.md | 10 +- packages/dashboard/app/api/legacy.ts | 1 + .../app/components/SetupWizardModal.css | 77 ++++++++- .../app/components/SetupWizardModal.tsx | 159 ++++++++++++------ .../__tests__/SetupWizardModal.test.tsx | 107 +++++++++++- .../src/__tests__/project-routes.test.ts | 98 +++++++++++ .../src/routes/register-project-routes.ts | 25 ++- packages/i18n/locales/en/app.json | 23 ++- packages/i18n/locales/es/app.json | 25 ++- packages/i18n/locales/fr/app.json | 25 ++- packages/i18n/locales/ko/app.json | 25 ++- packages/i18n/locales/zh-CN/app.json | 25 ++- packages/i18n/locales/zh-TW/app.json | 25 ++- 15 files changed, 538 insertions(+), 104 deletions(-) create mode 100644 .changeset/fn-7433-onboarding-git-setup.md diff --git a/.changeset/fn-7433-onboarding-git-setup.md b/.changeset/fn-7433-onboarding-git-setup.md new file mode 100644 index 0000000000..aaed5f19e2 --- /dev/null +++ b/.changeset/fn-7433-onboarding-git-setup.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add explicit onboarding choices to use, initialize, or clone a git repository. +category: feature +dev: Setup wizard now sends gitSetupMode while preserving cloneUrl-only project registration clients. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 17cbe56c15..7c4469ccc1 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -778,6 +778,16 @@ Memory view provides a multi-file editor for project and daily memory files. Its ![Memory view](./screenshots/memory-view.png) +## Setup Wizard Project Registration + +First-run setup and embedded project setup both expose a **Repository setup** section before path entry: + +- **Use Existing Directory** registers an existing git repository or workspace root. Workspace detection and the workspace-mode checkbox only appear in this mode. +- **Initialize New Repository** registers the selected local folder and relies on the server-side project registration path to run `git init` if the folder is not already a git repository. +- **Clone Git Repository** requires a non-blank remote URL and an absolute destination path. Fusion runs `git clone` with argument-vector execution, requires the destination to be absent or empty, cleans up a newly-created failed destination best-effort, then registers the cloned folder. + +Advanced setup remains limited to runtime node and isolation-mode choices, so repository mode selection is not hidden behind the advanced panel. + ## Agents View Agent list and detail surfaces now surface pending approvals per agent: diff --git a/docs/getting-started.md b/docs/getting-started.md index 14dbcb4ec2..64757167e4 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -90,11 +90,15 @@ Start the dashboard: fn dashboard ``` -On first launch, Fusion opens an onboarding wizard with three steps: +On first launch, Fusion opens an onboarding wizard with guided setup steps: 1. **AI Setup** — choose a provider and authenticate (you only need one to start). Anthropic/Claude and OpenAI Codex use a pasted authorization-code OAuth flow in onboarding and Settings (sign in, then paste the final redirect URL or code back into Fusion), and Fusion warns before login so you remember to copy the browser address bar URL before the redirect tab appears to fail. After the initial Claude OAuth login, Fusion normally refreshes the OAuth credential automatically with the stored refresh token when the access token expires, so repeated manual re-login is not usually required. **Anthropic — via Claude CLI** remains available as a separate optional path. Deprecated Google Gemini CLI / Antigravity entries are hidden; Google/Gemini API key, Google Generative AI, Vertex, and Cloud Code options remain available. -2. **GitHub (Optional)** — connect GitHub for issue import and PR workflows -3. **First Task** — create your first task or import one from GitHub +2. **GitHub (Optional)** — connect GitHub for issue import and PR workflows. +3. **Project Setup** — choose how Fusion should prepare a repository: + - **Use Existing Directory** registers a folder that is already a git repository or a workspace root with detected sub-repositories. + - **Initialize New Repository** registers an existing local folder and lets the server run `git init` during registration when the folder is not already a git repository. + - **Clone Git Repository** runs `git clone` from a remote URL into an empty or absent destination directory, then registers the cloned folder. Fusion rejects blank clone URLs and populated destinations. +4. **First Task** — create your first task or import one from GitHub. The wizard is dismissible and non-blocking. You can skip it and continue using Fusion, then reopen it later from **Settings → Authentication**. diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts index e8e61006e9..e4e10fe98d 100644 --- a/packages/dashboard/app/api/legacy.ts +++ b/packages/dashboard/app/api/legacy.ts @@ -6981,6 +6981,7 @@ export interface ProjectCreateInput { path: string; isolationMode?: "in-process" | "child-process"; nodeId?: string; + gitSetupMode?: "existing" | "init" | "clone"; cloneUrl?: string; workspaceMode?: boolean; taskPrefix?: string; diff --git a/packages/dashboard/app/components/SetupWizardModal.css b/packages/dashboard/app/components/SetupWizardModal.css index 0dea1b3602..cb930cad92 100644 --- a/packages/dashboard/app/components/SetupWizardModal.css +++ b/packages/dashboard/app/components/SetupWizardModal.css @@ -137,17 +137,51 @@ margin: 0 0 var(--space-xl); } +.setup-wizard-repository-section { + display: flex; + flex-direction: column; + gap: var(--space-sm); + margin-bottom: var(--space-lg); + padding: var(--space-md); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--card); +} + +.setup-wizard-section-heading { + display: flex; + flex-direction: column; + gap: calc(var(--space-xs) / 2); +} + +.setup-wizard-section-heading h3, +.setup-wizard-section-heading p { + margin: 0; +} + +.setup-wizard-section-heading h3 { + color: var(--text); + font-size: var(--font-size-md); + font-weight: 700; +} + +.setup-wizard-section-heading p { + color: var(--text-muted); + font-size: var(--font-size-sm); + line-height: 1.45; +} + .setup-wizard-mode-switch { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-sm); - margin: 0 0 var(--space-lg); + margin: 0; padding: 0; border: 0; } .setup-wizard-mode-switch legend { - font-size: 13px; + font-size: var(--font-size-sm); font-weight: 600; color: var(--text); margin-bottom: var(--space-xs); @@ -155,7 +189,7 @@ .setup-wizard-mode-option { display: flex; - align-items: center; + align-items: flex-start; gap: var(--space-sm); padding: var(--space-sm) var(--space-md); border: 1px solid var(--border); @@ -170,9 +204,30 @@ } .setup-wizard-mode-option input { + flex: 0 0 auto; + margin-top: calc(var(--space-xs) / 2); accent-color: var(--todo); } +.setup-wizard-mode-option-copy { + display: flex; + flex-direction: column; + gap: calc(var(--space-xs) / 2); + min-width: 0; +} + +.setup-wizard-mode-option-copy strong { + color: var(--text); + font-size: var(--font-size-sm); + line-height: 1.3; +} + +.setup-wizard-mode-option-copy span { + color: var(--text-muted); + font-size: var(--font-size-xs); + line-height: 1.35; +} + .setup-wizard-mode-option:hover { border-color: var(--text-dim); } @@ -281,6 +336,12 @@ margin-bottom: 0; } +.setup-wizard-inline-spinner { + display: inline-block; + margin-right: var(--space-xs); + vertical-align: middle; +} + .setup-wizard-auth-token { display: flex; flex-direction: column; @@ -650,6 +711,14 @@ gap: var(--space-xs); } + .setup-wizard-mode-switch { + grid-template-columns: 1fr; + } + + .setup-wizard-repository-section { + padding: var(--space-sm); + } + .setup-wizard-agent-layout { grid-template-columns: 1fr; } diff --git a/packages/dashboard/app/components/SetupWizardModal.tsx b/packages/dashboard/app/components/SetupWizardModal.tsx index 89461dad2b..21df88e3f6 100644 --- a/packages/dashboard/app/components/SetupWizardModal.tsx +++ b/packages/dashboard/app/components/SetupWizardModal.tsx @@ -46,7 +46,7 @@ export interface SetupWizardModalProps { } type WizardStep = "manual" | "agent" | "complete"; -type ManualSetupMode = "existing" | "clone"; +type ManualSetupMode = "existing" | "init" | "clone"; type AgentOutcome = "created" | "skipped" | null; interface WizardState { @@ -189,6 +189,19 @@ export function SetupWizardModal({ } }, [state.manualMode]); + const handleSetupModeChange = useCallback((mode: ManualSetupMode) => { + setState((prev) => ({ + ...prev, + manualMode: mode, + manualCloneUrl: mode === "clone" ? prev.manualCloneUrl : "", + detectedRepos: mode === "existing" ? prev.detectedRepos : [], + workspaceMode: mode === "existing" ? prev.workspaceMode : false, + isDetectingWorkspace: false, + error: null, + })); + detectWorkspaceRequestId.current += 1; + }, []); + const handleManualRegister = useCallback(async () => { const trimmedPath = state.manualPath.trim(); const trimmedName = state.manualName.trim(); @@ -205,8 +218,9 @@ export function SetupWizardModal({ path: trimmedPath, isolationMode: state.manualIsolationMode, nodeId: state.manualNodeId || undefined, + gitSetupMode: state.manualMode, cloneUrl: state.manualMode === "clone" ? trimmedCloneUrl : undefined, - workspaceMode: state.workspaceMode, + workspaceMode: state.manualMode === "existing" ? state.workspaceMode : false, taskPrefix: state.manualTaskPrefix.trim() || undefined, }; @@ -314,7 +328,13 @@ export function SetupWizardModal({ if (!isOpen) return null; const isExistingMode = state.manualMode === "existing"; + const isInitMode = state.manualMode === "init"; const isCloneMode = state.manualMode === "clone"; + const setupModeSubmitLabel = isCloneMode + ? t("setup.cloneAndRegisterProject", "Clone and Register Project") + : isInitMode + ? t("setup.initializeAndRegisterProject", "Initialize and Register Project") + : t("setup.registerProject", "Register Project"); const hasPath = state.manualPath.trim().length > 0; const hasName = state.manualName.trim().length > 0; const hasCloneUrl = state.manualCloneUrl.trim().length > 0; @@ -390,6 +410,63 @@ export function SetupWizardModal({ {/* Manual Step */} {state.step === "manual" && (
+ {/* + FNXC:Onboarding 2026-07-02-14:30: + First-run project setup must make existing, init, and clone repository paths visible before path entry. + The selected mode tells users whether Fusion will register an existing git repo, run server-side `git init`, or `git clone` into an empty destination so non-git folders do not strand onboarding. + */} +
+
+

{t("setup.repositorySetupTitle", "Repository setup")}

+

{t("setup.repositorySetupDescription", "Choose how Fusion should prepare the project directory before registration.")}

+
+
+ {t("setup.setupMode", "Setup Mode")} + + + +
+
+
{isCloneMode ? t("setup.projectNameHintClone", "By default this follows the destination folder name unless you edit it.") - : t("setup.projectNameHintExisting", "By default this follows the selected directory name unless you edit it.")} + : isInitMode + ? t("setup.projectNameHintInit", "By default this follows the folder name Fusion will initialize unless you edit it.") + : t("setup.projectNameHintExisting", "By default this follows the selected directory name unless you edit it.")}

- +

{isCloneMode - ? t("setup.clonePathHint", "Select or type an absolute destination path. Fusion will clone into this directory.") - : t("setup.projectPathHint", "Select or type the absolute path to your project")} + ? t("setup.clonePathHint", "Select or type an absolute destination path. Fusion will clone into this directory. The destination must be empty or absent.") + : isInitMode + ? t("setup.initPathHint", "Select or type an absolute path to an empty or non-git folder. Fusion will run git init during registration.") + : t("setup.projectPathHint", "Select or type the absolute path to an existing git repository or workspace root.")}

+ {isCloneMode && ( +
+ + setState((prev) => ({ ...prev, manualCloneUrl: e.target.value, error: null }))} + placeholder={t("setup.repositoryUrlPlaceholder", "https://github.com/owner/repo.git")} + /> +

+ {t("setup.cloneGitHint", "Fusion will run git clone into the destination directory, then register that cloned folder.")} +

+
+ )} + {/* FNXC:Workspace 2026-06-24-19:00: Workspace mode detection: when the selected directory contains git sub-repos, @@ -443,7 +540,7 @@ export function SetupWizardModal({ {state.isDetectingWorkspace && (

- + {t("setup.detectingWorkspace", "Detecting sub-repositories...")}

)} @@ -494,50 +591,6 @@ export function SetupWizardModal({ {showAdvancedSettings && (
-
- {t("setup.setupMode", "Setup Mode")} - - -
- - {isCloneMode && ( -
- - setState((prev) => ({ ...prev, manualCloneUrl: e.target.value }))} - placeholder={t("setup.repositoryUrlPlaceholder", "https://github.com/owner/repo.git")} - /> -

- {t("setup.cloneGitHint", "Fusion will run git clone into the destination directory, then register that cloned folder.")} -

-
- )} -
{t("setup.runtimeNode", "Runtime Node")} @@ -742,7 +795,7 @@ export function SetupWizardModal({ {t("setup.registering", "Registering...")} ) : ( - {t("setup.registerProject", "Register Project")} + {setupModeSubmitLabel} )} )} diff --git a/packages/dashboard/app/components/__tests__/SetupWizardModal.test.tsx b/packages/dashboard/app/components/__tests__/SetupWizardModal.test.tsx index 5643b1bbc9..4912ee063b 100644 --- a/packages/dashboard/app/components/__tests__/SetupWizardModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/SetupWizardModal.test.tsx @@ -84,11 +84,12 @@ vi.mock("../ExperimentalAgentOnboardingModal", () => ({ ), })); -import { createAgent, registerProject } from "../../api"; +import { createAgent, registerProject, detectWorkspace } from "../../api"; import { useNodes } from "../../hooks/useNodes"; const mockRegisterProject = vi.mocked(registerProject); const mockCreateAgent = vi.mocked(createAgent); +const mockDetectWorkspace = vi.mocked(detectWorkspace); const mockUseNodes = vi.mocked(useNodes); function buildMockProject(overrides = {}) { @@ -133,6 +134,7 @@ describe("SetupWizardModal", () => { vi.clearAllMocks(); mockRegisterProject.mockReset(); mockCreateAgent.mockReset(); + mockDetectWorkspace.mockResolvedValue({ repos: [], isWorkspace: false }); }); it("starts on the project form without an auth token step", () => { @@ -146,6 +148,10 @@ describe("SetupWizardModal", () => { expect(screen.getByText("Welcome to Fusion")).toBeDefined(); expect(screen.getByText("Project Name")).toBeDefined(); expect(screen.getByLabelText("Fusion logo")).toBeDefined(); + expect(screen.getByText("Repository setup")).toBeDefined(); + expect(screen.getByLabelText("Use Existing Directory")).toBeDefined(); + expect(screen.getByLabelText("Initialize New Repository")).toBeDefined(); + expect(screen.getByLabelText("Clone Git Repository")).toBeDefined(); expect(screen.getByText("Advanced settings")).toBeDefined(); expect(screen.queryByText("Set Auth Token")).toBeNull(); expect(screen.queryByText("Set Token & Continue")).toBeNull(); @@ -250,6 +256,7 @@ describe("SetupWizardModal", () => { path: "/existing/project", isolationMode: "in-process", nodeId: undefined, + gitSetupMode: "existing", cloneUrl: undefined, workspaceMode: false, taskPrefix: "PROJ", @@ -257,7 +264,7 @@ describe("SetupWizardModal", () => { }); }); - it("clone mode renders repository url input and destination picker", () => { + it("renders first-class repository setup modes without opening advanced settings", () => { render( { /> ); - fireEvent.click(screen.getByText("Advanced settings")); + expect(screen.getByText("Repository setup")).toBeDefined(); + expect(screen.getByText(/Register a folder that is already a git repository/)).toBeDefined(); + expect(screen.getByText(/let Fusion run git init/)).toBeDefined(); + expect(screen.getByText(/Clone a remote repository/)).toBeDefined(); + expect(screen.queryByLabelText("Repository URL")).toBeNull(); + fireEvent.click(screen.getByLabelText("Clone Git Repository")); expect(screen.getByLabelText("Repository URL")).toBeDefined(); expect(screen.getByPlaceholderText("/path/for/new-clone")).toBeDefined(); + expect(screen.getByText(/destination must be empty or absent/)).toBeDefined(); expect(screen.getByText(/Fusion will run git clone into the destination directory/)).toBeDefined(); }); @@ -292,7 +305,6 @@ describe("SetupWizardModal", () => { /> ); - fireEvent.click(screen.getByText("Advanced settings")); fireEvent.click(screen.getByLabelText("Clone Git Repository")); fireEvent.change(screen.getByLabelText("Repository URL"), { target: { value: "https://github.com/runfusion/fusion.git" }, @@ -301,7 +313,7 @@ describe("SetupWizardModal", () => { target: { value: "/tmp/fusion" }, }); - fireEvent.click(screen.getByText("Register Project")); + fireEvent.click(screen.getByText("Clone and Register Project")); await waitFor(() => { /* @@ -313,6 +325,7 @@ describe("SetupWizardModal", () => { path: "/tmp/fusion", isolationMode: "in-process", nodeId: undefined, + gitSetupMode: "clone", cloneUrl: "https://github.com/runfusion/fusion.git", workspaceMode: false, taskPrefix: "FUSI", @@ -323,6 +336,88 @@ describe("SetupWizardModal", () => { expect(onProjectRegistered).not.toHaveBeenCalled(); }); + it("initialize mode submits an explicit init setup payload without clone or workspace state", async () => { + mockRegisterProject.mockResolvedValueOnce({ + id: "proj_init", + name: "new-project", + path: "/tmp/new-project", + status: "active", + isolationMode: "in-process", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + + render( + + ); + + fireEvent.click(screen.getByLabelText("Initialize New Repository")); + expect(screen.getByText(/Fusion will run git init during registration/)).toBeDefined(); + fireEvent.change(screen.getByPlaceholderText("/path/to/new-project"), { + target: { value: "/tmp/new-project" }, + }); + + fireEvent.click(screen.getByText("Initialize and Register Project")); + + await waitFor(() => { + expect(mockRegisterProject).toHaveBeenCalledWith({ + name: "new-project", + path: "/tmp/new-project", + isolationMode: "in-process", + nodeId: undefined, + gitSetupMode: "init", + cloneUrl: undefined, + workspaceMode: false, + taskPrefix: "NEWP", + }); + }); + }); + + it("switching setup modes clears stale clone and workspace state", async () => { + mockDetectWorkspace.mockResolvedValueOnce({ repos: ["api"], isWorkspace: true }); + mockRegisterProject.mockResolvedValueOnce({ + id: "proj_existing", + name: "workspace", + path: "/tmp/workspace", + status: "active", + isolationMode: "in-process", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + + render( + + ); + + fireEvent.change(screen.getByPlaceholderText("/path/to/your/project"), { + target: { value: "/tmp/workspace" }, + }); + expect(await screen.findByText(/Found 1 repositories/)).toBeDefined(); + + fireEvent.click(screen.getByLabelText("Clone Git Repository")); + fireEvent.change(screen.getByLabelText("Repository URL"), { + target: { value: "https://github.com/runfusion/fusion.git" }, + }); + fireEvent.click(screen.getByLabelText("Initialize New Repository")); + expect(screen.queryByLabelText("Repository URL")).toBeNull(); + + fireEvent.click(screen.getByText("Initialize and Register Project")); + + await waitFor(() => { + expect(mockRegisterProject).toHaveBeenCalledWith(expect.objectContaining({ + gitSetupMode: "init", + cloneUrl: undefined, + workspaceMode: false, + })); + }); + }); + it("register button disabled/enabled logic is mode-aware", () => { render( { const registerBtn = screen.getByText("Register Project").closest("button")!; expect(registerBtn.disabled).toBe(true); - fireEvent.click(screen.getByText("Advanced settings")); fireEvent.click(screen.getByLabelText("Clone Git Repository")); fireEvent.change(screen.getByPlaceholderText("/path/for/new-clone"), { target: { value: "/tmp/repo" }, @@ -358,7 +452,6 @@ describe("SetupWizardModal", () => { /> ); - fireEvent.click(screen.getByText("Advanced settings")); fireEvent.click(screen.getByLabelText("Clone Git Repository")); const nameInput = screen.getByPlaceholderText("my-project") as HTMLInputElement; diff --git a/packages/dashboard/src/__tests__/project-routes.test.ts b/packages/dashboard/src/__tests__/project-routes.test.ts index af9067d0bb..7139b79237 100644 --- a/packages/dashboard/src/__tests__/project-routes.test.ts +++ b/packages/dashboard/src/__tests__/project-routes.test.ts @@ -760,6 +760,57 @@ describe("POST /api/projects route handler", () => { expect((res.body as any).status).toBe("active"); }); + it("accepts explicit initialize mode for a non-git folder through ensureProjectForPath", async () => { + const store = new MockStoreForRoutes(); + const app = await createApp(store); + + const res = await request( + app, + "POST", + "/api/projects", + JSON.stringify({ + name: "New Git Project", + path: "/tmp/new-git-project", + gitSetupMode: "init", + }), + { "Content-Type": "application/json" }, + ); + + expect(res.status).toBe(201); + expect(mockFsAccess).toHaveBeenCalledWith("/tmp/new-git-project"); + expect(mockExecFileAsync).not.toHaveBeenCalled(); + expect(mockEnsureProjectForPath).toHaveBeenCalledWith({ + path: "/tmp/new-git-project", + identity: undefined, + name: "New Git Project", + isolationMode: "in-process", + nodeId: undefined, + }); + }); + + it("keeps existing registration on the git-init fallback path when no mode is provided", async () => { + const store = new MockStoreForRoutes(); + const app = await createApp(store); + + const res = await request( + app, + "POST", + "/api/projects", + JSON.stringify({ name: "Existing Or Init", path: "/tmp/existing-or-init" }), + { "Content-Type": "application/json" }, + ); + + expect(res.status).toBe(201); + expect(mockExecFileAsync).not.toHaveBeenCalled(); + expect(mockEnsureProjectForPath).toHaveBeenCalledWith({ + path: "/tmp/existing-or-init", + identity: undefined, + name: "Existing Or Init", + isolationMode: "in-process", + nodeId: undefined, + }); + }); + it("passes nodeId to ensureProjectForPath when provided", async () => { const store = new MockStoreForRoutes(); const app = await createApp(store); @@ -912,6 +963,7 @@ describe("POST /api/projects route handler", () => { JSON.stringify({ name: "Cloned Project", path: cloneDestination, + gitSetupMode: "clone", cloneUrl: bareRepo, }), { "Content-Type": "application/json" }, @@ -971,6 +1023,7 @@ describe("POST /api/projects route handler", () => { JSON.stringify({ name: "Existing Destination", path: "/tmp/existing-destination", + gitSetupMode: "clone", cloneUrl: "https://github.com/runfusion/fusion.git", }), { "Content-Type": "application/json" }, @@ -982,6 +1035,51 @@ describe("POST /api/projects route handler", () => { expect(mockEnsureProjectForPath).not.toHaveBeenCalled(); }); + it("rejects explicit clone mode when cloneUrl is missing", async () => { + const store = new MockStoreForRoutes(); + const app = await createApp(store); + + const res = await request( + app, + "POST", + "/api/projects", + JSON.stringify({ + name: "Missing Clone Url", + path: "/tmp/missing-clone-url", + gitSetupMode: "clone", + }), + { "Content-Type": "application/json" }, + ); + + expect(res.status).toBe(400); + expect((res.body as { error?: string }).error).toContain("cloneUrl must be a non-empty string"); + expect(mockExecFileAsync).not.toHaveBeenCalled(); + expect(mockEnsureProjectForPath).not.toHaveBeenCalled(); + }); + + it("rejects cloneUrl when explicit mode is initialize", async () => { + const store = new MockStoreForRoutes(); + const app = await createApp(store); + + const res = await request( + app, + "POST", + "/api/projects", + JSON.stringify({ + name: "Conflicting Clone Url", + path: "/tmp/conflicting-clone-url", + gitSetupMode: "init", + cloneUrl: "https://github.com/runfusion/fusion.git", + }), + { "Content-Type": "application/json" }, + ); + + expect(res.status).toBe(400); + expect((res.body as { error?: string }).error).toContain("cloneUrl can only be provided"); + expect(mockExecFileAsync).not.toHaveBeenCalled(); + expect(mockEnsureProjectForPath).not.toHaveBeenCalled(); + }); + it("rejects clone mode when cloneUrl is blank", async () => { const store = new MockStoreForRoutes(); const app = await createApp(store); diff --git a/packages/dashboard/src/routes/register-project-routes.ts b/packages/dashboard/src/routes/register-project-routes.ts index 08bd3f5dbe..fd6694353e 100644 --- a/packages/dashboard/src/routes/register-project-routes.ts +++ b/packages/dashboard/src/routes/register-project-routes.ts @@ -252,6 +252,7 @@ export const registerProjectRoutes: ApiRouteRegistrar = (ctx) => { * path: string, * isolationMode?: "in-process" | "child-process", * nodeId?: string, + * gitSetupMode?: "existing" | "init" | "clone", * cloneUrl?: string, * workspaceMode?: boolean, * taskPrefix?: string @@ -260,7 +261,7 @@ export const registerProjectRoutes: ApiRouteRegistrar = (ctx) => { */ router.post("/projects", async (req, res) => { try { - const { name, path, isolationMode = "in-process", nodeId, cloneUrl, workspaceMode, taskPrefix } = req.body; + const { name, path, isolationMode = "in-process", nodeId, cloneUrl, gitSetupMode, workspaceMode, taskPrefix } = req.body; if (!name || typeof name !== "string" || !name.trim()) { throw badRequest("name is required and must be a non-empty string"); @@ -275,6 +276,14 @@ export const registerProjectRoutes: ApiRouteRegistrar = (ctx) => { const normalizedName = name.trim(); const normalizedPath = path.trim(); let normalizedCloneUrl: string | undefined; + let normalizedGitSetupMode: "existing" | "init" | "clone" | undefined; + + if (gitSetupMode !== undefined) { + if (!["existing", "init", "clone"].includes(gitSetupMode)) { + throw badRequest("gitSetupMode must be 'existing', 'init', or 'clone'"); + } + normalizedGitSetupMode = gitSetupMode; + } if (normalizedPath.includes("\0")) { throw badRequest("path cannot contain null bytes"); @@ -299,7 +308,19 @@ export const registerProjectRoutes: ApiRouteRegistrar = (ctx) => { normalizedCloneUrl = trimmedCloneUrl; } - const isCloneMode = normalizedCloneUrl !== undefined; + if (normalizedCloneUrl !== undefined && normalizedGitSetupMode !== undefined && normalizedGitSetupMode !== "clone") { + throw badRequest("cloneUrl can only be provided when gitSetupMode is 'clone'"); + } + if (normalizedGitSetupMode === "clone" && normalizedCloneUrl === undefined) { + throw badRequest("cloneUrl must be a non-empty string when gitSetupMode is 'clone'"); + } + + /* + FNXC:Onboarding 2026-07-02-14:48: + Dashboard onboarding now sends an explicit git setup mode, but existing clients may still only send cloneUrl. + Preserve the legacy cloneUrl trigger while treating existing/init as the CentralCore ensureProjectForPath path where non-git directories are initialized by ensureGitRepositoryForProjectPath. + */ + const isCloneMode = normalizedGitSetupMode === "clone" || normalizedCloneUrl !== undefined; let destinationCreatedForClone = false; if (!isCloneMode) { diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index 00256e87dd..b810bd31a7 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -6643,7 +6643,7 @@ }, "cloneGitHint": "Fusion will run git clone into the destination directory, then register that cloned folder.", "cloneGitRepository": "Clone Git Repository", - "clonePathHint": "Select or type an absolute destination path. Fusion will clone into this directory.", + "clonePathHint": "Select or type an absolute destination path. Fusion will clone into this directory. The destination must be empty or absent.", "clonePathPlaceholder": "/path/for/new-clone", "closeWizard": "Close wizard", "compactWarning": "⚠ Setup incomplete — AI and/or GitHub features will be limited.", @@ -6797,7 +6797,7 @@ "projectNameHintClone": "By default this follows the destination folder name unless you edit it.", "projectNameHintExisting": "By default this follows the selected directory name unless you edit it.", "projectNamePlaceholder": "my-project", - "projectPathHint": "Select or type the absolute path to your project", + "projectPathHint": "Select or type the absolute path to an existing git repository or workspace root.", "projectPathPlaceholder": "/path/to/your/project", "projectRegisteredSuccess": "Your project has been registered successfully.", "projectRequired": "A project is required before first-task actions are available.", @@ -6855,7 +6855,7 @@ "setupComplete": "Setup complete! Head to the board to create your first task, or explore the dashboard to see what's available.", "setupMode": "Setup Mode", "setUpProject": "Set Up Project", - "setupWizardHint": "In the setup wizard, pick an existing directory or paste a GitHub clone URL.", + "setupWizardHint": "In the setup wizard, register an existing git repository, initialize a selected folder, or clone a remote repository into an empty destination.", "skip": "Skip", "skipFirstAgent": "Skip for now", "skipForNow": "Skip for now", @@ -6897,7 +6897,7 @@ "whatDoesGitHubIntegrationDo": "What does GitHub integration do?", "whatDoesGitHubIntegrationDoBody": "Without GitHub, you can still create and manage tasks manually. GitHub integration adds the ability to import issues as tasks, track pull request status alongside your work, and automatically link commits to tasks. Connect anytime from Settings → Authentication.", "whatDoesProjectSetupDo": "What does project setup do?", - "whatDoesProjectSetupDoBody": "Project setup registers a workspace so Fusion knows where to read files, run commands, and track task changes.", + "whatDoesProjectSetupDoBody": "Project setup registers a workspace so Fusion knows where to read files, run commands, and track task changes. You can use an existing git repository, initialize git in a selected local folder, or clone a remote repository into an empty destination.", "whatHappensWhenCreateTask": "What happens when I create a task?", "whatHappensWhenCreateTaskBody": "Describe the work you want done. You can create tasks without an agent: Fusion starts temporary agents to plan, code, review, and merge. Track everything on the board.", "whatIsApiKey": "What is an API key?", @@ -6912,7 +6912,20 @@ "withoutGitHubHeading": "Without GitHub (available now):", "brandLogo": "Fusion logo", "brandName": "Fusion", - "setupCompleteTitle": "Setup Complete!" + "setupCompleteTitle": "Setup Complete!", + "cloneAndRegisterProject": "Clone and Register Project", + "cloneGitRepositoryHint": "Clone a remote repository into an empty or absent destination folder.", + "initializeAndRegisterProject": "Initialize and Register Project", + "initializeNewRepository": "Initialize New Repository", + "initializeNewRepositoryHint": "Register a selected non-git folder and let Fusion run git init on the server.", + "initPathHint": "Select or type an absolute path to an empty or non-git folder. Fusion will run git init during registration.", + "initPathPlaceholder": "/path/to/new-project", + "newRepositoryDirectory": "New Repository Directory", + "projectNameHintInit": "By default this follows the folder name Fusion will initialize unless you edit it.", + "projectSetupModeAriaLabel": "Project setup mode", + "repositorySetupDescription": "Choose how Fusion should prepare the project directory before registration.", + "repositorySetupTitle": "Repository setup", + "useExistingDirectoryHint": "Register a folder that is already a git repository or workspace root." }, "shell": { "activePill": "Active", diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json index 8ca36b87bd..48b29297bf 100644 --- a/packages/i18n/locales/es/app.json +++ b/packages/i18n/locales/es/app.json @@ -6631,9 +6631,9 @@ "useDashP": "", "validating": "Activado. Validando…" }, - "cloneGitHint": "Fusion ejecutará git clone en el directorio de destino y luego registrará esa carpeta clonada.", + "cloneGitHint": "Fusion will run git clone into the destination directory, then register that cloned folder.", "cloneGitRepository": "Clonar repositorio de Git", - "clonePathHint": "Seleccione o escriba una ruta de destino absoluta. Fusion clonará en este directorio.", + "clonePathHint": "Select or type an absolute destination path. Fusion will clone into this directory. The destination must be empty or absent.", "clonePathPlaceholder": "/ruta/para/nuevo-clon", "closeWizard": "Cerrar asistente", "compactWarning": "⚠ Configuración incompleta — Las funciones de IA y/o GitHub serán limitadas.", @@ -6787,7 +6787,7 @@ "projectNameHintClone": "De forma predeterminada, esto sigue el nombre de la carpeta de destino a menos que lo edite.", "projectNameHintExisting": "De forma predeterminada, esto sigue el nombre del directorio seleccionado a menos que lo edite.", "projectNamePlaceholder": "mi-proyecto", - "projectPathHint": "Seleccione o escriba la ruta absoluta de su proyecto", + "projectPathHint": "Select or type the absolute path to an existing git repository or workspace root.", "projectPathPlaceholder": "/ruta/a/tu/proyecto", "projectRegisteredSuccess": "Su proyecto se ha registrado exitosamente.", "projectRequired": "Se requiere un proyecto antes de que las acciones de primera tarea estén disponibles.", @@ -6845,7 +6845,7 @@ "setupComplete": "¡Configuración completada!", "setupMode": "Modo de configuración", "setUpProject": "Configurar proyecto", - "setupWizardHint": "En el asistente de configuración, elige un directorio existente o pega una URL de clon de GitHub.", + "setupWizardHint": "In the setup wizard, register an existing git repository, initialize a selected folder, or clone a remote repository into an empty destination.", "skip": "Omitir", "skipFirstAgent": "", "skipForNow": "Omitir por ahora", @@ -6887,7 +6887,7 @@ "whatDoesGitHubIntegrationDo": "¿Qué hace la integración con GitHub?", "whatDoesGitHubIntegrationDoBody": "Sin GitHub, aún puedes crear y gestionar tareas manualmente. La integración con GitHub agrega la capacidad de importar issues como tareas, rastrear el estado de pull requests y vincular automáticamente commits a tareas. Conéctate en cualquier momento desde Ajustes → Autenticación.", "whatDoesProjectSetupDo": "¿Qué hace la configuración del proyecto?", - "whatDoesProjectSetupDoBody": "La configuración del proyecto registra un espacio de trabajo para que Fusion sepa dónde leer archivos, ejecutar comandos y rastrear cambios de tareas.", + "whatDoesProjectSetupDoBody": "Project setup registers a workspace so Fusion knows where to read files, run commands, and track task changes. You can use an existing git repository, initialize git in a selected local folder, or clone a remote repository into an empty destination.", "whatHappensWhenCreateTask": "¿Qué ocurre cuando creo una tarea?", "whatHappensWhenCreateTaskBody": "Una tarea describe algo que quieres que se haga. Los agentes de IA de Fusion leerán tu descripción y trabajarán en implementarla. Puedes seguir el progreso en el tablero y revisar los resultados.", "whatIsApiKey": "¿Qué es una clave API?", @@ -6902,7 +6902,20 @@ "withoutGitHubHeading": "Sin GitHub (disponible ahora):", "brandLogo": "", "brandName": "", - "setupCompleteTitle": "" + "setupCompleteTitle": "", + "cloneAndRegisterProject": "Clone and Register Project", + "cloneGitRepositoryHint": "Clone a remote repository into an empty or absent destination folder.", + "initializeAndRegisterProject": "Initialize and Register Project", + "initializeNewRepository": "Initialize New Repository", + "initializeNewRepositoryHint": "Register a selected non-git folder and let Fusion run git init on the server.", + "initPathHint": "Select or type an absolute path to an empty or non-git folder. Fusion will run git init during registration.", + "initPathPlaceholder": "/path/to/new-project", + "newRepositoryDirectory": "New Repository Directory", + "projectNameHintInit": "By default this follows the folder name Fusion will initialize unless you edit it.", + "projectSetupModeAriaLabel": "Project setup mode", + "repositorySetupDescription": "Choose how Fusion should prepare the project directory before registration.", + "repositorySetupTitle": "Repository setup", + "useExistingDirectoryHint": "Register a folder that is already a git repository or workspace root." }, "shell": { "activePill": "Activo", diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json index 385732457f..0a1410ffc9 100644 --- a/packages/i18n/locales/fr/app.json +++ b/packages/i18n/locales/fr/app.json @@ -6631,9 +6631,9 @@ "useDashP": "", "validating": "Activé. Validation…" }, - "cloneGitHint": "Fusion exécutera git clone dans le répertoire de destination, puis enregistrera ce dossier cloné.", + "cloneGitHint": "Fusion will run git clone into the destination directory, then register that cloned folder.", "cloneGitRepository": "Cloner le référentiel Git", - "clonePathHint": "Sélectionnez ou entrez un chemin de destination absolu. Fusion clotera dans ce répertoire.", + "clonePathHint": "Select or type an absolute destination path. Fusion will clone into this directory. The destination must be empty or absent.", "clonePathPlaceholder": "/chemin/pour/nouveau-clone", "closeWizard": "Fermer l'assistant", "compactWarning": "⚠ Configuration incomplète — Les fonctionnalités IA et/ou GitHub seront limitées.", @@ -6787,7 +6787,7 @@ "projectNameHintClone": "Par défaut, cela suit le nom du dossier de destination sauf si vous l'éditez.", "projectNameHintExisting": "Par défaut, cela suit le nom du répertoire sélectionné sauf si vous l'éditez.", "projectNamePlaceholder": "mon-projet", - "projectPathHint": "Sélectionnez ou entrez le chemin absolu de votre projet", + "projectPathHint": "Select or type the absolute path to an existing git repository or workspace root.", "projectPathPlaceholder": "/chemin/vers/votre/projet", "projectRegisteredSuccess": "Votre projet a été enregistré avec succès.", "projectRequired": "Un projet est requis avant que les actions de première tâche soient disponibles.", @@ -6845,7 +6845,7 @@ "setupComplete": "Configuration terminée !", "setupMode": "Mode de configuration", "setUpProject": "Configurer le projet", - "setupWizardHint": "Dans l'assistant de configuration, choisissez un répertoire existant ou collez une URL de clone GitHub.", + "setupWizardHint": "In the setup wizard, register an existing git repository, initialize a selected folder, or clone a remote repository into an empty destination.", "skip": "Sauter", "skipFirstAgent": "", "skipForNow": "Ignorer pour l'instant", @@ -6887,7 +6887,7 @@ "whatDoesGitHubIntegrationDo": "À quoi sert l'intégration GitHub ?", "whatDoesGitHubIntegrationDoBody": "Sans GitHub, vous pouvez toujours créer et gérer des tâches manuellement. L'intégration GitHub ajoute la possibilité d'importer des tickets comme tâches, de suivre le statut des pull requests et de lier automatiquement les commits aux tâches. Connectez-vous à tout moment depuis Paramètres → Authentification.", "whatDoesProjectSetupDo": "À quoi sert la configuration du projet ?", - "whatDoesProjectSetupDoBody": "La configuration du projet enregistre un espace de travail pour que Fusion sache où lire les fichiers, exécuter les commandes et suivre les modifications de tâches.", + "whatDoesProjectSetupDoBody": "Project setup registers a workspace so Fusion knows where to read files, run commands, and track task changes. You can use an existing git repository, initialize git in a selected local folder, or clone a remote repository into an empty destination.", "whatHappensWhenCreateTask": "Que se passe-t-il quand je crée une tâche ?", "whatHappensWhenCreateTaskBody": "Une tâche décrit ce que vous voulez réaliser. Les agents IA de Fusion liront votre description et travailleront à sa mise en œuvre. Vous pouvez suivre la progression sur le tableau et examiner les résultats.", "whatIsApiKey": "Qu'est-ce qu'une clé API ?", @@ -6902,7 +6902,20 @@ "withoutGitHubHeading": "Sans GitHub (disponible maintenant) :", "brandLogo": "", "brandName": "", - "setupCompleteTitle": "" + "setupCompleteTitle": "", + "cloneAndRegisterProject": "Clone and Register Project", + "cloneGitRepositoryHint": "Clone a remote repository into an empty or absent destination folder.", + "initializeAndRegisterProject": "Initialize and Register Project", + "initializeNewRepository": "Initialize New Repository", + "initializeNewRepositoryHint": "Register a selected non-git folder and let Fusion run git init on the server.", + "initPathHint": "Select or type an absolute path to an empty or non-git folder. Fusion will run git init during registration.", + "initPathPlaceholder": "/path/to/new-project", + "newRepositoryDirectory": "New Repository Directory", + "projectNameHintInit": "By default this follows the folder name Fusion will initialize unless you edit it.", + "projectSetupModeAriaLabel": "Project setup mode", + "repositorySetupDescription": "Choose how Fusion should prepare the project directory before registration.", + "repositorySetupTitle": "Repository setup", + "useExistingDirectoryHint": "Register a folder that is already a git repository or workspace root." }, "shell": { "activePill": "Actif", diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json index bff751be7a..2e07f1e3df 100644 --- a/packages/i18n/locales/ko/app.json +++ b/packages/i18n/locales/ko/app.json @@ -6631,9 +6631,9 @@ "useDashP": "", "validating": "활성화됨. 유효성 검사 중…" }, - "cloneGitHint": "Fusion이 대상 디렉터리로 git clone을 실행한 후 해당 폴더를 등록합니다.", + "cloneGitHint": "Fusion will run git clone into the destination directory, then register that cloned folder.", "cloneGitRepository": "Git 저장소 복제", - "clonePathHint": "절대 경로를 선택하거나 입력하세요. Fusion이 이 디렉터리로 복제합니다.", + "clonePathHint": "Select or type an absolute destination path. Fusion will clone into this directory. The destination must be empty or absent.", "clonePathPlaceholder": "/path/for/new-clone", "closeWizard": "마법사 닫기", "compactWarning": "⚠ 설정 미완료 — AI 및/또는 GitHub 기능이 제한됩니다.", @@ -6787,7 +6787,7 @@ "projectNameHintClone": "기본적으로 대상 폴더 이름을 따르며, 직접 편집할 수 있습니다.", "projectNameHintExisting": "기본적으로 선택된 디렉터리 이름을 따르며, 직접 편집할 수 있습니다.", "projectNamePlaceholder": "my-project", - "projectPathHint": "프로젝트의 절대 경로를 선택하거나 입력하세요", + "projectPathHint": "Select or type the absolute path to an existing git repository or workspace root.", "projectPathPlaceholder": "/path/to/your/project", "projectRegisteredSuccess": "프로젝트가 성공적으로 등록되었습니다.", "projectRequired": "첫 번째 작업 동작을 사용하려면 프로젝트가 필요합니다.", @@ -6845,7 +6845,7 @@ "setupComplete": "설정 완료! 보드로 이동하여 첫 번째 작업을 만들거나, 대시보드를 탐색해 보세요.", "setupMode": "설정 모드", "setUpProject": "프로젝트 설정", - "setupWizardHint": "설정 마법사에서 기존 디렉터리를 선택하거나 GitHub 클론 URL을 붙여넣으세요.", + "setupWizardHint": "In the setup wizard, register an existing git repository, initialize a selected folder, or clone a remote repository into an empty destination.", "skip": "건너뛰기", "skipFirstAgent": "", "skipForNow": "지금은 건너뛰기", @@ -6887,7 +6887,7 @@ "whatDoesGitHubIntegrationDo": "GitHub 통합은 무엇을 하나요?", "whatDoesGitHubIntegrationDoBody": "GitHub 없이도 작업을 수동으로 생성하고 관리할 수 있습니다. GitHub 통합을 사용하면 이슈를 작업으로 가져오고, 작업과 함께 풀 리퀘스트 상태를 추적하며, 커밋을 작업에 자동으로 연결할 수 있습니다. 설정 → 인증에서 언제든지 연결하세요.", "whatDoesProjectSetupDo": "프로젝트 설정은 무엇을 하나요?", - "whatDoesProjectSetupDoBody": "프로젝트 설정은 Fusion이 파일을 읽고, 명령을 실행하며, 작업 변경 사항을 추적할 수 있도록 작업 공간을 등록합니다.", + "whatDoesProjectSetupDoBody": "Project setup registers a workspace so Fusion knows where to read files, run commands, and track task changes. You can use an existing git repository, initialize git in a selected local folder, or clone a remote repository into an empty destination.", "whatHappensWhenCreateTask": "작업을 만들면 어떻게 되나요?", "whatHappensWhenCreateTaskBody": "작업은 원하는 내용을 설명합니다. Fusion의 AI 에이전트가 설명을 읽고 구현을 진행합니다. 보드에서 진행 상황을 추적하고 결과를 검토할 수 있습니다.", "whatIsApiKey": "API 키란 무엇인가요?", @@ -6902,7 +6902,20 @@ "withoutGitHubHeading": "GitHub 없이 (지금 사용 가능):", "brandLogo": "", "brandName": "", - "setupCompleteTitle": "" + "setupCompleteTitle": "", + "cloneAndRegisterProject": "Clone and Register Project", + "cloneGitRepositoryHint": "Clone a remote repository into an empty or absent destination folder.", + "initializeAndRegisterProject": "Initialize and Register Project", + "initializeNewRepository": "Initialize New Repository", + "initializeNewRepositoryHint": "Register a selected non-git folder and let Fusion run git init on the server.", + "initPathHint": "Select or type an absolute path to an empty or non-git folder. Fusion will run git init during registration.", + "initPathPlaceholder": "/path/to/new-project", + "newRepositoryDirectory": "New Repository Directory", + "projectNameHintInit": "By default this follows the folder name Fusion will initialize unless you edit it.", + "projectSetupModeAriaLabel": "Project setup mode", + "repositorySetupDescription": "Choose how Fusion should prepare the project directory before registration.", + "repositorySetupTitle": "Repository setup", + "useExistingDirectoryHint": "Register a folder that is already a git repository or workspace root." }, "shell": { "activePill": "활성", diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json index 8af107f3f9..d3fd442234 100644 --- a/packages/i18n/locales/zh-CN/app.json +++ b/packages/i18n/locales/zh-CN/app.json @@ -6631,9 +6631,9 @@ "useDashP": "", "validating": "已启用。验证中…" }, - "cloneGitHint": "Fusion 将运行 git clone 到目标目录,然后注册该克隆的文件夹。", + "cloneGitHint": "Fusion will run git clone into the destination directory, then register that cloned folder.", "cloneGitRepository": "克隆 Git 存储库", - "clonePathHint": "选择或输入绝对目标路径。Fusion 将克隆到此目录。", + "clonePathHint": "Select or type an absolute destination path. Fusion will clone into this directory. The destination must be empty or absent.", "clonePathPlaceholder": "/path/for/new-clone", "closeWizard": "关闭向导", "compactWarning": "⚠ 设置未完成 — AI 和/或 GitHub 功能将受限。", @@ -6787,7 +6787,7 @@ "projectNameHintClone": "默认情况下,这遵循目标文件夹名称,除非您编辑它。", "projectNameHintExisting": "默认情况下,这遵循选定的目录名称,除非您编辑它。", "projectNamePlaceholder": "my-project", - "projectPathHint": "选择或输入您的项目的绝对路径", + "projectPathHint": "Select or type the absolute path to an existing git repository or workspace root.", "projectPathPlaceholder": "/path/to/your/project", "projectRegisteredSuccess": "您的项目已成功注册。", "projectRequired": "在可以使用第一个任务操作之前,需要先有一个项目。", @@ -6845,7 +6845,7 @@ "setupComplete": "设置完成!", "setupMode": "设置模式", "setUpProject": "设置项目", - "setupWizardHint": "在设置向导中,选择现有目录或粘贴 GitHub 克隆 URL。", + "setupWizardHint": "In the setup wizard, register an existing git repository, initialize a selected folder, or clone a remote repository into an empty destination.", "skip": "跳过", "skipFirstAgent": "", "skipForNow": "暂时跳过", @@ -6887,7 +6887,7 @@ "whatDoesGitHubIntegrationDo": "GitHub 集成有什么用?", "whatDoesGitHubIntegrationDoBody": "没有 GitHub,您仍然可以手动创建和管理任务。GitHub 集成增加了将问题导入为任务、跟踪拉取请求状态以及自动将提交关联到任务的功能。随时可以从设置 → 身份验证进行连接。", "whatDoesProjectSetupDo": "项目设置有什么用?", - "whatDoesProjectSetupDoBody": "项目设置注册一个工作区,让 Fusion 知道在哪里读取文件、运行命令和跟踪任务变更。", + "whatDoesProjectSetupDoBody": "Project setup registers a workspace so Fusion knows where to read files, run commands, and track task changes. You can use an existing git repository, initialize git in a selected local folder, or clone a remote repository into an empty destination.", "whatHappensWhenCreateTask": "创建任务时会发生什么?", "whatHappensWhenCreateTaskBody": "任务描述了您希望完成的事情。Fusion 的 AI 代理将读取您的描述并着手实现。您可以在看板上跟踪进度并查看结果。", "whatIsApiKey": "什么是 API 密钥?", @@ -6902,7 +6902,20 @@ "withoutGitHubHeading": "不使用 GitHub(现在可用):", "brandLogo": "", "brandName": "", - "setupCompleteTitle": "" + "setupCompleteTitle": "", + "cloneAndRegisterProject": "Clone and Register Project", + "cloneGitRepositoryHint": "Clone a remote repository into an empty or absent destination folder.", + "initializeAndRegisterProject": "Initialize and Register Project", + "initializeNewRepository": "Initialize New Repository", + "initializeNewRepositoryHint": "Register a selected non-git folder and let Fusion run git init on the server.", + "initPathHint": "Select or type an absolute path to an empty or non-git folder. Fusion will run git init during registration.", + "initPathPlaceholder": "/path/to/new-project", + "newRepositoryDirectory": "New Repository Directory", + "projectNameHintInit": "By default this follows the folder name Fusion will initialize unless you edit it.", + "projectSetupModeAriaLabel": "Project setup mode", + "repositorySetupDescription": "Choose how Fusion should prepare the project directory before registration.", + "repositorySetupTitle": "Repository setup", + "useExistingDirectoryHint": "Register a folder that is already a git repository or workspace root." }, "shell": { "activePill": "活跃", diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json index 0fae8aa271..b2052bbcbd 100644 --- a/packages/i18n/locales/zh-TW/app.json +++ b/packages/i18n/locales/zh-TW/app.json @@ -6631,9 +6631,9 @@ "useDashP": "", "validating": "已啟用。驗證中…" }, - "cloneGitHint": "Fusion 將執行 git clone 到目的地目錄,然後註冊該克隆的資料夾。", + "cloneGitHint": "Fusion will run git clone into the destination directory, then register that cloned folder.", "cloneGitRepository": "克隆 Git 存儲庫", - "clonePathHint": "選擇或輸入絕對目的地路徑。Fusion 將克隆到此目錄。", + "clonePathHint": "Select or type an absolute destination path. Fusion will clone into this directory. The destination must be empty or absent.", "clonePathPlaceholder": "/path/for/new-clone", "closeWizard": "關閉精靈", "compactWarning": "⚠ 設定未完成 — AI 和/或 GitHub 功能將受限。", @@ -6787,7 +6787,7 @@ "projectNameHintClone": "預設情況下,除非您編輯,否則這遵循目的地資料夾名稱。", "projectNameHintExisting": "預設情況下,除非您編輯,否則這遵循選定的目錄名稱。", "projectNamePlaceholder": "my-project", - "projectPathHint": "選擇或輸入您的專案的絕對路徑", + "projectPathHint": "Select or type the absolute path to an existing git repository or workspace root.", "projectPathPlaceholder": "/path/to/your/project", "projectRegisteredSuccess": "您的專案已成功註冊。", "projectRequired": "在可以使用第一個任務操作之前,需要先有一個專案。", @@ -6845,7 +6845,7 @@ "setupComplete": "設定完成!", "setupMode": "設定模式", "setUpProject": "設定專案", - "setupWizardHint": "在設定精靈中,選擇現有目錄或貼上 GitHub 複製 URL。", + "setupWizardHint": "In the setup wizard, register an existing git repository, initialize a selected folder, or clone a remote repository into an empty destination.", "skip": "跳過", "skipFirstAgent": "", "skipForNow": "暫時略過", @@ -6887,7 +6887,7 @@ "whatDoesGitHubIntegrationDo": "GitHub 整合有什麼用途?", "whatDoesGitHubIntegrationDoBody": "沒有 GitHub,您仍然可以手動建立和管理任務。GitHub 整合增加了將問題匯入為任務、追蹤拉取請求狀態以及自動將提交連結到任務的功能。隨時可以從設定 → 驗證進行連接。", "whatDoesProjectSetupDo": "專案設定有什麼用途?", - "whatDoesProjectSetupDoBody": "專案設定會註冊一個工作區,讓 Fusion 知道在哪裡讀取檔案、執行命令和追蹤任務變更。", + "whatDoesProjectSetupDoBody": "Project setup registers a workspace so Fusion knows where to read files, run commands, and track task changes. You can use an existing git repository, initialize git in a selected local folder, or clone a remote repository into an empty destination.", "whatHappensWhenCreateTask": "建立任務時會發生什麼?", "whatHappensWhenCreateTaskBody": "任務描述了您希望完成的事情。Fusion 的 AI 代理將讀取您的描述並著手實現。您可以在看板上追蹤進度並查看結果。", "whatIsApiKey": "什麼是 API 金鑰?", @@ -6902,7 +6902,20 @@ "withoutGitHubHeading": "不使用 GitHub(現在可用):", "brandLogo": "", "brandName": "", - "setupCompleteTitle": "" + "setupCompleteTitle": "", + "cloneAndRegisterProject": "Clone and Register Project", + "cloneGitRepositoryHint": "Clone a remote repository into an empty or absent destination folder.", + "initializeAndRegisterProject": "Initialize and Register Project", + "initializeNewRepository": "Initialize New Repository", + "initializeNewRepositoryHint": "Register a selected non-git folder and let Fusion run git init on the server.", + "initPathHint": "Select or type an absolute path to an empty or non-git folder. Fusion will run git init during registration.", + "initPathPlaceholder": "/path/to/new-project", + "newRepositoryDirectory": "New Repository Directory", + "projectNameHintInit": "By default this follows the folder name Fusion will initialize unless you edit it.", + "projectSetupModeAriaLabel": "Project setup mode", + "repositorySetupDescription": "Choose how Fusion should prepare the project directory before registration.", + "repositorySetupTitle": "Repository setup", + "useExistingDirectoryHint": "Register a folder that is already a git repository or workspace root." }, "shell": { "activePill": "作用中",