## Summary - Extend the workspace content-hash skip cache to **all** packages (not just plugins), with `--force` / `--full` flags - Default local CLI packaging to a **fast mode** (bin/extension + migrations only); full desktop/plugin/DTS staging runs on CI or `pnpm build:full` - Enable TypeScript `incremental` builds for warm recompiles - Add `maxConcurrentVerifications` (default **1**) so concurrent tasks cannot stack monorepo typecheck/build and peg CPU Warm `pnpm build` measured ~**126s → ~0.8s** when nothing changed. ## Test plan - [x] `node --test scripts/__tests__/build-workspace.test.mjs` (12 pass) - [x] `pnpm --filter @fusion/engine exec vitest run src/__tests__/verification-concurrency.test.ts` - [x] `pnpm --filter @fusion/core exec vitest run src/__tests__/settings-parity.test.ts` - [x] Local: first `pnpm build` rebuilds as needed; second warm `pnpm build` skips all packages (~0.8s) - [x] Fast CLI packaging logs skip of desktop/plugin staging without `FUSION_CLI_FULL_PACKAGE` - [ ] CI: `pnpm build` still full-packages under `CI=true` (plugin staging / release surfaces) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a Scheduling setting to limit concurrent verification tasks from 1–8, with a default of 1. * Verification tasks now support cancellation while waiting or running. * Added options for forced and full workspace builds. * **Performance** * Local builds can skip unchanged packages and use incremental compilation for faster rebuilds. * Local CLI packaging is faster by default, while full packaging remains available when needed. * **Documentation** * Updated the settings reference with the new verification concurrency option. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
249 lines
9.6 KiB
TypeScript
249 lines
9.6 KiB
TypeScript
/**
|
|
* Section -> owned settings keys (+ scope) registry (FN-7506).
|
|
*
|
|
* FNXC:SettingsReset 2026-07-04-00:00:
|
|
* "Reset this menu" in the Settings footer must touch ONLY the active
|
|
* section's own keys, at the correct scope (global vs project), and must
|
|
* never silently reset a section that isn't a simple settings blob. This
|
|
* module is the single source of truth for that mapping so the reset flow
|
|
* and `splitSettingsSave` (save-split.ts) never diverge on which keys belong
|
|
* to which section. Global-section entries are re-exported from
|
|
* `GLOBAL_SECTION_KEYS` in save-split.ts (do not duplicate that list here);
|
|
* this file adds the missing PROJECT-section entries and the shared
|
|
* exclusion list.
|
|
*
|
|
* Design decisions (recorded in the `plan` task document for FN-7506):
|
|
* 1. A key-owning section maps to { scope, keys }. Every key here MUST be a
|
|
* real member of GLOBAL_SETTINGS_KEYS or PROJECT_SETTINGS_KEYS matching
|
|
* the declared scope (enforced by section-keys.test.ts).
|
|
* 2. Non-key sections (secrets, global-mcp, mcp, plugins, memory,
|
|
* authentication, prompts, cli-agents, and the three runtime sections)
|
|
* are NOT a simple settings blob — they are managed by their own CRUD
|
|
* flows/routes. They are explicitly EXCLUDED from per-menu reset rather
|
|
* than silently reset. See EXCLUDED_RESET_SECTIONS below.
|
|
* 3. Reset semantics: GLOBAL keys reset to the canonical
|
|
* `DEFAULT_GLOBAL_SETTINGS` value (write). PROJECT keys reset via
|
|
* null-as-delete (write `null`) so an inherited/overridable project
|
|
* setting reverts to its inherited/default value, matching the existing
|
|
* null-as-delete convention already used by `splitSettingsSave`.
|
|
* 4. Some field names are edited from more than one section in the UI
|
|
* (e.g. `gitlabEnabled`'s enable toggle + URL fields live in "general",
|
|
* while its auth token fields live in "merge"). Each such key is
|
|
* assigned to exactly ONE canonical owning section below to keep every
|
|
* section's reset scoped to a disjoint key set; see the inline notes.
|
|
*/
|
|
import { GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS } from "@fusion/core";
|
|
import { GLOBAL_SECTION_KEYS, MODEL_LANE_KEYS } from "./save-split";
|
|
|
|
export type SettingsResetScope = "global" | "project";
|
|
|
|
export interface SectionKeyEntry {
|
|
scope: SettingsResetScope;
|
|
keys: readonly string[];
|
|
}
|
|
|
|
/**
|
|
* Project-scope section -> owned key registry. Reuses MODEL_LANE_KEYS from
|
|
* save-split.ts for the project-models lane overrides instead of duplicating
|
|
* them.
|
|
*/
|
|
const PROJECT_SECTION_KEYS: Record<string, readonly string[]> = {
|
|
general: [
|
|
"allowAbsoluteFileBrowserPaths",
|
|
"capacityRiskBannerEnabled",
|
|
"capacityRiskTodoThreshold",
|
|
"chatAutoCleanupDays",
|
|
"chatRoomCompactionFetchLimit",
|
|
"chatRoomRecentVerbatimMessages",
|
|
"chatRoomSummaryMaxChars",
|
|
"completionDocumentationMode",
|
|
"enabledBuiltinWorkflowIds",
|
|
"ephemeralAgentsCanCreateTasks",
|
|
"ephemeralAgentsEnabled",
|
|
"githubLinkImportedIssuesToTracking",
|
|
"githubTrackingDedupEnabled",
|
|
"githubTrackingDefaultRepo",
|
|
"githubTrackingEnabledByDefault",
|
|
"sessionAdvisorEnabledByDefault",
|
|
// gitlabEnabled/gitlabInstanceUrl/gitlabApiBaseUrl's enable+URL fields are
|
|
// owned here; gitlabAuthToken/gitlabAuthTokenType are owned by "merge".
|
|
"gitlabApiBaseUrl",
|
|
"gitlabEnabled",
|
|
"gitlabInstanceUrl",
|
|
"mailAutoCleanupDays",
|
|
"operationalLogRetentionDays",
|
|
"quickChatButtonMode",
|
|
"quickChatCloseOnOutsideClick",
|
|
"showQuickChatFAB",
|
|
"showTaskChatsInCommonFeed",
|
|
"taskPrefix",
|
|
"workspaceMode",
|
|
],
|
|
commands: ["buildCommand", "testCommand"],
|
|
worktrees: [
|
|
"executorAllowSiblingBranchRename",
|
|
"maxWorktrees",
|
|
"recycleWorktrees",
|
|
"showWorktreeGrouping",
|
|
"worktreeCopyFiles",
|
|
"worktreeInitCommand",
|
|
"worktreeNaming",
|
|
"worktreeRebaseBeforeMerge",
|
|
"worktreeRebaseLocalBase",
|
|
"worktreeRebaseRemote",
|
|
"worktreesDir",
|
|
"worktrunk",
|
|
],
|
|
scheduling: [
|
|
"archiveAgentLogMode",
|
|
"autoArchiveDoneAfterMs",
|
|
"autoArchiveDoneTasksEnabled",
|
|
"engineerBacklogAutoClaim",
|
|
"groupOverlappingFiles",
|
|
"heartbeatScopeDiscipline",
|
|
"ignoreHiddenOverlapPaths",
|
|
"maxConcurrent",
|
|
"maxConcurrentVerifications",
|
|
"maxStuckKills",
|
|
"maxTriageConcurrent",
|
|
"overlapIgnorePaths",
|
|
"pollIntervalMs",
|
|
"preserveProgressOnStuckRequeue",
|
|
"specStalenessEnabled",
|
|
"specStalenessMaxAgeMs",
|
|
"staleHighFanoutBlockerAgeThresholdMs",
|
|
"taskStuckTimeoutMs",
|
|
],
|
|
"scheduled-evals": ["evalSettings"],
|
|
"node-routing": ["defaultNodeId", "unavailableNodePolicy"],
|
|
merge: [
|
|
"autoMerge",
|
|
"autoResolveConflicts",
|
|
"commitAuthorEmail",
|
|
"commitAuthorEnabled",
|
|
"commitAuthorName",
|
|
"directMergeCommitStrategy",
|
|
"githubAuthMode",
|
|
"githubAuthToken",
|
|
// gitlabAuthToken/gitlabAuthTokenType are owned here; gitlabEnabled's
|
|
// enable+URL fields are owned by "general" (see above).
|
|
"gitlabAuthToken",
|
|
"gitlabAuthTokenType",
|
|
"includeTaskIdInCommit",
|
|
"integrationBranch",
|
|
"maxAutoMergeRetries",
|
|
"mergeAdvanceAutoSync",
|
|
"mergeConflictStrategy",
|
|
"mergeIntegrationWorktree",
|
|
"mergeStrategy",
|
|
"mergeStrategyOverlapBehavior",
|
|
"merger",
|
|
"planApprovalMode",
|
|
"postMergeAuditMode",
|
|
"pushAfterMerge",
|
|
"pushRemote",
|
|
"smartConflictResolution",
|
|
"testMode",
|
|
],
|
|
"agent-permissions": ["agentProvisioning", "defaultAgentPermissionPolicy"],
|
|
backups: [
|
|
"autoBackupDir",
|
|
"autoBackupEnabled",
|
|
"autoBackupRetention",
|
|
"autoBackupSchedule",
|
|
"memoryBackupDir",
|
|
"memoryBackupEnabled",
|
|
"memoryBackupRetention",
|
|
"memoryBackupSchedule",
|
|
"memoryBackupScope",
|
|
],
|
|
"research-project": ["researchSettings"],
|
|
"project-models": [
|
|
"autoSelectModelPreset",
|
|
"autoSummarizeTitles",
|
|
"defaultPresetBySize",
|
|
"defaultWorkflowId",
|
|
"modelPresets",
|
|
"prDescriptionPromptInstructions",
|
|
"prTitlePromptInstructions",
|
|
"tokenCap",
|
|
"useAiMergeCommitSummary",
|
|
...MODEL_LANE_KEYS,
|
|
],
|
|
};
|
|
|
|
/**
|
|
* Non-key sections that are NOT a simple settings blob. Each is managed by
|
|
* its own dedicated flow/routes (secrets store, MCP server CRUD, plugin
|
|
* manager, memory editor, auth/OAuth, prompt library, CLI adapter approvals,
|
|
* plugin runtime config), so a generic "reset to defaults" over the merged
|
|
* settings form would be meaningless or actively destructive. Per-menu reset
|
|
* is disabled for these with a documented reason (surfaced in the dialog).
|
|
*/
|
|
export const EXCLUDED_RESET_SECTIONS: Record<string, string> = {
|
|
secrets: "Secrets are managed by the Secrets store, not the settings form.",
|
|
"global-mcp": "MCP servers are managed by their own add/edit/remove flow.",
|
|
mcp: "MCP servers are managed by their own add/edit/remove flow.",
|
|
plugins: "Plugins and Pi extensions are managed by the Plugin Manager.",
|
|
memory: "Memory files are edited directly, not as a settings blob.",
|
|
authentication: "Authentication/provider credentials are managed by their own OAuth/API-key flow.",
|
|
prompts: "Prompt library entries are managed by their own editor, not bulk-reset here.",
|
|
"cli-agents": "Per-adapter CLI agent settings are managed by their own approval/config flow.",
|
|
"hermes-runtime": "Runtime plugin settings are managed by the plugin's own config surface.",
|
|
"openclaw-runtime": "Runtime plugin settings are managed by the plugin's own config surface.",
|
|
"paperclip-runtime": "Runtime plugin settings are managed by the plugin's own config surface.",
|
|
};
|
|
|
|
/**
|
|
* Resolve the { scope, keys } entry for a key-owning section id, or `null`
|
|
* for excluded/non-key/group-header sections.
|
|
*/
|
|
export function getSectionKeyEntry(sectionId: string): SectionKeyEntry | null {
|
|
/*
|
|
FNXC:SettingsReset 2026-07-04-00:10:
|
|
Exclusions are checked FIRST because a couple of section ids collide across
|
|
the two lookup tables for unrelated reasons: "global-mcp" has an entry in
|
|
GLOBAL_SECTION_KEYS (used by splitSettingsSave to gate the normal Save flow)
|
|
but is explicitly excluded from RESET because MCP servers are managed by
|
|
their own CRUD flow, not a bulk reset. "project-models" also has an entry in
|
|
GLOBAL_SECTION_KEYS (its dual-scope global lane baselines) but for reset
|
|
purposes only its project-owned keys are touched, so PROJECT_SECTION_KEYS
|
|
is checked before GLOBAL_SECTION_KEYS.
|
|
*/
|
|
if (EXCLUDED_RESET_SECTIONS[sectionId]) {
|
|
return null;
|
|
}
|
|
const projectKeys = PROJECT_SECTION_KEYS[sectionId];
|
|
if (projectKeys) {
|
|
return { scope: "project", keys: projectKeys };
|
|
}
|
|
const globalKeys = GLOBAL_SECTION_KEYS[sectionId];
|
|
if (globalKeys) {
|
|
return { scope: "global", keys: Array.from(globalKeys) };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** True when a section id has no reset-eligible key set (excluded or unknown/group-header). */
|
|
export function isResetEligibleSection(sectionId: string): boolean {
|
|
return getSectionKeyEntry(sectionId) !== null;
|
|
}
|
|
|
|
/** Human-readable reason a section's per-menu reset is disabled, or undefined if it is eligible. */
|
|
export function getResetIneligibleReason(sectionId: string): string | undefined {
|
|
return EXCLUDED_RESET_SECTIONS[sectionId];
|
|
}
|
|
|
|
/** Every PROJECT_SETTINGS_KEYS member, used for "reset all project settings". */
|
|
export const ALL_PROJECT_RESET_KEYS: readonly string[] = PROJECT_SETTINGS_KEYS;
|
|
|
|
/** Exposed for tests: validates every registry key against the canonical scope key sets. */
|
|
export function isRegistryKeyValidForScope(key: string, scope: SettingsResetScope): boolean {
|
|
if (scope === "global") {
|
|
return (GLOBAL_SETTINGS_KEYS as readonly string[]).includes(key);
|
|
}
|
|
return (PROJECT_SETTINGS_KEYS as readonly string[]).includes(key);
|
|
}
|
|
|
|
export { GLOBAL_SECTION_KEYS, MODEL_LANE_KEYS };
|