From 4b7f0d2d0ebd17cc483756c4d514a5a7a5dc1000 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 16 Jul 2026 01:06:43 -0700 Subject: [PATCH] fix(dashboard): theme every Settings checkbox and move all inline help behind ? icons - One settings-scoped checkbox rule (accent, size, focus ring) so the Advanced-settings toggle, SettingsToggleRow, ntfy/webhook card headers, and MCP toggle stop falling back to the browser-default accent. - Fix the empty/off-screen help bubble on mobile: .notification-provider-header and .settings-field-label-row are now positioned ancestors for SettingsHelpTip. - Migrate every remaining inline description across settings sections to the shared SettingsHelpTip "?" affordance (validation errors and live status stay inline). Co-Authored-By: Claude Fable 5 --- .../settings-help-icons-checkbox-theming.md | 7 ++ .../app/components/SettingsModal.css | 25 +++++++ .../app/components/SettingsModal.tsx | 24 ++++--- .../__tests__/SettingsModal.general.test.tsx | 8 ++- .../components/settings/SettingsHelpTip.css | 7 +- .../sections/AgentPermissionsSection.tsx | 14 ++-- .../sections/AuthenticationSection.tsx | 28 ++++---- .../settings/sections/ExperimentalSection.tsx | 11 +++- .../settings/sections/GeneralSection.tsx | 48 ++++++++++---- .../sections/GlobalGeneralSection.tsx | 35 ++++++---- .../sections/KeyboardShortcutsSection.tsx | 24 ++++--- .../settings/sections/McpServersCard.tsx | 14 ++-- .../settings/sections/MemorySection.tsx | 66 ++++++++++--------- .../settings/sections/MergeSection.tsx | 7 +- .../settings/sections/ModelPricingSection.tsx | 17 +++-- .../settings/sections/NodeRoutingSection.tsx | 12 +++- .../sections/NotificationsSection.tsx | 54 ++++++++------- .../sections/ProjectModelsSection.tsx | 52 ++++++++++----- .../settings/sections/PromptsSection.tsx | 13 ++-- .../settings/sections/RemoteSection.tsx | 4 ++ .../sections/ResearchGlobalSection.tsx | 24 +++++-- .../sections/ResearchProjectSection.tsx | 30 +++++---- .../settings/sections/SchedulingSection.tsx | 11 ++-- .../sections/SourceControlGlobalSection.tsx | 17 ++++- .../sections/SourceControlSection.tsx | 18 ++++- 25 files changed, 386 insertions(+), 184 deletions(-) create mode 100644 .changeset/settings-help-icons-checkbox-theming.md diff --git a/.changeset/settings-help-icons-checkbox-theming.md b/.changeset/settings-help-icons-checkbox-theming.md new file mode 100644 index 0000000000..1a754cbba6 --- /dev/null +++ b/.changeset/settings-help-icons-checkbox-theming.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Settings: consistent checkbox theming, inline help moved behind "?" icons, mobile ntfy help bubble fix. +category: fix +dev: New `.settings-modal input[type="checkbox"]` rule unifies accent/size; SettingsHelpTip mobile positioned-ancestor list now includes `.notification-provider-header` and `.settings-field-label-row`; all bespoke inline `` help across settings sections migrated to SettingsHelpTip. diff --git a/packages/dashboard/app/components/SettingsModal.css b/packages/dashboard/app/components/SettingsModal.css index de269932fe..be5c214bf6 100644 --- a/packages/dashboard/app/components/SettingsModal.css +++ b/packages/dashboard/app/components/SettingsModal.css @@ -1047,6 +1047,31 @@ Mirrors `.settings-field-row-head` (same gap, same wrap) so the two idioms produ line-height: 1.35; } +/* +FNXC:SettingsStyling 2026-07-16-12:40: +Every checkbox in Settings resolves to ONE appearance, whatever markup it sits in. +The global `.form-group input[type="checkbox"]` rule themes only checkboxes that happen to sit inside a form-group, so the Advanced-settings header toggle, SettingsToggleRow's `.settings-toggle`, the ntfy/webhook card-header enables, and the MCP card toggle all fell back to the browser-default accent — observed as a mix of default-blue and themed checkboxes in one section on Android. +Scoped to `.settings-modal` (both the dialog and the embedded `settings-modal--embedded` panel carry it) and declaration-identical to the global form-group rule, so cascade ties between the two are harmless. +*/ +.settings-modal input[type="checkbox"] { + width: 16px; + height: 16px; + flex-shrink: 0; + padding: 0; + margin: 0; + accent-color: var(--todo); + cursor: pointer; + border-radius: var(--radius-sm); + transition: box-shadow var(--transition-fast); +} +.settings-modal input[type="checkbox"]:focus-visible { + box-shadow: var(--focus-ring-strong); + outline: none; +} +.settings-modal input[type="checkbox"]:disabled { + cursor: default; +} + /* FNXC:SettingsStyling 2026-07-15-18:52: Every text-entry control in Settings resolves to ONE appearance, whatever markup it sits in. diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index 4a74ab5d4c..1a66ebae82 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -23,6 +23,7 @@ import { type DashboardShortcutAction, } from "../utils/keyboardShortcuts"; import type { DashboardKeyboardShortcutMap } from "../utils/keyboardShortcuts"; +import { SettingsHelpTip } from "./settings/SettingsHelpTip"; import type { SectionSaveHandler } from "./settings/sections/context"; import { AppearanceSection } from "./settings/sections/AppearanceSection"; import { ExperimentalSection } from "./settings/sections/ExperimentalSection"; @@ -4708,16 +4709,19 @@ export function SettingsModal({
- - {t("settings.importExport.replaceWarning", "If unchecked, existing settings will be replaced with imported values.")} + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. */} +
+ + {t("settings.importExport.replaceWarning", "If unchecked, existing settings will be replaced with imported values.")} +
diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx index cd2822488d..a7a5606289 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx @@ -740,13 +740,15 @@ describe("SettingsModal", () => { expect(screen.getByRole("checkbox", { name: "Save AI thinking for permanent agents" })).not.toBeChecked(); expect(screen.getByRole("checkbox", { name: "Save AI thinking for ephemeral / task-worker agents" })).not.toBeChecked(); - // Migrated rows source help from the primitive (now its help tip); the still-bespoke - // thinking-log group, whose one help string covers two checkboxes, keeps its . + /* + FNXC:SettingsHelp 2026-07-16-12:45: + All help copy — including the bespoke thinking-log group's shared string, which previously stayed in an inline — now renders behind the shared "?" affordance (operator requirement: no inline description paragraphs in Settings). Both helpers must resolve inside a help bubble. + */ expect(document.querySelector(".settings-field-help")).toBeNull(); const toolOutputHelper = screen.getByText(/When disabled, tool rows are still logged but detailed tool payloads are omitted/i); expect(toolOutputHelper.closest(".settings-help-bubble")).toBeTruthy(); const thinkingHelper = screen.getByText(/Leave both thinking toggles off to keep the original default behavior/i); - expect(thinkingHelper.closest("small")).toBeTruthy(); + expect(thinkingHelper.closest(".settings-help-bubble")).toBeTruthy(); }); /* diff --git a/packages/dashboard/app/components/settings/SettingsHelpTip.css b/packages/dashboard/app/components/settings/SettingsHelpTip.css index 658cc36416..b37021985d 100644 --- a/packages/dashboard/app/components/settings/SettingsHelpTip.css +++ b/packages/dashboard/app/components/settings/SettingsHelpTip.css @@ -100,10 +100,15 @@ On a narrow viewport the bubble anchors to the ROW, not to the trigger, and span Anchoring to the trigger fails wherever the trigger sits away from the left edge: the label line reads "Name [scope] ?", so the "?" is often near the right of a 390px screen, and a bubble starting there runs off-screen no matter how tightly `max-width` clamps it. Measured on an iPhone-sized viewport before this rule: the bubble spanned x=338→658 against a 390px screen — 268px of it unreachable. Neutralising `.settings-help`'s own positioning makes the row the nearest positioned ancestor, so `inset-inline: 0` resolves against the full row and the bubble simply cannot be clipped horizontally. It lands under the whole row rather than under the icon, which on a phone reads better anyway. The row selectors cover both idioms: `.settings-field-row` (shared primitive) and `.form-group` (rows that deliberately stay bespoke). + +FNXC:SettingsHelp 2026-07-16-12:40: +Every container that hosts a bespoke SettingsHelpTip must be in this positioned-ancestor list. The ntfy/webhook card headers (`.notification-provider-header`) were missing, so on a phone their bubble anchored to the page shell and rendered at top:100% of it — below the viewport, unreachable (the ntfy "Enable" tip opened with no visible bubble). `.settings-field-label-row` is included for bespoke label lines that sit outside a `.form-group` (e.g. card bodies), so a tip hosted there can never regress the same way. */ @media (max-width: 768px) { .settings-field-row, - .settings-content .form-group { + .settings-content .form-group, + .settings-field-label-row, + .notification-provider-header { position: relative; } diff --git a/packages/dashboard/app/components/settings/sections/AgentPermissionsSection.tsx b/packages/dashboard/app/components/settings/sections/AgentPermissionsSection.tsx index 555479c9e8..fbf6b56f55 100644 --- a/packages/dashboard/app/components/settings/sections/AgentPermissionsSection.tsx +++ b/packages/dashboard/app/components/settings/sections/AgentPermissionsSection.tsx @@ -3,6 +3,7 @@ import type { AgentPermissionPolicy, AgentPermissionPolicyRules } from "@fusion/ import { AgentPermissionPolicyEditor } from "../../AgentPermissionPolicyEditor"; import { AgentProvisioningPolicyEditor } from "../../AgentProvisioningPolicyEditor"; import type { SectionBaseProps } from "./context"; +import { SettingsHelpTip } from "../SettingsHelpTip"; import { useTranslation } from "react-i18next"; function toCompleteAgentPermissionRules(rules?: Partial): AgentPermissionPolicyRules { return AGENT_PERMISSION_POLICY_ACTION_CATEGORIES.reduce((acc, category) => { @@ -14,18 +15,19 @@ export type AgentPermissionsSectionProps = SectionBaseProps; export function AgentPermissionsSection({ form, setForm }: AgentPermissionsSectionProps) { const { t } = useTranslation("app"); return (<> -

{t("settings.agentPermissions.agentPermissions", "Agent Permissions")}

-
- {t("settings.agentPermissions.perAgentSettingsOverrideProjectDefaultsEachCategory", "Project defaults apply to permanent agents, ephemeral task workers, and fallback executor workers unless a per-agent override is set. Exact tool rules compose with the legacy ephemeral create-task toggle. Default: unset \u2014 every action category defaults to allow until a category is explicitly restricted.")} + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance \u2014 operator requirement: no inline description paragraphs in Settings. These are block-level descriptions of whole editor groups, so each tip sits beside its section heading. */} +
+

{t("settings.agentPermissions.agentPermissions", "Agent Permissions")}

+ {t("settings.agentPermissions.perAgentSettingsOverrideProjectDefaultsEachCategory", "Project defaults apply to permanent agents, ephemeral task workers, and fallback executor workers unless a per-agent override is set. Exact tool rules compose with the legacy ephemeral create-task toggle. Default: unset \u2014 every action category defaults to allow until a category is explicitly restricted.")}
setForm((f) => ({ ...f, defaultAgentPermissionPolicy: { rules: toCompleteAgentPermissionRules(next?.rules), ...(next?.toolRules ? { toolRules: next.toolRules } : {}) }, }))}/> -

{t("settings.agentPermissions.agentProvisioningApprovals", "Agent Provisioning Approvals")}

-
- {t("settings.agentPermissions.configureProjectLevelApprovalBehaviorForDurableProvisioning", " Configure project-level approval behavior for durable provisioning tools (fn_agent_create/fn_agent_delete). Default: no approval policy configured (empty). ")} +
+

{t("settings.agentPermissions.agentProvisioningApprovals", "Agent Provisioning Approvals")}

+ {t("settings.agentPermissions.configureProjectLevelApprovalBehaviorForDurableProvisioning", " Configure project-level approval behavior for durable provisioning tools (fn_agent_create/fn_agent_delete). Default: no approval policy configured (empty). ")}
setForm((f) => ({ ...f, agentProvisioning: next }))}/> ); diff --git a/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx b/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx index d2e46ff5a5..0edc2fc5c3 100644 --- a/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx +++ b/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx @@ -13,6 +13,7 @@ import { LoginInstructions } from "../../LoginInstructions"; import { LoadingSpinner } from "../../LoadingSpinner"; import { OAuthManualCodeForm } from "../../OAuthManualCodeForm"; import { CustomProvidersSection } from "../../CustomProvidersSection"; +import { SettingsHelpTip } from "../SettingsHelpTip"; import { copyTextToClipboard } from "../../../utils/copyToClipboard"; import { appendTokenQuery } from "../../../auth"; import { refreshModelsCache } from "../../../hooks/useModelsCache"; @@ -206,7 +207,11 @@ export function AuthenticationSection({ auth }: AuthenticationSectionProps) { Only `type: "api_key"` cards show key controls so OAuth logout never looks like it will clear `ANTHROPIC_API_KEY`. */ return (<> -

{t("settings.auth.title", "Authentication")}

+ {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. The panel-level "changes take effect immediately" blurb now hangs off the section heading. */} +
+

{t("settings.auth.title", "Authentication")}

+ {t("settings.auth.hint", "Authentication changes take effect immediately — no need to save.")} +
{authLoading ? (
) : authProviders.length === 0 ? (
{t("settings.auth.noProviders", "No providers available")}
) : (
@@ -257,19 +262,18 @@ export function AuthenticationSection({ auth }: AuthenticationSectionProps) {
)}
)} {/* - FNXC:SettingsHelp 2026-07-15-21:40: - No `` in this section moved behind the shared "?" help affordance, and none should. This section has no settings rows: it renders provider CARDS, whose ``s are all live state (save progress, key errors, provider loginError, OpenCode refresh status) that must stay visible, plus two section-level blurbs — this one and the onboarding hint below — that describe the panel rather than any one control. + FNXC:SettingsHelp 2026-07-16-12:45: + The provider cards' ``s stay inline: they are all live state (save progress, key errors, provider loginError, OpenCode refresh status) that must stay visible where the operator is acting. The two DESCRIPTIVE blurbs this section carried — the panel-level "changes take effect immediately" hint and the reopen-onboarding hint — moved behind the shared "?" affordance per the operator requirement that no inline description paragraphs remain in Settings. */} - - {t("settings.auth.hint", "Authentication changes take effect immediately — no need to save.")} - {onReopenOnboarding && (
- - - {t("settings.auth.reopenOnboardingHint", "Re-run the setup wizard to review or update your AI provider and model configuration.")} - +
+ + + {t("settings.auth.reopenOnboardingHint", "Re-run the setup wizard to review or update your AI provider and model configuration.")} + +
)} diff --git a/packages/dashboard/app/components/settings/sections/ExperimentalSection.tsx b/packages/dashboard/app/components/settings/sections/ExperimentalSection.tsx index 0b6b8385c3..3ed9661184 100644 --- a/packages/dashboard/app/components/settings/sections/ExperimentalSection.tsx +++ b/packages/dashboard/app/components/settings/sections/ExperimentalSection.tsx @@ -1,5 +1,6 @@ import type { SectionBaseProps } from "./context"; import { useTranslation } from "react-i18next"; +import { SettingsHelpTip } from "../SettingsHelpTip"; export interface ExperimentalSectionProps extends SectionBaseProps { /** Display labels for well-known features (always rendered). */ knownFeatures: Record; @@ -25,9 +26,13 @@ export function ExperimentalSection({ form, setForm, knownFeatures, legacyAliase ])).filter((key) => !hiddenFeatureKeys?.has(key)).sort((a, b) => a.localeCompare(b)); const featureFlags = allFeatureKeys.map((key) => [key, isFeatureEnabled(experimentalFeatures, key)] as const); return (<> -

{t("settings.experimental.experimentalFeatures", "Experimental Features")}

-
- {t("settings.experimental.experimentalFeaturesAreEarlyCapabilitiesThatAreNot", " Experimental features are early capabilities that are not yet fully stable. Enable them to test new functionality, but be aware they may change or be removed. Default: disabled for every feature flag below. ")} + {/* + FNXC:SettingsHelp 2026-07-16-12:45: + Section intro moved behind the shared "?" beside the heading - operator requirement: no inline description paragraphs in Settings. + */} +
+

{t("settings.experimental.experimentalFeatures", "Experimental Features")}

+ {t("settings.experimental.experimentalFeaturesAreEarlyCapabilitiesThatAreNot", " Experimental features are early capabilities that are not yet fully stable. Enable them to test new functionality, but be aware they may change or be removed. Default: disabled for every feature flag below. ")}
diff --git a/packages/dashboard/app/components/settings/sections/GeneralSection.tsx b/packages/dashboard/app/components/settings/sections/GeneralSection.tsx index 113697754f..923cbfeaf5 100644 --- a/packages/dashboard/app/components/settings/sections/GeneralSection.tsx +++ b/packages/dashboard/app/components/settings/sections/GeneralSection.tsx @@ -4,6 +4,7 @@ import { SettingsToggleRow } from "../SettingsToggleRow"; import { SettingsSelectRow } from "../SettingsSelectRow"; import { SettingsNumberRow } from "../SettingsNumberRow"; import { SettingsTextRow } from "../SettingsTextRow"; +import { SettingsHelpTip } from "../SettingsHelpTip"; /* FNXC:GitHubImportTranslate 2026-07-15-09:30: Locale labels come from core's shared `localeDisplayName` (endonyms), NOT from the LanguageSelector component: importing a component module for a constant drags its i18n/react-i18next initialization into every consumer of this section, which breaks tests that mock react-i18next narrowly. @@ -30,6 +31,9 @@ Plain settings rows render through the shared primitives instead of hand-rolled Every key here is project-scoped (DEFAULT_PROJECT_SETTINGS), which the per-row badge states: the nav already labels the section "Project General", but the badge is what distinguishes these from the global-tier settings an operator sees one section away. Rows that stay bespoke are the ones a single-string descriptor cannot carry without rewording the copy — help built from `t()` fragments interleaved with `` (ephemeral agents, completion documentation) — plus the custom widgets and editors: the workflow pickers, the built-in workflow enablement list, and the Clear-local-data button. +FNXC:SettingsHelp 2026-07-16-12:45: +Bespoke rows no longer render their help as inline `` paragraphs. Their copy moved VERBATIM (same `t()` keys, same `` fragments) behind the shared "?" affordance (`SettingsHelpTip`), matching the primitives' descriptor help — operator requirement: no inline description paragraphs in Settings. Only live status/feedback and validation errors stay inline. + FNXC:SourceControl 2026-07-15-20:30: GitHub/GitLab settings are NOT in this section. The tracking block, the tracking-repo select, and the GitLab disclosure moved to "Source Control · Project" (SourceControlSection.tsx), which also absorbed Merge's GitHub/GitLab auth blocks. Do not add source-control settings back here: `gitlabEnabled` was previously writable from both this section and Merge, and one owning section is what keeps that from recurring. */ @@ -156,10 +160,19 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError />
- {t("settings.general.newTasksInheritThisCustomWorkflowsStepsOverridable", "New tasks inherit this custom workflow's steps (overridable per task). No default \u2014 unset (built-in default workflow).")} + {/* + FNXC:SettingsHelp 2026-07-16-12:45: + Inline help moved behind the shared "?" affordance \u2014 operator requirement: no inline description paragraphs in Settings. + The tip sits AFTER the whole picker widget (not in a `.settings-field-label-row` beside its label) because ProjectDefaultWorkflowField renders its own label inside WorkflowSelector; re-parenting that label is not possible from here, and the tip must stay a sibling of any `
{builtinWorkflows.length > 0 && (
- + {/* FNXC:SettingsHelp 2026-07-16-12:45: The checkbox group's ONE shared help paragraph moved behind a single "?" beside the group label — operator requirement: no inline description paragraphs in Settings. */} +
+ + {t("settings.general.disabledFusionWorkflowsAreHiddenFromWorkflow", "Disabled Fusion workflows are hidden from workflow pickers. Existing tasks that already use one continue to resolve. Default: all built-in workflows enabled (unset).")} +
{builtinWorkflows.map((workflow) => ())}
- {t("settings.general.disabledFusionWorkflowsAreHiddenFromWorkflow", "Disabled Fusion workflows are hidden from workflow pickers. Existing tasks that already use one continue to resolve. Default: all built-in workflows enabled (unset).")}
)}
- + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. */} +
+ + {t("settings.general.aiUndoTaskWorkflowHelp", "Workflow assigned to AI-undo (revert) tasks, which reverse already-shipped code and warrant stricter review. Choose \"Inherit project default workflow\" to leave them on the project default. Default: review-heavy.")} +
- {t("settings.general.aiUndoTaskWorkflowHelp", "Workflow assigned to AI-undo (revert) tasks, which reverse already-shipped code and warrant stricter review. Choose \"Inherit project default workflow\" to leave them on the project default. Default: review-heavy.")}
- - {t("settings.general.whenEnabledDefaultFusionSpawnsShortLived", " When enabled (default), Fusion spawns short-lived ")}executor-FN-XXXX{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. ")} + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. The tip is a SIBLING of the checkbox label (a button inside a label breaks click-to-toggle). */} +
+ + {t("settings.general.whenEnabledDefaultFusionSpawnsShortLived", " When enabled (default), Fusion spawns short-lived ")}executor-FN-XXXX{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. ")} +
{/* FNXC:EphemeralAgentTaskCreation 2026-07-01-00:00: @@ -231,7 +249,11 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError onChange={(v) => setForm((f) => ({ ...f, allowAbsoluteFileBrowserPaths: v === true }))} />
- + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. */} +
+ + {t("settings.general.controlsHowFutureTaskSpecsHandleReleaseNote", " Controls how future task specs handle release-note artifacts at completion. Use changeset mode for repositories that follow ")}.changeset{t("settings.general.workflowsOrChangelogModeWhenContributorsShouldUpdate", " workflows, or changelog mode when contributors should update an existing changelog file. Default: off. ")} +
- {t("settings.general.controlsHowFutureTaskSpecsHandleReleaseNote", " Controls how future task specs handle release-note artifacts at completion. Use changeset mode for repositories that follow ")}.changeset{t("settings.general.workflowsOrChangelogModeWhenContributorsShouldUpdate", " workflows, or changelog mode when contributors should update an existing changelog file. Default: off. ")}
{/* FNXC:SettingsGeneral 2026-07-15-17:35: @@ -522,8 +543,11 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError */}

{t("settings.general.browserData", "Browser Data")}

- - {t("settings.general.clearLocalDataHint", "Remove cached board snapshots, chat threads, and UI preferences stored in this browser. Frees space when the dashboard runs low on browser storage. Your tasks and project settings are stored server-side and are not affected.")} + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. */} +
+ + {t("settings.general.clearLocalDataHint", "Remove cached board snapshots, chat threads, and UI preferences stored in this browser. Frees space when the dashboard runs low on browser storage. Your tasks and project settings are stored server-side and are not affected.")} +
diff --git a/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx b/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx index ff23a6cb1d..9984655a45 100644 --- a/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx +++ b/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx @@ -1,6 +1,7 @@ import { resolvePersistAgentThinkingLog } from "@fusion/core"; import { SettingsToggleRow } from "../SettingsToggleRow"; import { SettingsSelectRow } from "../SettingsSelectRow"; +import { SettingsHelpTip } from "../SettingsHelpTip"; import type { SectionBaseProps } from "./context"; import { useTranslation } from "react-i18next"; export type GlobalGeneralSectionProps = SectionBaseProps; @@ -9,6 +10,9 @@ FNXC:SettingsStyling 2026-07-15-17:35: Plain settings rows render through the shared primitives rather than hand-rolled `form-group` + `checkbox-label` markup, so labels, help copy, and padding come from one type scale. `.form-group` stays global and untouched — 35 non-settings files style forms with it. The migrated keys are all global-tier (DEFAULT_GLOBAL_SETTINGS), so each carries a "global" badge stating that it travels between projects. Rows that stay bespoke are the ones whose copy a single-string descriptor cannot carry without rewording it: the `fn` binary check, the update-check toggle, and the thinking-log group all build label or help from `t()` fragments interleaved with `` tags. The thinking-log pair additionally shares ONE help string across two checkboxes, which no per-row descriptor models. The CLI binary panel has moved to its own advanced-only section. + +FNXC:SettingsHelp 2026-07-16-12:45: +Bespoke rows no longer render their help as inline `` paragraphs. The copy moved VERBATIM (same `t()` keys, same `` fragments) behind the shared "?" affordance (`SettingsHelpTip`) — operator requirement: no inline description paragraphs in Settings. The thinking-log pair's shared help hangs off ONE tip beside the group heading. */ export function GlobalGeneralSection({ form, setForm }: GlobalGeneralSectionProps) { const { t } = useTranslation("app"); @@ -43,26 +47,35 @@ export function GlobalGeneralSection({ form, setForm }: GlobalGeneralSectionProp onChange={(v) => setForm((f) => ({ ...f, persistAgentToolOutput: v === true }))} />
-
{t("settings.globalGeneral.saveAIThinkingLogs", "Save AI thinking logs")}
+ {/* FNXC:SettingsHelp 2026-07-16-12:45: The pair's ONE shared help paragraph moved behind a single "?" beside the group heading — operator requirement: no inline description paragraphs in Settings. */} +
+
{t("settings.globalGeneral.saveAIThinkingLogs", "Save AI thinking logs")}
+ {t("settings.globalGeneral.leaveBothThinkingTogglesOffToKeepThe", " Leave both thinking toggles off to keep the original default behavior. This only controls persisted ")}thinking{t("settings.globalGeneral.rowsAndDoesNotAffectAssistantTextOr", " rows and does not affect assistant text or tool rows. Default: disabled for both permanent and ephemeral agents. ")} +
- {t("settings.globalGeneral.leaveBothThinkingTogglesOffToKeepThe", " Leave both thinking toggles off to keep the original default behavior. This only controls persisted ")}thinking{t("settings.globalGeneral.rowsAndDoesNotAffectAssistantTextOr", " rows and does not affect assistant text or tool rows. Default: disabled for both permanent and ephemeral agents. ")}
- - {t("settings.globalGeneral.whenEnabledTheDashboardProbesForAGlobally", " When enabled, the dashboard probes for a globally-installed")}{" "} - fn / fusion{t("settings.globalGeneral.cLIBySpawning", " CLI by spawning")}{" "} - <bin> --version{t("settings.globalGeneral.disableThisIfYourLocalDevProcessIs", ". Disable this if your local dev process is the source of truth and you don't want any outdated globally-installed binary executed during the probe. Default: enabled. ")} + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. The tip is a SIBLING of the checkbox label (a button inside a label breaks click-to-toggle). */} +
+ + {t("settings.globalGeneral.whenEnabledTheDashboardProbesForAGlobally", " When enabled, the dashboard probes for a globally-installed")}{" "} + fn / fusion{t("settings.globalGeneral.cLIBySpawning", " CLI by spawning")}{" "} + <bin> --version{t("settings.globalGeneral.disableThisIfYourLocalDevProcessIs", ". Disable this if your local dev process is the source of truth and you don't want any outdated globally-installed binary executed during the probe. Default: enabled. ")} +

{t("settings.globalGeneral.updates", "Updates")}

- - {t("settings.globalGeneral.whenEnabledFusionChecksNpmForNewVersions", " When enabled, Fusion checks npm for new versions of")}{" "} - @runfusion/fusion{t("settings.globalGeneral.andShowsUpdateNoticesInTheCLIAnd", " and shows update notices in the CLI and dashboard. Cadence is governed by the frequency below. Default: enabled. ")} + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. The tip is a SIBLING of the checkbox label (a button inside a label breaks click-to-toggle). */} +
+ + {t("settings.globalGeneral.whenEnabledFusionChecksNpmForNewVersions", " When enabled, Fusion checks npm for new versions of")}{" "} + @runfusion/fusion{t("settings.globalGeneral.andShowsUpdateNoticesInTheCLIAnd", " and shows update notices in the CLI and dashboard. Cadence is governed by the frequency below. Default: enabled. ")} +
{/* FNXC:SettingsGlobalGeneral 2026-07-15-17:35: diff --git a/packages/dashboard/app/components/settings/sections/KeyboardShortcutsSection.tsx b/packages/dashboard/app/components/settings/sections/KeyboardShortcutsSection.tsx index 6ede4d6710..45a8c1c2ad 100644 --- a/packages/dashboard/app/components/settings/sections/KeyboardShortcutsSection.tsx +++ b/packages/dashboard/app/components/settings/sections/KeyboardShortcutsSection.tsx @@ -1,5 +1,6 @@ import { useTranslation } from "react-i18next"; import type { SectionBaseProps } from "./context"; +import { SettingsHelpTip } from "../SettingsHelpTip"; import { ShortcutCaptureInput } from "./ShortcutCaptureInput"; import { DEFAULT_DASHBOARD_KEYBOARD_SHORTCUTS, @@ -18,7 +19,10 @@ FNXC:DashboardShortcuts 2026-07-04-00:00: FN-7553 promotes keyboard shortcuts from two bare inputs buried in Global General to their own dedicated settings section, grouped by category (Communication/Workspace/Navigation/Tasks from SHORTCUT_CATEGORIES) with a press-to-record capture control per row. `dashboardKeyboardShortcuts` ownership moved here from `global-general` (save-split.ts GLOBAL_SECTION_KEYS + section-keys.ts) so exactly one section owns the key for save/reset. FNXC:SettingsStyling 2026-07-15-17:35: -This section stays off the shared settings row primitives, unlike its neighbors. It renders no plain setting: every row is a ShortcutCaptureInput bound to one action inside the single `dashboardKeyboardShortcuts` map, and its per-row `small` carries live capture validation (`normalizeKeyboardShortcut` errors) rather than static help copy. A descriptor row keys on a settings field name and there is no field per shortcut — so there is nothing here for a toggle/text/select row to own, and no `.search.ts` sibling. Shortcut discovery is served by the nav entry's `searchableText` in SettingsModal instead. +This section stays off the shared settings row primitives, unlike its neighbors. It renders no plain setting: every row is a ShortcutCaptureInput bound to one action inside the single `dashboardKeyboardShortcuts` map. A descriptor row keys on a settings field name and there is no field per shortcut — so there is nothing here for a toggle/text/select row to own, and no `.search.ts` sibling. Shortcut discovery is served by the nav entry's `searchableText` in SettingsModal instead. + +FNXC:SettingsHelp 2026-07-16-12:45: +Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. The section description sits beside the heading; each row's static "Default: … Leave blank to disable." hint moved into a tip beside its label. The per-row `small` (still `aria-describedby` target via `hintId`) now carries ONLY live capture validation (`normalizeKeyboardShortcut` errors), and the conflict banner stays inline — validation feedback never hides behind a "?". */ export function KeyboardShortcutsSection({ form, setForm }: KeyboardShortcutsSectionProps) { const { t } = useTranslation("app"); @@ -35,8 +39,10 @@ export function KeyboardShortcutsSection({ form, setForm }: KeyboardShortcutsSec return ( <> -

{t("settings.keyboardShortcuts.title", "Keyboard Shortcuts")}

-

{t("settings.keyboardShortcuts.hint", "Configure global dashboard shortcuts. Click Record and press a combination, or type one manually. Shortcuts are ignored while typing in inputs, editors, chat composers, and terminal fields. Leave blank to disable an action.")}

+
+

{t("settings.keyboardShortcuts.title", "Keyboard Shortcuts")}

+ {t("settings.keyboardShortcuts.hint", "Configure global dashboard shortcuts. Click Record and press a combination, or type one manually. Shortcuts are ignored while typing in inputs, editors, chat composers, and terminal fields. Leave blank to disable an action.")} +
{SHORTCUT_CATEGORIES.map((category) => (
@@ -47,7 +53,10 @@ export function KeyboardShortcutsSection({ form, setForm }: KeyboardShortcutsSec const hintId = `${inputId}Hint`; return (
- +
+ + {t("settings.keyboardShortcuts.rowHint", "Default: {{default}}. Leave blank to disable.", { default: DEFAULT_DASHBOARD_KEYBOARD_SHORTCUTS[action] })} +
updateShortcut(action, value)} /> - - {parsed.valid - ? t("settings.keyboardShortcuts.rowHint", "Default: {{default}}. Leave blank to disable.", { default: DEFAULT_DASHBOARD_KEYBOARD_SHORTCUTS[action] }) - : parsed.error} - + {/* FNXC:SettingsHelp 2026-07-16-12:45: The small keeps its hintId (aria-describedby target) but now carries only live validation errors; the static default hint moved behind the "?" above. */} + {parsed.valid ? null : parsed.error}
); })} diff --git a/packages/dashboard/app/components/settings/sections/McpServersCard.tsx b/packages/dashboard/app/components/settings/sections/McpServersCard.tsx index 5ba427f5f1..8f1f53c255 100644 --- a/packages/dashboard/app/components/settings/sections/McpServersCard.tsx +++ b/packages/dashboard/app/components/settings/sections/McpServersCard.tsx @@ -1,5 +1,6 @@ import "./McpServersCard.css"; import { Download, Pencil, Play, Plus, RefreshCw, Trash2, Upload } from "lucide-react"; +import { SettingsHelpTip } from "../SettingsHelpTip"; import type { Dispatch, SetStateAction } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -540,11 +541,14 @@ export function McpServersCard({ scope, form, setForm, globalSettings, projectId
- - {t("settings.mcp.enabledHint", "Default: disabled, with no servers configured.")} + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. settingKey is scope-suffixed because this card renders once per scope and bubble DOM ids must stay unique. */} +
+ + {t("settings.mcp.enabledHint", "Default: disabled, with no servers configured.")} +
diff --git a/packages/dashboard/app/components/settings/sections/MemorySection.tsx b/packages/dashboard/app/components/settings/sections/MemorySection.tsx index 3b666d76d2..56c54b09ea 100644 --- a/packages/dashboard/app/components/settings/sections/MemorySection.tsx +++ b/packages/dashboard/app/components/settings/sections/MemorySection.tsx @@ -66,9 +66,13 @@ export function MemorySection({ form, setForm, memory }: MemorySectionProps) { dreams: "Dreams", }; return (<> -

{t("settings.memory.memory", "Memory")}

-
- {t("settings.memory.memoryLivesIn", " Memory lives in ")}.fusion/memory/{t("settings.memory.agentsSearchWithQmdFirstFallBackTo", ". Agents search with qmd first, fall back to local files when qmd is missing, and open exact line windows only when needed. ")} + {/* + FNXC:SettingsHelp 2026-07-16-12:45: + Section intro moved behind the shared "?" beside the heading — operator requirement: no inline description paragraphs in Settings. + */} +
+

{t("settings.memory.memory", "Memory")}

+ {t("settings.memory.memoryLivesIn", " Memory lives in ")}.fusion/memory/{t("settings.memory.agentsSearchWithQmdFirstFallBackTo", ". Agents search with qmd first, fall back to local files when qmd is missing, and open exact line windows only when needed. ")}
{t("settings.memory.dreaming", " Dreaming\u2026 ")}) : (t("settings.memory.dreamNow", "Dream Now"))} {/* - FNXC:SettingsHelp 2026-07-15-21:40: - Stays inline (same for "Compact Selected File" below): the affordance is a BUTTON, not a labelled control, so there is no label line for a tip to sit on. Hiding a one-shot action's description behind a "?" beside a button would hide what the button does. + FNXC:SettingsHelp 2026-07-16-12:45: + Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings, action buttons included. The tip is a sibling of the button; the button's own label still names the action. */} - {t("settings.memory.manuallyTriggerDreamProcessingNow", "Manually trigger dream processing now.")} + {t("settings.memory.manuallyTriggerDreamProcessingNow", "Manually trigger dream processing now.")}
)} @@ -244,7 +248,14 @@ export function MemorySection({ form, setForm, memory }: MemorySectionProps) { {memoryLoading ? (
) : (
- + {/* + FNXC:SettingsHelp 2026-07-16-12:45: + The descriptive branch moved behind the shared "?" beside the label — operator requirement: no inline description paragraphs in Settings. The dirty-state line below stays inline: it is the live reason the select is DISABLED, not help, and must be visible without opening a tip. + */} +
+ + Choose any project memory file to view or edit. Dreams is selected by default. +
- {/* - FNXC:SettingsHelp 2026-07-15-21:40: - Stays inline: in the dirty branch this copy is the reason the select is DISABLED, not help. An operator who finds the picker greyed out must be told why without hunting for a "?" — so the whole `` stays visible rather than splitting one string across two affordances by state. - */} - - {memoryDirty - ? "Save or discard the current edits before switching files." - : "Choose any project memory file to view or edit. Dreams is selected by default."} - + {memoryDirty && (Save or discard the current edits before switching files.)}
{selectedMemoryFile && (
{memoryLayerNames[selectedMemoryFile.layer]} @@ -271,17 +274,19 @@ export function MemorySection({ form, setForm, memory }: MemorySectionProps) {
)}
- {/* - FNXC:SettingsHelp 2026-07-15-21:40: - Stays inline: this describes what the SELECTED FILE holds and changes with the picker above, so it reads as content orientation for the editor pane, not as help for a control. It also labels a document editor rather than a settings control, which is why it never had an id to hang a tip's key off. + FNXC:SettingsHelp 2026-07-16-12:45: + Per-layer orientation copy moved behind the shared "?" beside the editor label — operator requirement: no inline description paragraphs in Settings. The bubble content still tracks the file picker, so it always describes the currently selected file. */} - - {selectedMemoryFile?.layer === "long-term" && "Curated durable decisions, conventions, constraints, and pitfalls promoted from dreams."} - {selectedMemoryFile?.layer === "daily" && "Raw daily observations, open loops, and running context for dream processing."} - {selectedMemoryFile?.layer === "dreams" && "Synthesized patterns and open loops promoted from daily memory."} - {!selectedMemoryFile && "Edits the selected memory file."} - +
+ + + {selectedMemoryFile?.layer === "long-term" && "Curated durable decisions, conventions, constraints, and pitfalls promoted from dreams."} + {selectedMemoryFile?.layer === "daily" && "Raw daily observations, open loops, and running context for dream processing."} + {selectedMemoryFile?.layer === "dreams" && "Synthesized patterns and open loops promoted from daily memory."} + {!selectedMemoryFile && "Edits the selected memory file."} + +
{ setMemoryContent(content); @@ -295,11 +300,12 @@ export function MemorySection({ form, setForm, memory }: MemorySectionProps) { - - {memoryDirty - ? "Save or discard edits before compacting this file." - : `Compacts ${selectedMemoryPath} and writes the result back to the same file.`} - + {/* + FNXC:SettingsHelp 2026-07-16-12:45: + The descriptive branch moved behind the shared "?" beside the action button — operator requirement: no inline description paragraphs in Settings. The dirty-state line stays inline: it is the live reason the button is DISABLED, not help. + */} + {`Compacts ${selectedMemoryPath} and writes the result back to the same file.`} + {memoryDirty && (Save or discard edits before compacting this file.)}
)} {memoryDirty && isEditingAllowed && (
diff --git a/packages/dashboard/app/components/settings/sections/MergeSection.tsx b/packages/dashboard/app/components/settings/sections/MergeSection.tsx index 5fbe5d872b..39059fd23e 100644 --- a/packages/dashboard/app/components/settings/sections/MergeSection.tsx +++ b/packages/dashboard/app/components/settings/sections/MergeSection.tsx @@ -193,8 +193,11 @@ export function MergeSection({ form, setForm, integrationBranchOptions, integrat }))} />
-
{t("settings.merge.legacyAutoMergeStampCleanup", "Legacy auto-merge stamp cleanup")}
- {t("settings.merge.findsInReviewTasksWhoseAutoMergeValue", " Finds in-review tasks whose auto-merge value came from the legacy review-entry stamp. Dry-run is automatic; applying delegates to the store cleanup and preserves genuine per-task overrides. ")} + {/* FNXC:SettingsHelp 2026-07-16-12:45: The panel's descriptive paragraph moved behind the shared "?" beside its heading — operator requirement: no inline description paragraphs in Settings. The live status/count/success/error ``s below stay inline: they are dynamic feedback, not help copy. */} +
+
{t("settings.merge.legacyAutoMergeStampCleanup", "Legacy auto-merge stamp cleanup")}
+ {t("settings.merge.findsInReviewTasksWhoseAutoMergeValue", " Finds in-review tasks whose auto-merge value came from the legacy review-entry stamp. Dry-run is automatic; applying delegates to the store cleanup and preserves genuine per-task overrides. ")} +
{legacyStampLoading ? ({t("settings.merge.checkingForLegacyAutoMergeStamps", "Checking for legacy auto-merge stamps\u2026")}) : legacyStampCandidates.length === 0 ? ({t("settings.merge.noLegacyAutoMergeStampsToCleanUp", " No legacy auto-merge stamps to clean up. ")}) : (<> {legacyStampCandidates.length}{t("settings.merge.legacyAutoMergeStamp", " legacy auto-merge stamp")}{legacyStampCandidates.length === 1 ? "" : "s"}{t("settings.merge.readyToCleanUp", " ready to clean up.")}
    diff --git a/packages/dashboard/app/components/settings/sections/ModelPricingSection.tsx b/packages/dashboard/app/components/settings/sections/ModelPricingSection.tsx index 9f7d806fe1..9649287ae2 100644 --- a/packages/dashboard/app/components/settings/sections/ModelPricingSection.tsx +++ b/packages/dashboard/app/components/settings/sections/ModelPricingSection.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { X } from "lucide-react"; import type { ModelPricing, ModelPricingOverrides } from "@fusion/core"; import { api } from "../../../api"; +import { SettingsHelpTip } from "../SettingsHelpTip"; import type { ToastType } from "../../../hooks/useToast"; import type { SetSettingsForm, SettingsFormState } from "./context"; import "./ModelPricingSection.css"; @@ -231,10 +232,17 @@ export function ModelPricingSection({ form, setForm, addToast, projectId }: Mode
    -

    {t("settings.modelPricing.title", "Model Pricing")}

    -

    - {t("settings.modelPricing.description", "Override per-1M token rates used by Command Center cost estimates. Overrides win over the built-in baseline; unlisted models still use the baseline. No default \u2014 unset (no overrides).")} -

    + {/* + FNXC:SettingsHelp 2026-07-16-12:45: + Section description and the manual-edit save hint moved behind the shared "?" beside the heading - operator requirement: no inline description paragraphs in Settings. The fetched-at line below stays inline: it is live status (snapshot date/source), not help. + */} +
    +

    {t("settings.modelPricing.title", "Model Pricing")}

    + + {t("settings.modelPricing.description", "Override per-1M token rates used by Command Center cost estimates. Overrides win over the built-in baseline; unlisted models still use the baseline. No default \u2014 unset (no overrides).")}{" "} + {t("settings.modelPricing.saveHint", "Manual edits are saved with the rest of Global settings.")} + +

    {form.modelPricingFetchedAt ? t("settings.modelPricing.pricesAsOf", "Prices as of {{date}}", { date: new Date(form.modelPricingFetchedAt).toLocaleString() }) @@ -256,7 +264,6 @@ export function ModelPricingSection({ form, setForm, addToast, projectId }: Mode {t("settings.modelPricing.viewTable", "View pricing table")}

    - {t("settings.modelPricing.saveHint", "Manual edits are saved with the rest of Global settings.")} {renderPricingTableModal()}
    ); diff --git a/packages/dashboard/app/components/settings/sections/NodeRoutingSection.tsx b/packages/dashboard/app/components/settings/sections/NodeRoutingSection.tsx index 375e5fb015..07115bc70b 100644 --- a/packages/dashboard/app/components/settings/sections/NodeRoutingSection.tsx +++ b/packages/dashboard/app/components/settings/sections/NodeRoutingSection.tsx @@ -1,6 +1,7 @@ import type { NodeInfo } from "../../../api"; import { NodeHealthDot } from "../../NodeHealthDot"; import { SettingsSelectRow } from "../SettingsSelectRow"; +import { SettingsHelpTip } from "../SettingsHelpTip"; import type { SettingsFormState, SetSettingsForm } from "./context"; import { useTranslation } from "react-i18next"; function getNodeStatusLabel(status: "online" | "offline" | "connecting" | "error", t: ReturnType>["t"]): string { @@ -29,10 +30,16 @@ export function NodeRoutingSection({ form, setForm, nodes }: NodeRoutingSectionP

    {t("settings.nodeRouting.theseSettingsApplyAtTheProjectLevel", "These settings apply at the project level.")}

    {/* FNXC:SettingsStyling 2026-07-15-17:35: - This row stays hand-rolled: routing safety requires the live NodeHealthDot for the selected node to sit between the control and its help text, and the shared select row renders only label/control/help with no slot for an adjacent status widget. Forcing it onto the primitive would move or drop the health readout, so the dot wins over row uniformity here. + This row stays hand-rolled: routing safety requires the live NodeHealthDot for the selected node to render right under the control, and the shared select row renders only label/control/help with no slot for an adjacent status widget. Forcing it onto the primitive would move or drop the health readout, so the dot wins over row uniformity here. + + FNXC:SettingsHelp 2026-07-16-12:45: + Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. The tip is a SIBLING of the label (a button inside a label is invalid), so both sit in a `.settings-field-label-row`. The live NodeHealthDot status stays inline: it is state feedback, not help copy. */}
    - +
    + + {t("settings.nodeRouting.usedWhenATaskHasNoNodeOverride", "Used when a task has no node override. Node status is shown for safer routing selection. No default — unset (local execution).")} +
    { - const current = form.ntfyEvents ?? [...DEFAULT_NTFY_EVENTS]; - const newEvents = e.target.checked - ? (current.includes(event) ? current : [...current, event]) - : current.filter((ev): ev is NtfyNotificationEvent => ev !== event); - setForm((f) => ({ ...f, ntfyEvents: newEvents.length > 0 ? newEvents : undefined })); - }}/> - {label} - - {description} + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. */} +
    + + {description} +
    ); })}
@@ -300,17 +303,20 @@ export function NotificationsSection({ form, setForm, testNotificationLoading, t const currentEvents = form.webhookEvents ?? [...DEFAULT_NTFY_EVENTS]; const checked = currentEvents.includes(event); return (
- - {description} + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. */} +
+ + {description} +
); })}
diff --git a/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx b/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx index 3664feac06..c9e8d74434 100644 --- a/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx +++ b/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx @@ -425,15 +425,21 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW /> {/* --- Project Model Lanes --- */} -

{t("settings.projectModels.modelLanes", "Model Lanes")}

-

{t("settings.projectModels.overrideGlobalModelSettingsAtTheProjectLevel", " Override global model settings at the project level. Each lane controls a specific AI usage context. Unset lanes inherit from the corresponding global lane. The Project Default Model is the fallback for this project when a more specific lane is unset. ")}

+ {/* FNXC:SettingsHelp 2026-07-16-12:45: Section description moved behind the shared "?" affordance beside the heading — operator requirement: no inline description paragraphs in Settings. */} +
+

{t("settings.projectModels.modelLanes", "Model Lanes")}

+ {t("settings.projectModels.overrideGlobalModelSettingsAtTheProjectLevel", " Override global model settings at the project level. Each lane controls a specific AI usage context. Unset lanes inherit from the corresponding global lane. The Project Default Model is the fallback for this project when a more specific lane is unset. ")} +
{modelsLoading ? (
) : availableModels.length === 0 ? (
{t("settings.projectModels.noModelsAvailableConfigureAuthenticationFirst", " No models available. Configure authentication first. ")}
) : (<> {projectModelLanes.map(renderProjectLane)} )} {/* FNXC:ChatModels 2026-07-12-20:45: Project Models owns the Direct-chat default because New Chat needs a project-scoped model-or-agent target plus prompt-vs-direct creation mode without changing workflow or in-chat switcher settings. */} -

{t("settings.projectModels.chatHeading", "Chat")}

-

{t("settings.projectModels.chatDescription", "Choose the default target for new Direct chats and whether New Chat should prompt or immediately use that default.")}

+ {/* FNXC:SettingsHelp 2026-07-16-12:45: Section description moved behind the shared "?" affordance beside the heading — operator requirement: no inline description paragraphs in Settings. */} +
+

{t("settings.projectModels.chatHeading", "Chat")}

+ {t("settings.projectModels.chatDescription", "Choose the default target for new Direct chats and whether New Chat should prompt or immediately use that default.")} +
{/* FNXC:SettingsModels 2026-07-15-17:35: Only the mode select migrates to a primitive. The target picker below it (Model/Agent segmented toggle + model dropdown or agent select + shared Reset) is one compound control over five form keys, not a row per key, so it stays bespoke. @@ -468,7 +474,7 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW {/* FNXC:SettingsHelp 2026-07-15-21:40: Model mode is a plain label + control + help row, so its help hangs off the same "?" as the New Chat behavior select directly above it instead of printing a paragraph beside it. - The agent-mode branch below keeps its `` inline: that slot swaps to "No agents are available for this project yet.", which explains an empty picker and must stay in view. + FNXC:SettingsHelp 2026-07-16-12:45: The agent-mode branch's descriptive help now hangs off the same "?" too — operator requirement: no inline description paragraphs in Settings. Only its dynamic empty-state line ("No agents are available for this project yet.") stays inline, because it is live status explaining an empty picker and must stay in view. */}
@@ -481,7 +487,10 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW {chatDefaultCustomized && ()}
) : (
- +
+ + {t("settings.projectModels.chatDefaultAgentHelp", "Agent-mode New Chat starts a Direct chat with the selected durable agent.")} +
{t("settings.researchGlobal.webSearch", " Web Search ")}{t("settings.researchGlobal.alwaysOn", "Always on")} + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline source hints moved behind the shared "?" affordance beside each option's label — operator requirement: no inline description paragraphs in Settings. The tip is a SIBLING of the checkbox label (a button inside a label breaks click-to-toggle). */}
-
{hasMissingResearchCredential && (
{t("settings.researchGlobal.missingCredentialsForTheSelectedResearchProvider", " Missing credentials for the selected research provider. ")} diff --git a/packages/dashboard/app/components/settings/sections/ResearchProjectSection.tsx b/packages/dashboard/app/components/settings/sections/ResearchProjectSection.tsx index f41fae04e5..4153373dfb 100644 --- a/packages/dashboard/app/components/settings/sections/ResearchProjectSection.tsx +++ b/packages/dashboard/app/components/settings/sections/ResearchProjectSection.tsx @@ -16,11 +16,14 @@ FNXC:SettingsSearch 2026-07-15-17:35: The descriptor key is the dotted path `researchSettings.enabled`, not the bare `researchSettings` blob key, because the toggle owns exactly one leaf of that nested object. The sources and limits below own their own leaves of the same blob, so a bare key would collide the moment either migrates, and search would anchor several controls to one row. FNXC:SettingsStyling 2026-07-15-17:35: -Two groups deliberately keep their bespoke markup because they are not plain label+control+help rows: the Enabled Sources grid pairs an always-on locked Web Search row with a checkbox grid carrying inline per-source default hints, and the limits grid lays four numeric fields plus a shared validation error out side by side (`settings-research-limit-field`). +Two groups deliberately keep their bespoke markup because they are not plain label+control+help rows: the Enabled Sources grid pairs an always-on locked Web Search row with a checkbox grid (per-source default hints ride "?" tips beside each label), and the limits grid lays four numeric fields plus a shared validation error out side by side (`settings-research-limit-field`). FNXC:SettingsHelp 2026-07-15-21:40: Each limits field is still one control with one help string, so its "Default: N." hangs off the same "?" as the migrated toggle above (`.settings-field-label-row` + `SettingsHelpTip`) rather than printing four paragraphs under a section whose other row hides its help behind an icon. -Two things stay inline: the shared limits validation error (a message the operator has to open a tip to find is one they will not see) and the Enabled Sources copy — the always-on Web Search note points at another section, and the per-source hints annotate a checkbox grid rather than describe one control. + +FNXC:SettingsHelp 2026-07-16-12:45: +The Enabled Sources copy now rides the same "?" too — operator requirement: no inline description paragraphs in Settings. The always-on Web Search note and each per-source "Default: …" hint hang off a tip beside that option's label (settingKey = the input id); only the "Always on" span stays inline, because it is a status tag, not help. +The one thing still inline is the shared limits validation error — a message the operator has to open a tip to find is one they will not see. */ export function ResearchProjectSection({ form, setForm, researchLimitError }: ResearchProjectSectionProps) { const { t } = useTranslation("app"); @@ -46,18 +49,22 @@ export function ResearchProjectSection({ form, setForm, researchLimitError }: Re />
- - {t("settings.researchProject.webSearchIsAlwaysEnabledConfigureTheSearch", " Web search is always enabled. Configure the search provider under Research Defaults. ")} + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline source hints moved behind the shared "?" affordance beside each option's label — operator requirement: no inline description paragraphs in Settings. The tip is a SIBLING of the checkbox label (a button inside a label breaks click-to-toggle); the "Always on" span stays inline as a status tag. */} +
+ + {t("settings.researchProject.webSearchIsAlwaysEnabledConfigureTheSearch", " Web search is always enabled. Configure the search provider under Research Defaults. ")} +
{[ ["pageFetch", t("settings.researchProject.pageFetch", "Page Fetch"), "Default: enabled."], ["github", t("settings.researchProject.github", "GitHub"), "Default: disabled."], ["localDocs", t("settings.researchProject.localDocs", "Local Docs"), "Default: enabled."], ["llmSynthesis", t("settings.researchProject.llmSynthesis", "LLM Synthesis"), "Default: enabled."], - ].map(([key, label, defaultHint]) => (
diff --git a/packages/dashboard/app/components/settings/sections/SchedulingSection.tsx b/packages/dashboard/app/components/settings/sections/SchedulingSection.tsx index f348b1f907..43e7009863 100644 --- a/packages/dashboard/app/components/settings/sections/SchedulingSection.tsx +++ b/packages/dashboard/app/components/settings/sections/SchedulingSection.tsx @@ -3,6 +3,7 @@ import { MovedSettingsStub } from "./MovedSettingsStub"; import { SettingsToggleRow } from "../SettingsToggleRow"; import { SettingsSelectRow } from "../SettingsSelectRow"; import { SettingsNumberRow } from "../SettingsNumberRow"; +import { SettingsHelpTip } from "../SettingsHelpTip"; import type { SettingsFormState, SetSettingsForm } from "./context"; const MS_PER_DAY = 24 * 60 * 60 * 1000; const AUTO_ARCHIVE_DEFAULT_AFTER_DAYS = 2; @@ -26,7 +27,7 @@ The machine-wide cap (`globalMaxConcurrent`) moved to SchedulingGlobalSection, a Rows keep their per-row `scope` badge even though the section is now uniformly project-scoped: search can land an operator on a single control with no section chrome in view, so the badge is the only scope signal at that moment. FNXC:SettingsStyling 2026-07-15-17:35: -The `overlapIgnorePaths` allowlist deliberately keeps its bespoke markup: it is a repeating row editor with per-row Browse/Remove buttons, and its help interleaves `t()` fragments with `` elements, which a single-string descriptor `help` cannot express. +The `overlapIgnorePaths` allowlist deliberately keeps its bespoke markup: it is a repeating row editor with per-row Browse/Remove buttons, so no shared row primitive fits. Its help interleaves `t()` fragments with `` elements, which a single-string descriptor `help` cannot express — but SettingsHelpTip takes ReactNode, so that copy now lives behind the shared "?" affordance instead of an inline ``. */ export function SchedulingSection({ form, setForm, concurrencyLoading = false, onOverlapIgnorePathChange, onOpenOverlapPathPicker, onRemoveOverlapIgnorePath, onAddOverlapIgnorePath, onOpenWorkflowSettings, }: SchedulingSectionProps) { const { t } = useTranslation("app"); @@ -301,9 +302,11 @@ export function SchedulingSection({ form, setForm, concurrencyLoading = false, o />
- - {t("settings.scheduling.optionalFileOrDirectoryPathsToIgnoreWhen", " No default \u2014 unset (empty). Optional file or directory paths to ignore when overlap serialization is enabled. Paths are project-relative (for example ")}docs/{t("settings.scheduling.or", " or ")}generated/*{t("settings.scheduling.closeParenPeriod", ").")} - + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance \u2014 operator requirement: no inline description paragraphs in Settings. The tip is a SIBLING of the label (a button inside a label is invalid), wrapped with it in `.settings-field-label-row`; the `` fragments ride along verbatim because SettingsHelpTip takes ReactNode. */} +
+ + {t("settings.scheduling.optionalFileOrDirectoryPathsToIgnoreWhen", " No default \u2014 unset (empty). Optional file or directory paths to ignore when overlap serialization is enabled. Paths are project-relative (for example ")}docs/{t("settings.scheduling.or", " or ")}generated/*{t("settings.scheduling.closeParenPeriod", ").")} +
{(form.overlapIgnorePaths && form.overlapIgnorePaths.length > 0 ? form.overlapIgnorePaths : [""]).map((path, index) => (
diff --git a/packages/dashboard/app/components/settings/sections/SourceControlGlobalSection.tsx b/packages/dashboard/app/components/settings/sections/SourceControlGlobalSection.tsx index 21d9b66112..1f09834103 100644 --- a/packages/dashboard/app/components/settings/sections/SourceControlGlobalSection.tsx +++ b/packages/dashboard/app/components/settings/sections/SourceControlGlobalSection.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { TrackingRepoSelect, type TrackingRepoOption } from "../../TrackingRepoSelect"; import { SettingsSelectRow } from "../SettingsSelectRow"; import { SettingsTextRow } from "../SettingsTextRow"; +import { SettingsHelpTip } from "../SettingsHelpTip"; import type { SectionBaseProps } from "./context"; type GlobalGitlabSettings = Pick; @@ -34,10 +35,13 @@ export function SourceControlGlobalSection({ form, setForm, globalSettings, onGl */ const globalGitlab = globalSettings ?? form; return (<> + {/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance beside the label — operator requirement: no inline description paragraphs in Settings. The bespoke TrackingRepoSelect widget is no obstacle: the tip belongs to the label line, not the control. */}
- +
+ + {t("settings.globalGeneral.projectsInheritThisValueWhenTheyDoNot", "Projects inherit this value when they do not set a project default tracking repo. No default — unset.")} +
setForm((f) => ({ ...f, githubTrackingDefaultRepo: nextValue || undefined }))}/> - {t("settings.globalGeneral.projectsInheritThisValueWhenTheyDoNot", "Projects inherit this value when they do not set a project default tracking repo. No default — unset.")}
{/* FNXC:GitLabEnablement 2026-07-02-00:00: @@ -50,8 +54,15 @@ export function SourceControlGlobalSection({ form, setForm, globalSettings, onGl onGlobalGitlabSettingsChange({ gitlabEnabled: e.target.checked })}/> {t("settings.globalGeneral.enableGitLabIntegration", "Enable GitLab integration")} + {/* + FNXC:SettingsHelp 2026-07-16-12:45: + Inline disclosure hint moved behind the shared "?" affordance beside the summary title — operator requirement: no inline description paragraphs in Settings. + The copy stays conditional on `gitlabEnabled` inside the tip. The wrapping span stops propagation, same as the summary's checkbox label, so opening the tip never toggles the disclosure. + */} + event.stopPropagation()}> + {globalGitlab.gitlabEnabled === false ? t("settings.globalGeneral.gitLabDisabledHint", "GitLab API operations are disabled by global default. Saved URL and token fallbacks remain stored for re-enable.") : t("settings.globalGeneral.gitLabEnabledHint", "Global GitLab URL and token fallbacks apply to projects that do not set their own values. No default — unset (unset behaves as enabled until explicitly disabled).")} + - {globalGitlab.gitlabEnabled === false ? t("settings.globalGeneral.gitLabDisabledHint", "GitLab API operations are disabled by global default. Saved URL and token fallbacks remain stored for re-enable.") : t("settings.globalGeneral.gitLabEnabledHint", "Global GitLab URL and token fallbacks apply to projects that do not set their own values. No default — unset (unset behaves as enabled until explicitly disabled).")}
setForm((f) => ({ ...f, gitlabEnabled: e.target.checked }))}/> {t("settings.general.enableGitLabIntegration", "Enable GitLab integration")} + {/* + FNXC:SettingsHelp 2026-07-16-12:45: + Inline disclosure hint moved behind the shared "?" affordance beside the summary title — operator requirement: no inline description paragraphs in Settings. + The copy stays conditional on `gitlabEnabled` (the tip's ReactNode children carry it verbatim). The wrapping span stops propagation, same as the summary's checkbox label, so opening the tip never toggles the disclosure. + */} + event.stopPropagation()}> + {form.gitlabEnabled === false ? t("settings.general.gitLabDisabledHint", "GitLab API imports, comments, close/reopen, and refresh operations are disabled. Saved URLs and tokens remain stored for re-enable.") : t("settings.general.gitLabEnabledHint", "Configure GitLab.com or self-managed GitLab URLs. Blank values inherit global fallbacks and then GitLab.com. No default — unset (unset behaves as enabled until explicitly disabled).")} + - {form.gitlabEnabled === false ? t("settings.general.gitLabDisabledHint", "GitLab API imports, comments, close/reopen, and refresh operations are disabled. Saved URLs and tokens remain stored for re-enable.") : t("settings.general.gitLabEnabledHint", "Configure GitLab.com or self-managed GitLab URLs. Blank values inherit global fallbacks and then GitLab.com. No default — unset (unset behaves as enabled until explicitly disabled).")}
{t("settings.merge.gitLabAuthentication", "GitLab Authentication")} - {form.gitlabEnabled === false ? t("settings.merge.gitLabDisabledHint", "GitLab comments, close/reopen, import fetches, and refresh operations are disabled. Saved tokens remain stored for re-enable.") : t("settings.merge.gitLabAuthDetails", "Fusion uses GitLab REST API token authentication with the PRIVATE-TOKEN header. Leave the token blank to clear the project override and fall back to a configured global GitLab token or GITLAB_TOKEN where available. No default — unset (unset behaves as enabled until explicitly disabled).")} +
+
{t("settings.merge.gitLabAuthentication", "GitLab Authentication")}
+ {form.gitlabEnabled === false ? t("settings.merge.gitLabDisabledHint", "GitLab comments, close/reopen, import fetches, and refresh operations are disabled. Saved tokens remain stored for re-enable.") : t("settings.merge.gitLabAuthDetails", "Fusion uses GitLab REST API token authentication with the PRIVATE-TOKEN header. Leave the token blank to clear the project override and fall back to a configured global GitLab token or GITLAB_TOKEN where available. No default — unset (unset behaves as enabled until explicitly disabled).")} +