fix(workspace): detect sub-repos when workspace.json is missing (#1739)
## Problem The previous fix (#1738) only checked `loadWorkspaceConfig` to skip `git init` on workspace-mode roots. However, the dashboard `POST /api/projects` and `fn project add` routes never create `workspace.json` (only the CLI interactive setup in `registerProjectInteractive` does). So re-adding a workspace project through the dashboard still triggered `git init` because the guard saw no `workspace.json`, creating a stray `.git` at the workspace root. ## Fix Add `detectWorkspaceRepos` as a fallback in `ensureGitRepositoryForProjectPath`: after `loadWorkspaceConfig` and `isInsideGitWorkTree` both miss, probe for git sub-repos. If found, persist `workspace.json` and return `"existing"` without running `git init`. This covers all registration surfaces. ## Files Changed - `packages/core/src/git-repository.ts` — `detectWorkspaceRepos` fallback + `saveWorkspaceConfig` + FNXC comment - `packages/core/src/__tests__/git-repository.test.ts` — regression test verifying sub-repo detection skips `git init` and persists `workspace.json` ## Testing - `pnpm typecheck` (`@fusion/core`) — pass - `pnpm lint` — pass - `vitest run git-repository.test.ts` — 6/6 pass (5 existing + 1 new) <!-- stage-review-badge-begin --> --- <a href="https://stagereview.app/Runfusion/Fusion/pull/1739"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://stagereview.app/assets/gh-open-in-stage-dark.svg"> <img src="https://stagereview.app/assets/gh-open-in-stage-light.svg" alt="Open in Stage"> </picture> </a> <!-- stage-review-badge-end --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a “Workspace mode (multi-repo)” checkbox in the dashboard to enable/disable workspace behavior. * Updated CLI interactive mode to prompt for using workspace mode; non-interactive mode continues to auto-detect sub-repositories. * **Bug Fixes** * Improved workspace auto-detection when workspace config is missing, including persisting detected repositories. * Workspace mode now skips creating a root `.git` when sub-repositories are detected. * When workspace mode is explicitly disabled, auto-detection is skipped; disabling also removes the workspace config. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -645,8 +645,27 @@ export async function registerProjectInteractive(
|
||||
if (subRepos.length > 0) {
|
||||
console.log(`\n Found ${subRepos.length} git repositories in ${absPath}:`);
|
||||
subRepos.forEach((r: string) => console.log(` • ${r}`));
|
||||
console.log(`\n Initializing as a Fusion workspace...\n`);
|
||||
detectedSubRepos = subRepos;
|
||||
|
||||
if (interactive) {
|
||||
/*
|
||||
FNXC:Workspace 2026-06-24-16:00:
|
||||
Ask the user to confirm workspace mode instead of auto-applying it. A directory with
|
||||
nested git repos might be a monorepo with submodules or an existing project that
|
||||
happens to have git-tracked dependencies — the user must explicitly opt in.
|
||||
*/
|
||||
const useWorkspace = await promptConfirm(
|
||||
`\n Use workspace mode? (tasks run per sub-repo, no git at the root)`,
|
||||
true,
|
||||
);
|
||||
if (useWorkspace) {
|
||||
detectedSubRepos = subRepos;
|
||||
} else {
|
||||
console.log(` ⚠ Skipping workspace mode. A git repo will be initialized at the root.`);
|
||||
}
|
||||
} else {
|
||||
// Non-interactive: auto-apply (dashboard registration or scripted flow)
|
||||
detectedSubRepos = subRepos;
|
||||
}
|
||||
// workspace.json is written below, only after a confirmed store.init() succeeds.
|
||||
}
|
||||
// else: fall through to existing error path
|
||||
@@ -663,6 +682,8 @@ export async function registerProjectInteractive(
|
||||
await store.init();
|
||||
if (detectedSubRepos) {
|
||||
await saveWorkspaceConfig(absPath, { repos: detectedSubRepos });
|
||||
// Persist workspaceMode in config.json so it's visible/toggleable in the dashboard
|
||||
await store.updateSettings({ workspaceMode: true });
|
||||
}
|
||||
console.log(` ✓ Initialized fn at ${absPath}`);
|
||||
} else {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { execFile } from "node:child_process";
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import {
|
||||
ensureGitRepositoryForProjectPath,
|
||||
GitRepositoryInitializationError,
|
||||
detectWorkspaceRepos,
|
||||
type GitRepositoryCommandRunner,
|
||||
} from "../git-repository.js";
|
||||
|
||||
@@ -120,4 +121,83 @@ describe("ensureGitRepositoryForProjectPath", () => {
|
||||
// No .git should be created at the workspace root
|
||||
expect(existsSync(join(projectPath, ".git"))).toBe(false);
|
||||
});
|
||||
|
||||
it("detects workspace sub-repos and skips git init when workspace.json is missing", async () => {
|
||||
const projectPath = tempDir("fusion-git-workspace-detect-");
|
||||
// Create a real git sub-repo inside the project root (but no workspace.json)
|
||||
const subRepo = join(projectPath, "repo-a");
|
||||
mkdirSync(subRepo, { recursive: true });
|
||||
await git(subRepo, ["init", "-b", "main"]);
|
||||
await git(subRepo, ["config", "user.email", "test@test.com"]);
|
||||
await git(subRepo, ["config", "user.name", "Test"]);
|
||||
writeFileSync(join(subRepo, "README.md"), "# repo-a\n");
|
||||
await git(subRepo, ["add", "README.md"]);
|
||||
await git(subRepo, ["commit", "-m", "init"]);
|
||||
|
||||
const outcome = await ensureGitRepositoryForProjectPath(projectPath);
|
||||
|
||||
expect(outcome).toBe("existing");
|
||||
expect(existsSync(join(projectPath, ".git"))).toBe(false);
|
||||
// workspace.json should be auto-persisted so future calls hit the fast path
|
||||
expect(existsSync(join(projectPath, ".fusion", "workspace.json"))).toBe(true);
|
||||
// config.json should reflect workspaceMode: true so the dashboard toggle is correct
|
||||
const configPath = join(projectPath, ".fusion", "config.json");
|
||||
expect(existsSync(configPath)).toBe(true);
|
||||
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
||||
expect(config.settings?.workspaceMode).toBe(true);
|
||||
});
|
||||
|
||||
it("does not misclassify node_modules git dirs as workspace sub-repos", async () => {
|
||||
const projectPath = tempDir("fusion-git-workspace-nodemodules-");
|
||||
// Create a node_modules sub-dir with a real .git (simulates a package installed from git)
|
||||
const fakePkg = join(projectPath, "node_modules", "some-package");
|
||||
mkdirSync(fakePkg, { recursive: true });
|
||||
await git(fakePkg, ["init", "-b", "main"]);
|
||||
await git(fakePkg, ["config", "user.email", "test@test.com"]);
|
||||
await git(fakePkg, ["config", "user.name", "Test"]);
|
||||
writeFileSync(join(fakePkg, "index.js"), "module.exports = {};\n");
|
||||
await git(fakePkg, ["add", "index.js"]);
|
||||
await git(fakePkg, ["commit", "-m", "init"]);
|
||||
|
||||
// Also create a real sibling sub-repo to prove it IS detected while node_modules is excluded
|
||||
const realRepo = join(projectPath, "my-app");
|
||||
mkdirSync(realRepo, { recursive: true });
|
||||
await git(realRepo, ["init", "-b", "main"]);
|
||||
await git(realRepo, ["config", "user.email", "test@test.com"]);
|
||||
await git(realRepo, ["config", "user.name", "Test"]);
|
||||
writeFileSync(join(realRepo, "README.md"), "# my-app\n");
|
||||
await git(realRepo, ["add", "README.md"]);
|
||||
await git(realRepo, ["commit", "-m", "init"]);
|
||||
|
||||
const detected = await detectWorkspaceRepos(projectPath);
|
||||
|
||||
// node_modules is excluded; my-app is detected
|
||||
expect(detected).toEqual(["my-app"]);
|
||||
});
|
||||
|
||||
it("skips auto-detection when workspaceMode is explicitly false in config.json", async () => {
|
||||
const projectPath = tempDir("fusion-git-workspace-disabled-");
|
||||
// Create a real git sub-repo so detectWorkspaceRepos would find it
|
||||
const subRepo = join(projectPath, "repo-a");
|
||||
mkdirSync(subRepo, { recursive: true });
|
||||
await git(subRepo, ["init", "-b", "main"]);
|
||||
await git(subRepo, ["config", "user.email", "test@test.com"]);
|
||||
await git(subRepo, ["config", "user.name", "Test"]);
|
||||
writeFileSync(join(subRepo, "README.md"), "# repo-a\n");
|
||||
await git(subRepo, ["add", "README.md"]);
|
||||
await git(subRepo, ["commit", "-m", "init"]);
|
||||
|
||||
// Write config.json with workspaceMode: false (user disabled it via dashboard)
|
||||
mkdirSync(join(projectPath, ".fusion"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(projectPath, ".fusion", "config.json"),
|
||||
JSON.stringify({ settings: { workspaceMode: false } }),
|
||||
);
|
||||
|
||||
const outcome = await ensureGitRepositoryForProjectPath(projectPath);
|
||||
|
||||
// Should proceed to git init, not workspace detection
|
||||
expect(outcome).toBe("initialized");
|
||||
expect(existsSync(join(projectPath, ".git"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,6 +59,34 @@ export async function ensureGitRepositoryForProjectPath(
|
||||
return "existing";
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:Workspace 2026-06-24-14:30:
|
||||
Fallback workspace detection: when workspace.json is missing (e.g. project added via
|
||||
dashboard or `fn project add`, which don't run the interactive workspace detection flow),
|
||||
probe for git sub-repos. If found, persist workspace.json AND set workspaceMode: true in
|
||||
config.json so the dashboard toggle reflects the actual state. This covers all registration
|
||||
surfaces: the CLI interactive setup writes workspace.json explicitly, but dashboard POST
|
||||
/api/projects and `fn project add` do not — without this fallback they would create a stray
|
||||
.git at the workspace root because loadWorkspaceConfig returned null.
|
||||
|
||||
FNXC:Workspace 2026-06-24-17:00:
|
||||
If the user has explicitly disabled workspace mode (workspaceMode: false in config.json),
|
||||
skip auto-detection and proceed to git init. Without this guard, toggling workspace mode off
|
||||
via the dashboard would have no lasting effect — the fallback would re-detect sub-repos and
|
||||
re-create workspace.json on the next registration call.
|
||||
*/
|
||||
if (!(await isWorkspaceModeExplicitlyDisabled(projectPath))) {
|
||||
const detectedRepos = await detectWorkspaceRepos(projectPath, runner, timeout);
|
||||
if (detectedRepos.length > 0) {
|
||||
// Write config.json first so a failure here doesn't leave a stale workspace.json
|
||||
// that would short-circuit loadWorkspaceConfig on the next call without the
|
||||
// workspaceMode setting being persisted.
|
||||
await setWorkspaceModeInConfig(projectPath, true);
|
||||
await saveWorkspaceConfig(projectPath, { repos: detectedRepos });
|
||||
return "existing";
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await runner("git", ["-C", projectPath, "init"], { timeout });
|
||||
return "initialized";
|
||||
@@ -116,8 +144,16 @@ function extractCommandErrorMessage(error: unknown): string {
|
||||
/**
|
||||
* Scans `dir` one level deep for sub-directories that are git repositories.
|
||||
* Returns relative paths of found repos, sorted alphabetically.
|
||||
*
|
||||
* Excludes `node_modules`, `.fusion`, and other known non-workspace directories so that
|
||||
* packages installed from git sources (which leave real `.git` dirs) do not produce
|
||||
* false-positive workspace members.
|
||||
*/
|
||||
export async function detectWorkspaceRepos(dir: string): Promise<string[]> {
|
||||
export async function detectWorkspaceRepos(
|
||||
dir: string,
|
||||
runner: GitRepositoryCommandRunner = runGitCommand,
|
||||
timeout: number = DEFAULT_GIT_TIMEOUT_MS,
|
||||
): Promise<string[]> {
|
||||
let entries: string[];
|
||||
try {
|
||||
const { readdir } = await import("node:fs/promises");
|
||||
@@ -134,7 +170,17 @@ export async function detectWorkspaceRepos(dir: string): Promise<string[]> {
|
||||
proof of a git repository. Each candidate child is validated with a real `git rev-parse`
|
||||
work-tree probe before it counts, so stray `.git` entries do not yield false-positive repos.
|
||||
*/
|
||||
/*
|
||||
FNXC:Workspace 2026-06-24-15:00:
|
||||
Exclude node_modules and .fusion so that npm packages installed from git sources (which
|
||||
leave real .git directories inside node_modules/<package>) and Fusion's own state directory
|
||||
do not produce false-positive workspace members. A workspace root is a plain directory whose
|
||||
immediate children are the intended sub-repos, not transitive dependency artifacts.
|
||||
*/
|
||||
const EXCLUDED_ENTRIES = new Set(["node_modules", ".fusion", ".git", ".pi"]);
|
||||
for (const entry of entries) {
|
||||
if (EXCLUDED_ENTRIES.has(entry)) continue;
|
||||
|
||||
const childDir = join(dir, entry);
|
||||
// Cheap pre-filter: skip children with no `.git` marker at all before spawning git.
|
||||
try {
|
||||
@@ -143,7 +189,7 @@ export async function detectWorkspaceRepos(dir: string): Promise<string[]> {
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (await isInsideGitWorkTree(childDir, runGitCommand, DEFAULT_GIT_TIMEOUT_MS)) {
|
||||
if (await isInsideGitWorkTree(childDir, runner, timeout)) {
|
||||
found.push(entry);
|
||||
}
|
||||
}
|
||||
@@ -156,6 +202,51 @@ export interface WorkspaceConfig {
|
||||
|
||||
const WORKSPACE_CONFIG_FILENAME = "workspace.json";
|
||||
|
||||
/**
|
||||
* Reads .fusion/config.json and returns true when `workspaceMode` is explicitly
|
||||
* set to `false`. This guards the auto-detection fallback so a user who has
|
||||
* intentionally disabled workspace mode doesn't get it silently re-enabled.
|
||||
*/
|
||||
async function isWorkspaceModeExplicitlyDisabled(projectPath: string): Promise<boolean> {
|
||||
try {
|
||||
const { readFile } = await import("node:fs/promises");
|
||||
const { join } = await import("node:path");
|
||||
const raw = await readFile(join(projectPath, ".fusion", "config.json"), "utf-8");
|
||||
const config = JSON.parse(raw) as { settings?: { workspaceMode?: boolean } };
|
||||
return config.settings?.workspaceMode === false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:Workspace 2026-06-24-17:15:
|
||||
* Writes `workspaceMode: true` into .fusion/config.json so the dashboard toggle
|
||||
* reflects that workspace mode is active after auto-detection. Reads-merges-writes
|
||||
* to avoid clobbering existing config settings.
|
||||
*/
|
||||
async function setWorkspaceModeInConfig(projectPath: string, value: boolean): Promise<void> {
|
||||
const { readFile, writeFile, mkdir } = await import("node:fs/promises");
|
||||
const { join } = await import("node:path");
|
||||
const configPath = join(projectPath, ".fusion", "config.json");
|
||||
let config: Record<string, unknown> = {};
|
||||
try {
|
||||
config = JSON.parse(await readFile(configPath, "utf-8")) as Record<string, unknown>;
|
||||
} catch (err) {
|
||||
// Only treat "file not found" as empty config; re-throw parse/permission errors
|
||||
// so a corrupted config.json doesn't get silently clobbered with a fresh object.
|
||||
if ((err as NodeJS.ErrnoException)?.code !== "ENOENT") throw err;
|
||||
}
|
||||
// Validate settings is a plain object before merging
|
||||
if (typeof config.settings !== "object" || config.settings === null || Array.isArray(config.settings)) {
|
||||
config.settings = {};
|
||||
}
|
||||
const settings = config.settings as Record<string, unknown>;
|
||||
settings.workspaceMode = value;
|
||||
await mkdir(join(projectPath, ".fusion"), { recursive: true });
|
||||
await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:Workspace 2026-06-22-00:00:
|
||||
Workspace repo entries are later joined onto the workspace root to resolve worktrees, so an
|
||||
|
||||
@@ -538,6 +538,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
researchDefaultTimeout: 300000,
|
||||
researchMaxSourcesPerRun: 20,
|
||||
researchMaxSynthesisRounds: 2,
|
||||
workspaceMode: false,
|
||||
} satisfies CompleteSettings<ProjectSettingsSchema>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdir, readdir, readFile, stat, writeFile, rename, unlink } from "node:fs/promises";
|
||||
import { mkdir, readdir, readFile, stat, writeFile, rename, unlink, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { existsSync, watch, type Dirent, type FSWatcher } from "node:fs";
|
||||
import { detectWorkspaceRepos, saveWorkspaceConfig, loadWorkspaceConfig } from "./git-repository.js";
|
||||
import type { Task, TaskDetail, TaskCreateInput, TaskAttachment, AgentLogEntry, BoardConfig, Column, ColumnId, CheckoutClaimPrecondition, MergeResult, Settings, GlobalSettings, ProjectSettings, ActivityLogEntry, ActivityEventType, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, Artifact, ArtifactCreateInput, ArtifactType, ArtifactWithTask, InboxTask, TaskLogEntry, RunMutationContext, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, ArchivedTaskEntry, ArchiveAgentLogMode, TaskPriority, SourceType, WorkflowStepTemplate, Agent, AutostashOrphanRecord, TaskCommitAssociation, TaskCommitAssociationMatchSource, TaskCommitAssociationConfidence, CommitAssociationDiffBackfillReport, GithubIssueAction, MergeQueueEntry, MergeQueueEnqueueOptions, MergeQueueAcquireOptions, MergeQueueReleaseOutcome, HandoffToReviewOptions, GoalCitation, GoalCitationFilter, GoalCitationInput, GoalCitationSurface, BranchGroup, BranchGroupCreateInput, BranchGroupUpdate, TaskBranchAssignmentMode, MergeRequestRecord, MergeRequestState, MergeRequestWorkflowProjectionOptions, CompletionHandoffMarker, WorkflowWorkItem, WorkflowWorkItemDueFilter, WorkflowWorkItemKind, WorkflowWorkItemState, WorkflowWorkItemTransitionPatch, WorkflowWorkItemUpsertInput, PrEntity, PrEntityCreateInput, PrEntityUpdate, PrEntityState, PrThreadState, PrThreadOutcome, PrConflictState, PrChecksRollup, PrReviewDecision, PluginActivation, PluginActivationInput } from "./types.js";
|
||||
import { createActivityLogSnapshot, createRunAuditSnapshot, createTaskMetadataSnapshot, toTaskMetadataRecord, validateSnapshotEnvelope, type ActivityLogSnapshot, type RunAuditSnapshot, type TaskMetadataSnapshot } from "./shared-mesh-state.js";
|
||||
import { VALID_TRANSITIONS, COLUMNS, DEFAULT_SETTINGS, isColumn, isGlobalOnlySettingsKey, WORKFLOW_STEP_TEMPLATES, validateDocumentKey, assertNotWorkspaceTaskMerge } from "./types.js";
|
||||
@@ -3879,6 +3880,40 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:Workspace 2026-06-24-16:00:
|
||||
When workspaceMode is toggled on, detect sub-repos and persist workspace.json so the
|
||||
executor and ensureGitRepositoryForProjectPath treat the root as workspace-mode. When
|
||||
toggled off, remove workspace.json so the root falls back to single-repo behavior.
|
||||
*/
|
||||
if (updatedMerged.workspaceMode === true && previousMerged.workspaceMode !== true) {
|
||||
try {
|
||||
const existing = await loadWorkspaceConfig(this.rootDir);
|
||||
if (!existing) {
|
||||
const repos = await detectWorkspaceRepos(this.rootDir);
|
||||
if (repos.length > 0) {
|
||||
await saveWorkspaceConfig(this.rootDir, { repos });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
storeLog.warn("workspace.json sync failed after workspaceMode toggle-on", {
|
||||
phase: "updateSettings:workspace-toggle-on",
|
||||
rootDir: this.rootDir,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
} else if (updatedMerged.workspaceMode === false && previousMerged.workspaceMode === true) {
|
||||
try {
|
||||
await rm(join(this.rootDir, ".fusion", "workspace.json"), { force: true });
|
||||
} catch (err) {
|
||||
storeLog.warn("workspace.json removal failed after workspaceMode toggle-off", {
|
||||
phase: "updateSettings:workspace-toggle-off",
|
||||
rootDir: this.rootDir,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return updatedMerged;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4466,6 +4466,16 @@ export interface ProjectSettings {
|
||||
/** Hard cap on the synthesized "Earlier room context" summary block.
|
||||
* Default: 1500. */
|
||||
chatRoomSummaryMaxChars?: number;
|
||||
/**
|
||||
* FNXC:Workspace 2026-06-24-16:00:
|
||||
* When true, the project root is treated as a workspace-mode parent directory containing
|
||||
* multiple git sub-repos (recorded in .fusion/workspace.json), not a single git repo.
|
||||
* ensureGitRepositoryForProjectPath skips `git init` for workspace roots, and the executor
|
||||
* runs tasks per-sub-repo instead of at the root. Auto-detected at registration time when
|
||||
* sub-repos are found, with an interactive confirmation prompt. Can be toggled per-project
|
||||
* via the dashboard Settings modal or PUT /settings.
|
||||
*/
|
||||
workspaceMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -94,6 +94,18 @@ export function GeneralSection({ scopeBanner, form, setForm, projectId, addToast
|
||||
<input id="ephemeralAgentsEnabled" type="checkbox" checked={form.ephemeralAgentsEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, ephemeralAgentsEnabled: e.target.checked }))}/>{t("settings.general.useEphemeralTaskWorkerAgents", " Use ephemeral task-worker agents ")}</label>
|
||||
<small>{t("settings.general.whenEnabledDefaultFusionSpawnsShortLived", " When enabled (default), Fusion spawns short-lived ")}<code>executor-FN-XXXX</code>{t("settings.general.agentsToRunEachTaskWhenDisabledOnly", " agents to run each task. When disabled, only permanent agents execute tasks and the scheduler auto-assigns work using the agent reporting chain. Tasks with no eligible permanent agent stay queued. ")}</small>
|
||||
</div>
|
||||
{/*
|
||||
FNXC:Workspace 2026-06-24-16:00:
|
||||
Workspace mode toggle: when enabled, the project root is treated as a workspace parent
|
||||
containing multiple git sub-repos instead of a single git repo. The executor runs tasks
|
||||
per-sub-repo, and git init is skipped at the root. Toggling on triggers detectWorkspaceRepos
|
||||
and persists .fusion/workspace.json; toggling off removes it.
|
||||
*/}
|
||||
<div className="form-group">
|
||||
<label htmlFor="workspaceMode" className="checkbox-label">
|
||||
<input id="workspaceMode" type="checkbox" checked={form.workspaceMode === true} onChange={(e) => setForm((f) => ({ ...f, workspaceMode: e.target.checked }))}/>{t("settings.general.workspaceMode", " Workspace mode (multi-repo) ")}</label>
|
||||
<small>{t("settings.general.workspaceModeHint", "When enabled, the project root is treated as a workspace containing multiple git sub-repos. Tasks run per-sub-repo and no git repo is created at the root. Disable for single-repo projects.")}</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="completionDocumentationMode">{t("settings.general.completionDocumentationAutomation", "Completion Documentation Automation")}</label>
|
||||
<select id="completionDocumentationMode" value={form.completionDocumentationMode || "off"} onChange={(e) => setForm((f) => ({
|
||||
|
||||
Reference in New Issue
Block a user