Files
fusion/packages/dashboard/app/components/settings/section-keys.ts
gsxdsm e9f14bf024 perf: speed up local pnpm build and cap stacked verifications (#2134)
## 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 -->
2026-07-15 08:44:11 -07:00

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 };