feat(dashboard): workspace detection + task prefix in project import wizard (#1746)
## Problem After PR #1741 merged the core workspace mode defaults and prefix derivation, the dashboard project import wizard (SetupWizardModal) still had no workspace detection or task prefix field. Users importing a multi-repo project (e.g. a directory containing `openvide/` and `swarmclaw/` as separate git repos) saw no workspace mode option and no prefix configuration. ## Changes ### New API endpoint: `POST /api/projects/detect-workspace` - Probes a directory for git sub-repos using `detectWorkspaceRepos` from `@fusion/core` - Returns `{ repos: string[], isWorkspace: boolean }` - Excludes `node_modules`, `.fusion`, `.git`, `.pi` from detection ### Modified `POST /api/projects` registration route - Accepts `workspaceMode` and `taskPrefix` from the client - When `workspaceMode: true`, detects and persists sub-repos to `workspace.json` and sets `workspaceMode: true` in config.json - When unspecified, auto-detects sub-repos and applies workspace mode if found - Task prefix uses client-provided value or falls back to `suggestTaskPrefix(name)` ### SetupWizardModal UI - **Workspace mode checkbox**: Auto-detects sub-repos when a directory path is entered via `POST /api/projects/detect-workspace`. Shows a pre-checked "Workspace mode (multi-repo)" checkbox listing detected repos when sub-repos are found - **Task prefix field**: Auto-derived from project name, editable, capped at 5 chars. Shown alongside the workspace checkbox ### Review fixes (from PR #1741 round 3) - Aligned dashboard settings regex from `{1,10}` to `{1,5}` to match CLI cap - Fixed `distributed-task-id.ts` fallback from `"KB"` to `"FN"` (3 occurrences) - Moved CLI `taskPrefix`/`defaultWorkflowId` persistence outside interactive-only block - Wrapped both CLI `TaskStore` lifecycles in `try/finally` to guarantee `close()` on error ## Testing - `pnpm typecheck` — pass - `pnpm lint` — pass - `pnpm test:gate` — 313/313 pass - `vitest run git-repository.test.ts` — 8/8 pass - Verified `detectWorkspaceRepos` returns `["openvide", "swarmclaw"]` on `/Users/eclipxe/Projects/multiclaw` <!-- stage-review-badge-begin --> --- <a href="https://stagereview.app/Runfusion/Fusion/pull/1746"> <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 workspace detection during project setup, including sub-repo discovery and a workspace mode toggle when applicable. * Added task prefix support during setup/registration, with auto-suggestions derived from the project name and persisted preferences. * Exposed a workspace-detect API used by the setup wizard to guide selection. * **Bug Fixes** * Improved setup registration cleanup to reliably close background resources. * Updated the legacy task ID prefix fallback when configuration is missing or unreadable. * Tightened task prefix validation length limits in settings. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -680,14 +680,17 @@ export async function registerProjectInteractive(
|
||||
// Initialize the project (create .fusion/)
|
||||
const { TaskStore } = await import("@fusion/core");
|
||||
const store = new TaskStore(absPath);
|
||||
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 });
|
||||
try {
|
||||
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}`);
|
||||
} finally {
|
||||
await store.close();
|
||||
}
|
||||
await store.close();
|
||||
console.log(` ✓ Initialized fn at ${absPath}`);
|
||||
} else {
|
||||
throw new ProjectResolutionError(
|
||||
"Cannot register project without .fusion/ directory. Run `fn init` first.",
|
||||
@@ -747,29 +750,32 @@ export async function registerProjectInteractive(
|
||||
|
||||
/*
|
||||
FNXC:Onboarding 2026-06-24-18:00:
|
||||
After registration, prompt the user to confirm a task prefix and default workflow.
|
||||
The prefix defaults to the first 2-4 chars of the project name so each project gets
|
||||
recognizable task IDs (e.g., "MYPR" for "my-project"). The workflow defaults to coding.
|
||||
Both are persisted to config.json via the TaskStore.
|
||||
After registration, set a task prefix and default workflow. The prefix defaults to
|
||||
the first 2-4 chars of the project name so each project gets recognizable task IDs
|
||||
(e.g., "MYPR" for "my-project"). The workflow defaults to coding. Both are persisted
|
||||
to config.json via the TaskStore.
|
||||
*/
|
||||
if (interactive) {
|
||||
{
|
||||
const { TaskStore } = await import("@fusion/core");
|
||||
const store = new TaskStore(absPath);
|
||||
await store.init();
|
||||
|
||||
const suggestedPrefix = suggestTaskPrefix(name);
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
const prefixInput = await rl.question(`\n Task prefix [${suggestedPrefix}]: `);
|
||||
rl.close();
|
||||
const rawPrefix = prefixInput.trim().toUpperCase().replace(/[^A-Z]/g, "");
|
||||
const prefix = rawPrefix.length >= 2 && rawPrefix.length <= 5 ? rawPrefix : suggestedPrefix;
|
||||
|
||||
await store.updateSettings({
|
||||
taskPrefix: prefix,
|
||||
defaultWorkflowId: "builtin:coding",
|
||||
});
|
||||
await store.close();
|
||||
console.log(` ✓ Task prefix set to "${prefix}", default workflow: coding`);
|
||||
try {
|
||||
await store.init();
|
||||
let prefix = suggestTaskPrefix(name);
|
||||
if (interactive) {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
const prefixInput = await rl.question(`\n Task prefix [${prefix}]: `);
|
||||
rl.close();
|
||||
const rawPrefix = prefixInput.trim().toUpperCase().replace(/[^A-Z]/g, "");
|
||||
if (rawPrefix.length >= 1 && rawPrefix.length <= 5) prefix = rawPrefix;
|
||||
}
|
||||
await store.updateSettings({
|
||||
taskPrefix: prefix,
|
||||
defaultWorkflowId: "builtin:coding",
|
||||
});
|
||||
console.log(` ✓ Task prefix set to "${prefix}", default workflow: coding`);
|
||||
} finally {
|
||||
await store.close();
|
||||
}
|
||||
}
|
||||
|
||||
return createResolvedProject(project);
|
||||
|
||||
@@ -77,16 +77,16 @@ function getConfiguredPrefixAndLegacyNextId(db: Database): { prefix: string; nex
|
||||
.prepare("SELECT nextId, settings FROM config WHERE id = 1")
|
||||
.get() as { nextId: number | null; settings: string | null } | undefined;
|
||||
if (!row) {
|
||||
return { prefix: "KB", nextId: null };
|
||||
return { prefix: "FN", nextId: null };
|
||||
}
|
||||
|
||||
const settings = row.settings ? (JSON.parse(row.settings) as { taskPrefix?: string }) : null;
|
||||
return {
|
||||
prefix: (settings?.taskPrefix ?? "KB").trim().toUpperCase(),
|
||||
prefix: (settings?.taskPrefix ?? "FN").trim().toUpperCase(),
|
||||
nextId: typeof row.nextId === "number" ? row.nextId : null,
|
||||
};
|
||||
} catch {
|
||||
return { prefix: "KB", nextId: null };
|
||||
return { prefix: "FN", nextId: null };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6770,6 +6770,8 @@ export interface ProjectCreateInput {
|
||||
isolationMode?: "in-process" | "child-process";
|
||||
nodeId?: string;
|
||||
cloneUrl?: string;
|
||||
workspaceMode?: boolean;
|
||||
taskPrefix?: string;
|
||||
}
|
||||
|
||||
export type DockerNodeConfigInfo = DockerNodeConfig;
|
||||
@@ -7236,6 +7238,13 @@ export function registerProject(input: ProjectCreateInput): Promise<ProjectInfo>
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
/** Detect git sub-repos in a directory (workspace mode detection) */
|
||||
export function detectWorkspace(path: string): Promise<{ repos: string[]; isWorkspace: boolean }> {
|
||||
return api<{ repos: string[]; isWorkspace: boolean }>("/projects/detect-workspace", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ path }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Unregister a project */
|
||||
export function unregisterProject(id: string): Promise<void> {
|
||||
|
||||
@@ -3,9 +3,22 @@ import { lazy, Suspense, useState, useCallback, useMemo, useRef, useEffect, type
|
||||
import { X, Loader2, CheckCircle, ChevronRight, Sparkles } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { AgentOnboardingSummary, ProjectInfo, ProjectCreateInput } from "../api";
|
||||
import { createAgent, registerProject } from "../api";
|
||||
import { createAgent, registerProject, detectWorkspace } from "../api";
|
||||
import { DirectoryPicker } from "./DirectoryPicker";
|
||||
import { suggestProjectName } from "../utils/projectDetection";
|
||||
|
||||
/*
|
||||
FNXC:TaskPrefix 2026-06-24-19:00:
|
||||
Derive a task prefix from a project name in the browser. Mirrors the logic in
|
||||
@fusion/core's suggestTaskPrefix: strip non-alpha, uppercase, take 2-4 chars,
|
||||
fall back to "FN". Duplicated because @fusion/core is server-only.
|
||||
*/
|
||||
function suggestTaskPrefixFromName(name: string): string {
|
||||
const cleaned = name.replace(/[^a-zA-Z]/g, "").toUpperCase();
|
||||
if (cleaned.length >= 2 && cleaned.length <= 4) return cleaned;
|
||||
if (cleaned.length > 4) return cleaned.slice(0, 4);
|
||||
return "FN";
|
||||
}
|
||||
import { useNodes } from "../hooks/useNodes";
|
||||
import { AgentAvatar } from "./AgentAvatar";
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
@@ -44,6 +57,10 @@ interface WizardState {
|
||||
manualName: string;
|
||||
manualIsolationMode: "in-process" | "child-process";
|
||||
manualNodeId: string;
|
||||
manualTaskPrefix: string;
|
||||
detectedRepos: string[];
|
||||
workspaceMode: boolean;
|
||||
isDetectingWorkspace: boolean;
|
||||
registeredProject: ProjectInfo | null;
|
||||
selectedPresetId: string;
|
||||
agentDraft: AgentDraftValues;
|
||||
@@ -90,6 +107,10 @@ export function SetupWizardModal({
|
||||
manualName: "",
|
||||
manualIsolationMode: "in-process",
|
||||
manualNodeId: "",
|
||||
manualTaskPrefix: "",
|
||||
detectedRepos: [],
|
||||
workspaceMode: false,
|
||||
isDetectingWorkspace: false,
|
||||
registeredProject: null,
|
||||
selectedPresetId: ceoPreset.id,
|
||||
agentDraft: mapPresetToAgentDraft(ceoPreset),
|
||||
@@ -125,16 +146,48 @@ export function SetupWizardModal({
|
||||
}
|
||||
}, [state.agentError]);
|
||||
|
||||
const detectWorkspaceRequestId = useRef(0);
|
||||
|
||||
const handlePathChange = useCallback((path: string) => {
|
||||
setState((prev) => {
|
||||
const updates: Partial<WizardState> = { manualPath: path };
|
||||
const updates: Partial<WizardState> = { manualPath: path, detectedRepos: [], workspaceMode: false };
|
||||
// Auto-suggest name when path changes and name is empty or was previously auto-suggested
|
||||
if (path && (!prev.manualName || prev.manualName === suggestProjectName(prev.manualPath))) {
|
||||
updates.manualName = suggestProjectName(path);
|
||||
}
|
||||
// Auto-suggest prefix when name changes and prefix is empty or was previously auto-suggested
|
||||
const suggestedName = updates.manualName ?? prev.manualName;
|
||||
if (suggestedName && (!prev.manualTaskPrefix || prev.manualTaskPrefix === suggestTaskPrefixFromName(suggestProjectName(prev.manualPath)))) {
|
||||
updates.manualTaskPrefix = suggestTaskPrefixFromName(suggestedName);
|
||||
}
|
||||
return { ...prev, ...updates };
|
||||
});
|
||||
}, []);
|
||||
|
||||
/*
|
||||
FNXC:Workspace 2026-06-24-21:00:
|
||||
Detect workspace sub-repos only in existing-directory mode (clone mode creates a fresh
|
||||
directory with a single repo). A monotonic request ID guards against stale responses
|
||||
overwriting state from a newer path entry (race condition on rapid typing).
|
||||
*/
|
||||
if (state.manualMode === "existing" && path.trim() && path.trim() !== "/") {
|
||||
const requestId = ++detectWorkspaceRequestId.current;
|
||||
setState((prev) => ({ ...prev, isDetectingWorkspace: true }));
|
||||
detectWorkspace(path.trim())
|
||||
.then((result) => {
|
||||
if (requestId !== detectWorkspaceRequestId.current) return;
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isDetectingWorkspace: false,
|
||||
detectedRepos: result.repos,
|
||||
workspaceMode: result.isWorkspace,
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
if (requestId !== detectWorkspaceRequestId.current) return;
|
||||
setState((prev) => ({ ...prev, isDetectingWorkspace: false }));
|
||||
});
|
||||
}
|
||||
}, [state.manualMode]);
|
||||
|
||||
const handleManualRegister = useCallback(async () => {
|
||||
const trimmedPath = state.manualPath.trim();
|
||||
@@ -153,6 +206,8 @@ export function SetupWizardModal({
|
||||
isolationMode: state.manualIsolationMode,
|
||||
nodeId: state.manualNodeId || undefined,
|
||||
cloneUrl: state.manualMode === "clone" ? trimmedCloneUrl : undefined,
|
||||
workspaceMode: state.workspaceMode,
|
||||
taskPrefix: state.manualTaskPrefix.trim() || undefined,
|
||||
};
|
||||
|
||||
const result = await registerProject(input);
|
||||
@@ -179,7 +234,7 @@ export function SetupWizardModal({
|
||||
error: err instanceof Error ? err.message : "Failed to register project",
|
||||
}));
|
||||
}
|
||||
}, [includeAgentStep, onProjectRegistered, state.manualPath, state.manualName, state.manualCloneUrl, state.manualMode, state.manualIsolationMode, state.manualNodeId]);
|
||||
}, [includeAgentStep, onProjectRegistered, state.manualPath, state.manualName, state.manualCloneUrl, state.manualMode, state.manualIsolationMode, state.manualNodeId, state.workspaceMode, state.manualTaskPrefix]);
|
||||
|
||||
const handlePresetSelect = useCallback((presetId: string) => {
|
||||
const preset = getPresetById(presetId);
|
||||
@@ -369,6 +424,64 @@ export function SetupWizardModal({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
FNXC:Workspace 2026-06-24-19:00:
|
||||
Workspace mode detection: when the selected directory contains git sub-repos,
|
||||
show a checkbox letting the user opt into workspace mode. In workspace mode,
|
||||
tasks run per-sub-repo and no git repo is created at the root.
|
||||
*/}
|
||||
{isExistingMode && state.manualPath.trim() && (
|
||||
<div className="form-group">
|
||||
<label htmlFor="workspace-mode" className="checkbox-label">
|
||||
<input
|
||||
id="workspace-mode"
|
||||
type="checkbox"
|
||||
checked={state.workspaceMode}
|
||||
onChange={(e) => setState((prev) => ({ ...prev, workspaceMode: e.target.checked }))}
|
||||
/>
|
||||
{t("setup.workspaceMode", "Workspace mode (multi-repo)")}
|
||||
</label>
|
||||
{state.isDetectingWorkspace && (
|
||||
<p className="form-hint">
|
||||
<Loader2 size={12} className="animate-spin" style={{ display: "inline-block", verticalAlign: "middle", marginRight: 4 }} />
|
||||
{t("setup.detectingWorkspace", "Detecting sub-repositories...")}
|
||||
</p>
|
||||
)}
|
||||
{!state.isDetectingWorkspace && state.detectedRepos.length > 0 && (
|
||||
<p className="form-hint">
|
||||
{t("setup.detectedRepos", "Found {{count}} repositories:", { count: state.detectedRepos.length })}
|
||||
{" "}
|
||||
{state.detectedRepos.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
{!state.isDetectingWorkspace && state.detectedRepos.length === 0 && state.workspaceMode === false && state.manualPath.trim() && (
|
||||
<p className="form-hint">
|
||||
{t("setup.noSubReposDetected", "No sub-repositories detected. Enable if this is a multi-repo workspace.")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*
|
||||
FNXC:TaskPrefix 2026-06-24-19:00:
|
||||
Task prefix field: auto-derived from the project name. The prefix is used
|
||||
for task IDs (e.g. "MYPR-1"). Users can override it.
|
||||
*/}
|
||||
<div className="form-group">
|
||||
<label htmlFor="task-prefix">{t("setup.taskPrefix", "Task Prefix")}</label>
|
||||
<input
|
||||
id="task-prefix"
|
||||
type="text"
|
||||
value={state.manualTaskPrefix}
|
||||
onChange={(e) => setState((prev) => ({ ...prev, manualTaskPrefix: e.target.value.toUpperCase() }))}
|
||||
placeholder={suggestTaskPrefixFromName(state.manualName || "FN")}
|
||||
maxLength={5}
|
||||
/>
|
||||
<p className="form-hint">
|
||||
{t("setup.taskPrefixHint", "Used for task IDs (e.g. \"{{prefix}}-1\"). Derived from project name.", { prefix: state.manualTaskPrefix || "FN" })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="setup-wizard-advanced">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -65,8 +65,8 @@ export function GeneralSection({ scopeBanner, form, setForm, projectId, addToast
|
||||
<input id="taskPrefix" type="text" placeholder={t("settings.general.fN", "FN")} value={form.taskPrefix || ""} onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setForm((f) => ({ ...f, taskPrefix: val || undefined }));
|
||||
if (val && !/^[A-Z]{1,10}$/.test(val)) {
|
||||
setPrefixError(t("settings.general.prefixMustBe110UppercaseLetters", "Prefix must be 1–10 uppercase letters"));
|
||||
if (val && !/^[A-Z]{1,5}$/.test(val)) {
|
||||
setPrefixError(t("settings.general.prefixMustBe15UppercaseLetters", "Prefix must be 1–5 uppercase letters"));
|
||||
}
|
||||
else {
|
||||
setPrefixError(null);
|
||||
|
||||
@@ -210,6 +210,39 @@ export const registerProjectRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/projects/detect-workspace
|
||||
* Probe a directory for git sub-repos (workspace mode detection).
|
||||
* Body: { path: string }
|
||||
* Returns: { repos: string[], isWorkspace: boolean }
|
||||
*/
|
||||
router.post("/projects/detect-workspace", async (req, res) => {
|
||||
try {
|
||||
const { path } = req.body;
|
||||
if (!path || typeof path !== "string" || !path.trim()) {
|
||||
throw badRequest("path is required");
|
||||
}
|
||||
const normalizedPath = path.trim();
|
||||
if (!isAbsolute(normalizedPath)) {
|
||||
throw badRequest("path must be an absolute path");
|
||||
}
|
||||
|
||||
try {
|
||||
await access(normalizedPath);
|
||||
} catch {
|
||||
throw badRequest("Project path does not exist");
|
||||
}
|
||||
|
||||
const { detectWorkspaceRepos } = await import("@fusion/core");
|
||||
const repos = await detectWorkspaceRepos(normalizedPath);
|
||||
|
||||
res.json({ repos, isWorkspace: repos.length > 0 });
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) throw err;
|
||||
throw new ApiError(500, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/projects
|
||||
* Register a new project.
|
||||
@@ -218,13 +251,15 @@ export const registerProjectRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
* path: string,
|
||||
* isolationMode?: "in-process" | "child-process",
|
||||
* nodeId?: string,
|
||||
* cloneUrl?: string
|
||||
* cloneUrl?: string,
|
||||
* workspaceMode?: boolean,
|
||||
* taskPrefix?: string
|
||||
* }
|
||||
* Returns: RegisteredProject
|
||||
*/
|
||||
router.post("/projects", async (req, res) => {
|
||||
try {
|
||||
const { name, path, isolationMode = "in-process", nodeId, cloneUrl } = req.body;
|
||||
const { name, path, isolationMode = "in-process", nodeId, cloneUrl, workspaceMode, taskPrefix } = req.body;
|
||||
|
||||
if (!name || typeof name !== "string" || !name.trim()) {
|
||||
throw badRequest("name is required and must be a non-empty string");
|
||||
@@ -376,21 +411,47 @@ export const registerProjectRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
|
||||
/*
|
||||
FNXC:Onboarding 2026-06-24-18:00:
|
||||
For new registrations (not reattachments), set a derived task prefix and default
|
||||
workflow via the per-project TaskStore config.json so the project is immediately
|
||||
usable without manual settings configuration.
|
||||
For new registrations (not reattachments), configure workspace mode (if specified or
|
||||
auto-detected), set a task prefix, and default workflow via the per-project TaskStore
|
||||
config.json so the project is immediately usable without manual settings configuration.
|
||||
*/
|
||||
if (activeProjectWithOutcome.outcome === "registered") {
|
||||
try {
|
||||
const { TaskStore, suggestTaskPrefix } = await import("@fusion/core");
|
||||
const { TaskStore, suggestTaskPrefix, detectWorkspaceRepos, saveWorkspaceConfig } = await import("@fusion/core");
|
||||
const store = new TaskStore(normalizedPath);
|
||||
await store.init();
|
||||
const prefix = suggestTaskPrefix(normalizedName);
|
||||
await store.updateSettings({
|
||||
taskPrefix: prefix,
|
||||
defaultWorkflowId: "builtin:coding",
|
||||
});
|
||||
await store.close();
|
||||
try {
|
||||
await store.init();
|
||||
|
||||
/*
|
||||
FNXC:Workspace 2026-06-24-19:00:
|
||||
Workspace mode: if the client explicitly requested it (workspaceMode: true from the
|
||||
wizard checkbox), detect and persist sub-repos. If the client didn't specify and
|
||||
auto-detection finds sub-repos, also apply it. This mirrors the CLI interactive flow.
|
||||
*/
|
||||
if (workspaceMode === true) {
|
||||
const repos = await detectWorkspaceRepos(normalizedPath);
|
||||
if (repos.length > 0) {
|
||||
await saveWorkspaceConfig(normalizedPath, { repos });
|
||||
await store.updateSettings({ workspaceMode: true });
|
||||
}
|
||||
} else if (workspaceMode === undefined) {
|
||||
const repos = await detectWorkspaceRepos(normalizedPath);
|
||||
if (repos.length > 0) {
|
||||
await saveWorkspaceConfig(normalizedPath, { repos });
|
||||
await store.updateSettings({ workspaceMode: true });
|
||||
}
|
||||
}
|
||||
|
||||
const rawPrefix = typeof taskPrefix === "string" ? taskPrefix.trim().toUpperCase() : "";
|
||||
const validPrefix = /^[A-Z]{1,5}$/.test(rawPrefix) ? rawPrefix : "";
|
||||
const prefix = validPrefix || suggestTaskPrefix(normalizedName);
|
||||
await store.updateSettings({
|
||||
taskPrefix: prefix,
|
||||
defaultWorkflowId: "builtin:coding",
|
||||
});
|
||||
} finally {
|
||||
await store.close();
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: project registration succeeded; settings can be configured later
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user