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 <small> description across settings sections to the shared SettingsHelpTip "?" affordance (validation errors and live status stay inline). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/settings-help-icons-checkbox-theming.md
Normal file
7
.changeset/settings-help-icons-checkbox-theming.md
Normal file
@@ -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 `<small>` help across settings sections migrated to SettingsHelpTip.
|
||||||
@@ -1047,6 +1047,31 @@ Mirrors `.settings-field-row-head` (same gap, same wrap) so the two idioms produ
|
|||||||
line-height: 1.35;
|
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:
|
FNXC:SettingsStyling 2026-07-15-18:52:
|
||||||
Every text-entry control in Settings resolves to ONE appearance, whatever markup it sits in.
|
Every text-entry control in Settings resolves to ONE appearance, whatever markup it sits in.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
type DashboardShortcutAction,
|
type DashboardShortcutAction,
|
||||||
} from "../utils/keyboardShortcuts";
|
} from "../utils/keyboardShortcuts";
|
||||||
import type { DashboardKeyboardShortcutMap } from "../utils/keyboardShortcuts";
|
import type { DashboardKeyboardShortcutMap } from "../utils/keyboardShortcuts";
|
||||||
|
import { SettingsHelpTip } from "./settings/SettingsHelpTip";
|
||||||
import type { SectionSaveHandler } from "./settings/sections/context";
|
import type { SectionSaveHandler } from "./settings/sections/context";
|
||||||
import { AppearanceSection } from "./settings/sections/AppearanceSection";
|
import { AppearanceSection } from "./settings/sections/AppearanceSection";
|
||||||
import { ExperimentalSection } from "./settings/sections/ExperimentalSection";
|
import { ExperimentalSection } from "./settings/sections/ExperimentalSection";
|
||||||
@@ -4708,16 +4709,19 @@ export function SettingsModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="import-merge" className="checkbox-label">
|
{/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. */}
|
||||||
<input
|
<div className="settings-field-label-row">
|
||||||
id="import-merge"
|
<label htmlFor="import-merge" className="checkbox-label">
|
||||||
type="checkbox"
|
<input
|
||||||
checked={importMerge}
|
id="import-merge"
|
||||||
onChange={(e) => setImportMerge(e.target.checked)}
|
type="checkbox"
|
||||||
/>
|
checked={importMerge}
|
||||||
{t("settings.importExport.mergeExisting", "Merge with existing settings (recommended)")}
|
onChange={(e) => setImportMerge(e.target.checked)}
|
||||||
</label>
|
/>
|
||||||
<small>{t("settings.importExport.replaceWarning", "If unchecked, existing settings will be replaced with imported values.")}</small>
|
{t("settings.importExport.mergeExisting", "Merge with existing settings (recommended)")}
|
||||||
|
</label>
|
||||||
|
<SettingsHelpTip settingKey="import-merge">{t("settings.importExport.replaceWarning", "If unchecked, existing settings will be replaced with imported values.")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-actions">
|
<div className="modal-actions">
|
||||||
|
|||||||
@@ -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 permanent agents" })).not.toBeChecked();
|
||||||
expect(screen.getByRole("checkbox", { name: "Save AI thinking for ephemeral / task-worker 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 <small>.
|
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 <small> — 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();
|
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);
|
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();
|
expect(toolOutputHelper.closest(".settings-help-bubble")).toBeTruthy();
|
||||||
const thinkingHelper = screen.getByText(/Leave both thinking toggles off to keep the original default behavior/i);
|
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -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.
|
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.
|
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).
|
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) {
|
@media (max-width: 768px) {
|
||||||
.settings-field-row,
|
.settings-field-row,
|
||||||
.settings-content .form-group {
|
.settings-content .form-group,
|
||||||
|
.settings-field-label-row,
|
||||||
|
.notification-provider-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { AgentPermissionPolicy, AgentPermissionPolicyRules } from "@fusion/
|
|||||||
import { AgentPermissionPolicyEditor } from "../../AgentPermissionPolicyEditor";
|
import { AgentPermissionPolicyEditor } from "../../AgentPermissionPolicyEditor";
|
||||||
import { AgentProvisioningPolicyEditor } from "../../AgentProvisioningPolicyEditor";
|
import { AgentProvisioningPolicyEditor } from "../../AgentProvisioningPolicyEditor";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
function toCompleteAgentPermissionRules(rules?: Partial<AgentPermissionPolicyRules>): AgentPermissionPolicyRules {
|
function toCompleteAgentPermissionRules(rules?: Partial<AgentPermissionPolicyRules>): AgentPermissionPolicyRules {
|
||||||
return AGENT_PERMISSION_POLICY_ACTION_CATEGORIES.reduce((acc, category) => {
|
return AGENT_PERMISSION_POLICY_ACTION_CATEGORIES.reduce((acc, category) => {
|
||||||
@@ -14,18 +15,19 @@ export type AgentPermissionsSectionProps = SectionBaseProps;
|
|||||||
export function AgentPermissionsSection({ form, setForm }: AgentPermissionsSectionProps) {
|
export function AgentPermissionsSection({ form, setForm }: AgentPermissionsSectionProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
return (<>
|
return (<>
|
||||||
<h4 className="settings-section-heading">{t("settings.agentPermissions.agentPermissions", "Agent Permissions")}</h4>
|
{/* 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. */}
|
||||||
<div className="form-group">
|
<div className="settings-field-label-row">
|
||||||
<small className="settings-muted">{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.")}</small>
|
<h4 className="settings-section-heading">{t("settings.agentPermissions.agentPermissions", "Agent Permissions")}</h4>
|
||||||
|
<SettingsHelpTip settingKey="defaultAgentPermissionPolicy">{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.")}</SettingsHelpTip>
|
||||||
</div>
|
</div>
|
||||||
<AgentPermissionPolicyEditor mode="project-default" value={form.defaultAgentPermissionPolicy ? { presetId: "custom", rules: toCompleteAgentPermissionRules(form.defaultAgentPermissionPolicy.rules), ...(form.defaultAgentPermissionPolicy.toolRules ? { toolRules: form.defaultAgentPermissionPolicy.toolRules } : {}) } as AgentPermissionPolicy : { presetId: "custom", rules: toCompleteAgentPermissionRules() }} onChange={(next) => setForm((f) => ({
|
<AgentPermissionPolicyEditor mode="project-default" value={form.defaultAgentPermissionPolicy ? { presetId: "custom", rules: toCompleteAgentPermissionRules(form.defaultAgentPermissionPolicy.rules), ...(form.defaultAgentPermissionPolicy.toolRules ? { toolRules: form.defaultAgentPermissionPolicy.toolRules } : {}) } as AgentPermissionPolicy : { presetId: "custom", rules: toCompleteAgentPermissionRules() }} onChange={(next) => setForm((f) => ({
|
||||||
...f,
|
...f,
|
||||||
defaultAgentPermissionPolicy: { rules: toCompleteAgentPermissionRules(next?.rules), ...(next?.toolRules ? { toolRules: next.toolRules } : {}) },
|
defaultAgentPermissionPolicy: { rules: toCompleteAgentPermissionRules(next?.rules), ...(next?.toolRules ? { toolRules: next.toolRules } : {}) },
|
||||||
}))}/>
|
}))}/>
|
||||||
|
|
||||||
<h4 className="settings-section-heading">{t("settings.agentPermissions.agentProvisioningApprovals", "Agent Provisioning Approvals")}</h4>
|
<div className="settings-field-label-row">
|
||||||
<div className="form-group">
|
<h4 className="settings-section-heading">{t("settings.agentPermissions.agentProvisioningApprovals", "Agent Provisioning Approvals")}</h4>
|
||||||
<small className="settings-muted">{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). ")}</small>
|
<SettingsHelpTip settingKey="agentProvisioning">{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). ")}</SettingsHelpTip>
|
||||||
</div>
|
</div>
|
||||||
<AgentProvisioningPolicyEditor value={form.agentProvisioning} onChange={(next) => setForm((f) => ({ ...f, agentProvisioning: next }))}/>
|
<AgentProvisioningPolicyEditor value={form.agentProvisioning} onChange={(next) => setForm((f) => ({ ...f, agentProvisioning: next }))}/>
|
||||||
</>);
|
</>);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { LoginInstructions } from "../../LoginInstructions";
|
|||||||
import { LoadingSpinner } from "../../LoadingSpinner";
|
import { LoadingSpinner } from "../../LoadingSpinner";
|
||||||
import { OAuthManualCodeForm } from "../../OAuthManualCodeForm";
|
import { OAuthManualCodeForm } from "../../OAuthManualCodeForm";
|
||||||
import { CustomProvidersSection } from "../../CustomProvidersSection";
|
import { CustomProvidersSection } from "../../CustomProvidersSection";
|
||||||
|
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||||
import { copyTextToClipboard } from "../../../utils/copyToClipboard";
|
import { copyTextToClipboard } from "../../../utils/copyToClipboard";
|
||||||
import { appendTokenQuery } from "../../../auth";
|
import { appendTokenQuery } from "../../../auth";
|
||||||
import { refreshModelsCache } from "../../../hooks/useModelsCache";
|
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`.
|
Only `type: "api_key"` cards show key controls so OAuth logout never looks like it will clear `ANTHROPIC_API_KEY`.
|
||||||
*/
|
*/
|
||||||
return (<>
|
return (<>
|
||||||
<h4 className="settings-section-heading">{t("settings.auth.title", "Authentication")}</h4>
|
{/* 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. */}
|
||||||
|
<div className="settings-field-label-row">
|
||||||
|
<h4 className="settings-section-heading">{t("settings.auth.title", "Authentication")}</h4>
|
||||||
|
<SettingsHelpTip settingKey="auth-section">{t("settings.auth.hint", "Authentication changes take effect immediately — no need to save.")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
{authLoading ? (<div className="settings-empty-state"><LoadingSpinner label={t("settings.auth.loadingStatus", "Loading authentication status…")} /></div>) : authProviders.length === 0 ? (<div className="settings-empty-state settings-muted">
|
{authLoading ? (<div className="settings-empty-state"><LoadingSpinner label={t("settings.auth.loadingStatus", "Loading authentication status…")} /></div>) : authProviders.length === 0 ? (<div className="settings-empty-state settings-muted">
|
||||||
{t("settings.auth.noProviders", "No providers available")}
|
{t("settings.auth.noProviders", "No providers available")}
|
||||||
</div>) : (<div className="auth-panel-body">
|
</div>) : (<div className="auth-panel-body">
|
||||||
@@ -257,19 +262,18 @@ export function AuthenticationSection({ auth }: AuthenticationSectionProps) {
|
|||||||
</div>)}
|
</div>)}
|
||||||
</div>)}
|
</div>)}
|
||||||
{/*
|
{/*
|
||||||
FNXC:SettingsHelp 2026-07-15-21:40:
|
FNXC:SettingsHelp 2026-07-16-12:45:
|
||||||
No `<small>` in this section moved behind the shared "?" help affordance, and none should. This section has no settings rows: it renders provider CARDS, whose `<small>`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.
|
The provider cards' `<small>`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.
|
||||||
*/}
|
*/}
|
||||||
<small className="auth-hint">
|
|
||||||
{t("settings.auth.hint", "Authentication changes take effect immediately — no need to save.")}
|
|
||||||
</small>
|
|
||||||
{onReopenOnboarding && (<div className="form-group" style={{ marginTop: "var(--space-md)" }}>
|
{onReopenOnboarding && (<div className="form-group" style={{ marginTop: "var(--space-md)" }}>
|
||||||
<button type="button" className="btn btn-sm" onClick={onReopenOnboarding}>
|
<div className="settings-field-label-row">
|
||||||
{t("settings.auth.reopenOnboarding", "Reopen onboarding guide")}
|
<button type="button" className="btn btn-sm" onClick={onReopenOnboarding}>
|
||||||
</button>
|
{t("settings.auth.reopenOnboarding", "Reopen onboarding guide")}
|
||||||
<small className="settings-muted">
|
</button>
|
||||||
{t("settings.auth.reopenOnboardingHint", "Re-run the setup wizard to review or update your AI provider and model configuration.")}
|
<SettingsHelpTip settingKey="reopen-onboarding">
|
||||||
</small>
|
{t("settings.auth.reopenOnboardingHint", "Re-run the setup wizard to review or update your AI provider and model configuration.")}
|
||||||
|
</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
</div>)}
|
</div>)}
|
||||||
|
|
||||||
<CustomProvidersSection />
|
<CustomProvidersSection />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||||
export interface ExperimentalSectionProps extends SectionBaseProps {
|
export interface ExperimentalSectionProps extends SectionBaseProps {
|
||||||
/** Display labels for well-known features (always rendered). */
|
/** Display labels for well-known features (always rendered). */
|
||||||
knownFeatures: Record<string, string>;
|
knownFeatures: Record<string, string>;
|
||||||
@@ -25,9 +26,13 @@ export function ExperimentalSection({ form, setForm, knownFeatures, legacyAliase
|
|||||||
])).filter((key) => !hiddenFeatureKeys?.has(key)).sort((a, b) => a.localeCompare(b));
|
])).filter((key) => !hiddenFeatureKeys?.has(key)).sort((a, b) => a.localeCompare(b));
|
||||||
const featureFlags = allFeatureKeys.map((key) => [key, isFeatureEnabled(experimentalFeatures, key)] as const);
|
const featureFlags = allFeatureKeys.map((key) => [key, isFeatureEnabled(experimentalFeatures, key)] as const);
|
||||||
return (<>
|
return (<>
|
||||||
<h4 className="settings-section-heading">{t("settings.experimental.experimentalFeatures", "Experimental Features")}</h4>
|
{/*
|
||||||
<div className="form-group">
|
FNXC:SettingsHelp 2026-07-16-12:45:
|
||||||
<small>{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. ")}</small>
|
Section intro moved behind the shared "?" beside the heading - operator requirement: no inline description paragraphs in Settings.
|
||||||
|
*/}
|
||||||
|
<div className="settings-field-label-row">
|
||||||
|
<h4 className="settings-section-heading">{t("settings.experimental.experimentalFeatures", "Experimental Features")}</h4>
|
||||||
|
<SettingsHelpTip settingKey="experimental-section">{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. ")}</SettingsHelpTip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { SettingsToggleRow } from "../SettingsToggleRow";
|
|||||||
import { SettingsSelectRow } from "../SettingsSelectRow";
|
import { SettingsSelectRow } from "../SettingsSelectRow";
|
||||||
import { SettingsNumberRow } from "../SettingsNumberRow";
|
import { SettingsNumberRow } from "../SettingsNumberRow";
|
||||||
import { SettingsTextRow } from "../SettingsTextRow";
|
import { SettingsTextRow } from "../SettingsTextRow";
|
||||||
|
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||||
/*
|
/*
|
||||||
FNXC:GitHubImportTranslate 2026-07-15-09:30:
|
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.
|
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.
|
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 `<code>` (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.
|
Rows that stay bespoke are the ones a single-string descriptor cannot carry without rewording the copy — help built from `t()` fragments interleaved with `<code>` (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 `<small>` paragraphs. Their copy moved VERBATIM (same `t()` keys, same `<code>` 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:
|
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.
|
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
|
|||||||
/>
|
/>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<ProjectDefaultWorkflowField projectId={projectId} addToast={addToast}/>
|
<ProjectDefaultWorkflowField projectId={projectId} addToast={addToast}/>
|
||||||
<small>{t("settings.general.newTasksInheritThisCustomWorkflowsStepsOverridable", "New tasks inherit this custom workflow's steps (overridable per task). No default \u2014 unset (built-in default workflow).")}</small>
|
{/*
|
||||||
|
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 `<label>`, never nested in one.
|
||||||
|
*/}
|
||||||
|
<SettingsHelpTip settingKey="projectDefaultWorkflow">{t("settings.general.newTasksInheritThisCustomWorkflowsStepsOverridable", "New tasks inherit this custom workflow's steps (overridable per task). No default \u2014 unset (built-in default workflow).")}</SettingsHelpTip>
|
||||||
</div>
|
</div>
|
||||||
{builtinWorkflows.length > 0 && (<div className="form-group">
|
{builtinWorkflows.length > 0 && (<div className="form-group">
|
||||||
<label>{t("settings.general.fusionWorkflows", "Fusion workflows")}</label>
|
{/* 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. */}
|
||||||
|
<div className="settings-field-label-row">
|
||||||
|
<label>{t("settings.general.fusionWorkflows", "Fusion workflows")}</label>
|
||||||
|
<SettingsHelpTip settingKey="enabledBuiltinWorkflowIds">{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).")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
|
||||||
{builtinWorkflows.map((workflow) => (<label key={workflow.id} htmlFor={`builtin-workflow-${workflow.id}`} className="checkbox-label">
|
{builtinWorkflows.map((workflow) => (<label key={workflow.id} htmlFor={`builtin-workflow-${workflow.id}`} className="checkbox-label">
|
||||||
<input id={`builtin-workflow-${workflow.id}`} type="checkbox" checked={enabledBuiltinWorkflowIds.has(workflow.id)} onChange={(e) => setBuiltinWorkflowEnabled(workflow.id, e.target.checked)}/>
|
<input id={`builtin-workflow-${workflow.id}`} type="checkbox" checked={enabledBuiltinWorkflowIds.has(workflow.id)} onChange={(e) => setBuiltinWorkflowEnabled(workflow.id, e.target.checked)}/>
|
||||||
@@ -167,10 +180,13 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError
|
|||||||
<span>{workflow.name}</span>
|
<span>{workflow.name}</span>
|
||||||
</label>))}
|
</label>))}
|
||||||
</div>
|
</div>
|
||||||
<small>{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).")}</small>
|
|
||||||
</div>)}
|
</div>)}
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="aiUndoTaskWorkflowId">{t("settings.general.aiUndoTaskWorkflow", "AI-undo task workflow")}</label>
|
{/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. */}
|
||||||
|
<div className="settings-field-label-row">
|
||||||
|
<label htmlFor="aiUndoTaskWorkflowId">{t("settings.general.aiUndoTaskWorkflow", "AI-undo task workflow")}</label>
|
||||||
|
<SettingsHelpTip settingKey="aiUndoTaskWorkflowId">{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.")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<select id="aiUndoTaskWorkflowId" className="select" data-testid="ai-undo-workflow-select" value={aiUndoTaskWorkflowValue} onChange={(e) => setForm((f) => ({ ...f, aiUndoTaskWorkflowId: e.target.value }))}>
|
<select id="aiUndoTaskWorkflowId" className="select" data-testid="ai-undo-workflow-select" value={aiUndoTaskWorkflowValue} onChange={(e) => setForm((f) => ({ ...f, aiUndoTaskWorkflowId: e.target.value }))}>
|
||||||
<option value="">{t("settings.general.aiUndoTaskWorkflowInherit", "Inherit project default workflow")}</option>
|
<option value="">{t("settings.general.aiUndoTaskWorkflowInherit", "Inherit project default workflow")}</option>
|
||||||
{aiUndoWorkflowOptions.map((workflow) => (<option key={workflow.id} value={workflow.id}>
|
{aiUndoWorkflowOptions.map((workflow) => (<option key={workflow.id} value={workflow.id}>
|
||||||
@@ -178,12 +194,14 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError
|
|||||||
</option>))}
|
</option>))}
|
||||||
{!aiUndoWorkflowHasStoredValue && (<option value={aiUndoTaskWorkflowValue}>{aiUndoTaskWorkflowValue}</option>)}
|
{!aiUndoWorkflowHasStoredValue && (<option value={aiUndoTaskWorkflowValue}>{aiUndoTaskWorkflowValue}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<small>{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.")}</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="ephemeralAgentsEnabled" className="checkbox-label">
|
{/* 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). */}
|
||||||
<input id="ephemeralAgentsEnabled" type="checkbox" checked={form.ephemeralAgentsEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, ephemeralAgentsEnabled: e.target.checked }))}/>{t("settings.general.useEphemeralTaskWorkerAgents", " Use ephemeral task-worker agents ")}</label>
|
<div className="settings-field-label-row">
|
||||||
<small>{t("settings.general.whenEnabledDefaultFusionSpawnsShortLived", " When enabled (default), Fusion spawns short-lived ")}<code>executor-FN-XXXX</code>{t("settings.general.agentsToRunEachTaskWhenDisabledOnly", " agents to run each task. When disabled, only permanent agents execute tasks and the scheduler auto-assigns work using the agent reporting chain. Tasks with no eligible permanent agent stay queued. ")}</small>
|
<label htmlFor="ephemeralAgentsEnabled" className="checkbox-label">
|
||||||
|
<input id="ephemeralAgentsEnabled" type="checkbox" checked={form.ephemeralAgentsEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, ephemeralAgentsEnabled: e.target.checked }))}/>{t("settings.general.useEphemeralTaskWorkerAgents", " Use ephemeral task-worker agents ")}</label>
|
||||||
|
<SettingsHelpTip settingKey="ephemeralAgentsEnabled">{t("settings.general.whenEnabledDefaultFusionSpawnsShortLived", " When enabled (default), Fusion spawns short-lived ")}<code>executor-FN-XXXX</code>{t("settings.general.agentsToRunEachTaskWhenDisabledOnly", " agents to run each task. When disabled, only permanent agents execute tasks and the scheduler auto-assigns work using the agent reporting chain. Tasks with no eligible permanent agent stay queued. ")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/*
|
{/*
|
||||||
FNXC:EphemeralAgentTaskCreation 2026-07-01-00:00:
|
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 }))}
|
onChange={(v) => setForm((f) => ({ ...f, allowAbsoluteFileBrowserPaths: v === true }))}
|
||||||
/>
|
/>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="completionDocumentationMode">{t("settings.general.completionDocumentationAutomation", "Completion Documentation Automation")}</label>
|
{/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. */}
|
||||||
|
<div className="settings-field-label-row">
|
||||||
|
<label htmlFor="completionDocumentationMode">{t("settings.general.completionDocumentationAutomation", "Completion Documentation Automation")}</label>
|
||||||
|
<SettingsHelpTip settingKey="completionDocumentationMode">{t("settings.general.controlsHowFutureTaskSpecsHandleReleaseNote", " Controls how future task specs handle release-note artifacts at completion. Use changeset mode for repositories that follow ")}<code>.changeset</code>{t("settings.general.workflowsOrChangelogModeWhenContributorsShouldUpdate", " workflows, or changelog mode when contributors should update an existing changelog file. Default: off. ")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<select id="completionDocumentationMode" value={form.completionDocumentationMode || "off"} onChange={(e) => setForm((f) => ({
|
<select id="completionDocumentationMode" value={form.completionDocumentationMode || "off"} onChange={(e) => setForm((f) => ({
|
||||||
...f,
|
...f,
|
||||||
completionDocumentationMode: e.target.value as "off" | "changeset" | "changelog",
|
completionDocumentationMode: e.target.value as "off" | "changeset" | "changelog",
|
||||||
@@ -240,7 +262,6 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError
|
|||||||
<option value="changeset">{t("settings.general.requireChangesetChangesetMd", "Require changeset (.changeset/*.md)")}</option>
|
<option value="changeset">{t("settings.general.requireChangesetChangesetMd", "Require changeset (.changeset/*.md)")}</option>
|
||||||
<option value="changelog">{t("settings.general.requireChangelogUpdateExistingChangelog", "Require changelog update (existing changelog)")}</option>
|
<option value="changelog">{t("settings.general.requireChangelogUpdateExistingChangelog", "Require changelog update (existing changelog)")}</option>
|
||||||
</select>
|
</select>
|
||||||
<small>{t("settings.general.controlsHowFutureTaskSpecsHandleReleaseNote", " Controls how future task specs handle release-note artifacts at completion. Use changeset mode for repositories that follow ")}<code>.changeset</code>{t("settings.general.workflowsOrChangelogModeWhenContributorsShouldUpdate", " workflows, or changelog mode when contributors should update an existing changelog file. Default: off. ")}</small>
|
|
||||||
</div>
|
</div>
|
||||||
{/*
|
{/*
|
||||||
FNXC:SettingsGeneral 2026-07-15-17:35:
|
FNXC:SettingsGeneral 2026-07-15-17:35:
|
||||||
@@ -522,8 +543,11 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError
|
|||||||
*/}
|
*/}
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.general.browserData", "Browser Data")}</h4>
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.general.browserData", "Browser Data")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>{t("settings.general.clearLocalData", "Clear local data")}</label>
|
{/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. */}
|
||||||
<small>{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.")}</small>
|
<div className="settings-field-label-row">
|
||||||
|
<label>{t("settings.general.clearLocalData", "Clear local data")}</label>
|
||||||
|
<SettingsHelpTip settingKey="clearLocalData">{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.")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<div style={{ marginTop: "var(--space-sm)" }}>
|
<div style={{ marginTop: "var(--space-sm)" }}>
|
||||||
<button type="button" className="btn btn-sm" onClick={handleClearLocalData}>{t("settings.general.clearLocalDataButton", "Clear local data")}</button>
|
<button type="button" className="btn btn-sm" onClick={handleClearLocalData}>{t("settings.general.clearLocalDataButton", "Clear local data")}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { resolvePersistAgentThinkingLog } from "@fusion/core";
|
import { resolvePersistAgentThinkingLog } from "@fusion/core";
|
||||||
import { SettingsToggleRow } from "../SettingsToggleRow";
|
import { SettingsToggleRow } from "../SettingsToggleRow";
|
||||||
import { SettingsSelectRow } from "../SettingsSelectRow";
|
import { SettingsSelectRow } from "../SettingsSelectRow";
|
||||||
|
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
export type GlobalGeneralSectionProps = SectionBaseProps;
|
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.
|
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.
|
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 `<code>` 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.
|
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 `<code>` 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 `<small>` paragraphs. The copy moved VERBATIM (same `t()` keys, same `<code>` 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) {
|
export function GlobalGeneralSection({ form, setForm }: GlobalGeneralSectionProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
@@ -43,26 +47,35 @@ export function GlobalGeneralSection({ form, setForm }: GlobalGeneralSectionProp
|
|||||||
onChange={(v) => setForm((f) => ({ ...f, persistAgentToolOutput: v === true }))}
|
onChange={(v) => setForm((f) => ({ ...f, persistAgentToolOutput: v === true }))}
|
||||||
/>
|
/>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<h5 className="settings-section-heading">{t("settings.globalGeneral.saveAIThinkingLogs", "Save AI thinking logs")}</h5>
|
{/* 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. */}
|
||||||
|
<div className="settings-field-label-row">
|
||||||
|
<h5 className="settings-section-heading">{t("settings.globalGeneral.saveAIThinkingLogs", "Save AI thinking logs")}</h5>
|
||||||
|
<SettingsHelpTip settingKey="persistAgentThinkingLog">{t("settings.globalGeneral.leaveBothThinkingTogglesOffToKeepThe", " Leave both thinking toggles off to keep the original default behavior. This only controls persisted ")}<code>thinking</code>{t("settings.globalGeneral.rowsAndDoesNotAffectAssistantTextOr", " rows and does not affect assistant text or tool rows. Default: disabled for both permanent and ephemeral agents. ")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<label htmlFor="persistAgentThinkingLogPermanent" className="checkbox-label">
|
<label htmlFor="persistAgentThinkingLogPermanent" className="checkbox-label">
|
||||||
<input id="persistAgentThinkingLogPermanent" type="checkbox" checked={resolvePersistAgentThinkingLog(form, { ephemeral: false })} onChange={(e) => setForm((f) => ({ ...f, persistAgentThinkingLogPermanent: e.target.checked }))}/>{t("settings.globalGeneral.saveAIThinkingForPermanentAgents", " Save AI thinking for permanent agents ")}</label>
|
<input id="persistAgentThinkingLogPermanent" type="checkbox" checked={resolvePersistAgentThinkingLog(form, { ephemeral: false })} onChange={(e) => setForm((f) => ({ ...f, persistAgentThinkingLogPermanent: e.target.checked }))}/>{t("settings.globalGeneral.saveAIThinkingForPermanentAgents", " Save AI thinking for permanent agents ")}</label>
|
||||||
<label htmlFor="persistAgentThinkingLogEphemeral" className="checkbox-label">
|
<label htmlFor="persistAgentThinkingLogEphemeral" className="checkbox-label">
|
||||||
<input id="persistAgentThinkingLogEphemeral" type="checkbox" checked={resolvePersistAgentThinkingLog(form, { ephemeral: true })} onChange={(e) => setForm((f) => ({ ...f, persistAgentThinkingLogEphemeral: e.target.checked }))}/>{t("settings.globalGeneral.saveAIThinkingForEphemeralTaskWorkerAgents", " Save AI thinking for ephemeral / task-worker agents ")}</label>
|
<input id="persistAgentThinkingLogEphemeral" type="checkbox" checked={resolvePersistAgentThinkingLog(form, { ephemeral: true })} onChange={(e) => setForm((f) => ({ ...f, persistAgentThinkingLogEphemeral: e.target.checked }))}/>{t("settings.globalGeneral.saveAIThinkingForEphemeralTaskWorkerAgents", " Save AI thinking for ephemeral / task-worker agents ")}</label>
|
||||||
<small>{t("settings.globalGeneral.leaveBothThinkingTogglesOffToKeepThe", " Leave both thinking toggles off to keep the original default behavior. This only controls persisted ")}<code>thinking</code>{t("settings.globalGeneral.rowsAndDoesNotAffectAssistantTextOr", " rows and does not affect assistant text or tool rows. Default: disabled for both permanent and ephemeral agents. ")}</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="fnBinaryCheckEnabled" className="checkbox-label">
|
{/* 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). */}
|
||||||
<input id="fnBinaryCheckEnabled" type="checkbox" checked={form.fnBinaryCheckEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, fnBinaryCheckEnabled: e.target.checked }))}/>{t("settings.globalGeneral.checkForThe", " Check for the ")}<code>fn</code>{t("settings.globalGeneral.cLIBinaryOnPATH", " CLI binary on PATH ")}</label>
|
<div className="settings-field-label-row">
|
||||||
<small>{t("settings.globalGeneral.whenEnabledTheDashboardProbesForAGlobally", " When enabled, the dashboard probes for a globally-installed")}{" "}
|
<label htmlFor="fnBinaryCheckEnabled" className="checkbox-label">
|
||||||
<code>fn</code> / <code>fusion</code>{t("settings.globalGeneral.cLIBySpawning", " CLI by spawning")}{" "}
|
<input id="fnBinaryCheckEnabled" type="checkbox" checked={form.fnBinaryCheckEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, fnBinaryCheckEnabled: e.target.checked }))}/>{t("settings.globalGeneral.checkForThe", " Check for the ")}<code>fn</code>{t("settings.globalGeneral.cLIBinaryOnPATH", " CLI binary on PATH ")}</label>
|
||||||
<code><bin> --version</code>{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. ")}</small>
|
<SettingsHelpTip settingKey="fnBinaryCheckEnabled">{t("settings.globalGeneral.whenEnabledTheDashboardProbesForAGlobally", " When enabled, the dashboard probes for a globally-installed")}{" "}
|
||||||
|
<code>fn</code> / <code>fusion</code>{t("settings.globalGeneral.cLIBySpawning", " CLI by spawning")}{" "}
|
||||||
|
<code><bin> --version</code>{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. ")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.globalGeneral.updates", "Updates")}</h4>
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.globalGeneral.updates", "Updates")}</h4>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="updateCheckEnabled" className="checkbox-label">
|
{/* 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). */}
|
||||||
<input id="updateCheckEnabled" type="checkbox" checked={form.updateCheckEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, updateCheckEnabled: e.target.checked }))}/>{t("settings.globalGeneral.checkForUpdatesAutomatically", " Check for updates automatically ")}</label>
|
<div className="settings-field-label-row">
|
||||||
<small>{t("settings.globalGeneral.whenEnabledFusionChecksNpmForNewVersions", " When enabled, Fusion checks npm for new versions of")}{" "}
|
<label htmlFor="updateCheckEnabled" className="checkbox-label">
|
||||||
<code>@runfusion/fusion</code>{t("settings.globalGeneral.andShowsUpdateNoticesInTheCLIAnd", " and shows update notices in the CLI and dashboard. Cadence is governed by the frequency below. Default: enabled. ")}</small>
|
<input id="updateCheckEnabled" type="checkbox" checked={form.updateCheckEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, updateCheckEnabled: e.target.checked }))}/>{t("settings.globalGeneral.checkForUpdatesAutomatically", " Check for updates automatically ")}</label>
|
||||||
|
<SettingsHelpTip settingKey="updateCheckEnabled">{t("settings.globalGeneral.whenEnabledFusionChecksNpmForNewVersions", " When enabled, Fusion checks npm for new versions of")}{" "}
|
||||||
|
<code>@runfusion/fusion</code>{t("settings.globalGeneral.andShowsUpdateNoticesInTheCLIAnd", " and shows update notices in the CLI and dashboard. Cadence is governed by the frequency below. Default: enabled. ")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/*
|
{/*
|
||||||
FNXC:SettingsGlobalGeneral 2026-07-15-17:35:
|
FNXC:SettingsGlobalGeneral 2026-07-15-17:35:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||||
import { ShortcutCaptureInput } from "./ShortcutCaptureInput";
|
import { ShortcutCaptureInput } from "./ShortcutCaptureInput";
|
||||||
import {
|
import {
|
||||||
DEFAULT_DASHBOARD_KEYBOARD_SHORTCUTS,
|
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.
|
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:
|
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) {
|
export function KeyboardShortcutsSection({ form, setForm }: KeyboardShortcutsSectionProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
@@ -35,8 +39,10 @@ export function KeyboardShortcutsSection({ form, setForm }: KeyboardShortcutsSec
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4 className="settings-section-heading">{t("settings.keyboardShortcuts.title", "Keyboard Shortcuts")}</h4>
|
<div className="settings-field-label-row">
|
||||||
<p className="settings-description">{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.")}</p>
|
<h4 className="settings-section-heading">{t("settings.keyboardShortcuts.title", "Keyboard Shortcuts")}</h4>
|
||||||
|
<SettingsHelpTip settingKey="dashboardKeyboardShortcuts">{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.")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<div className="form-group settings-keyboard-shortcuts" data-testid="keyboard-shortcuts-settings">
|
<div className="form-group settings-keyboard-shortcuts" data-testid="keyboard-shortcuts-settings">
|
||||||
{SHORTCUT_CATEGORIES.map((category) => (
|
{SHORTCUT_CATEGORIES.map((category) => (
|
||||||
<div className="shortcut-category" key={category.id}>
|
<div className="shortcut-category" key={category.id}>
|
||||||
@@ -47,7 +53,10 @@ export function KeyboardShortcutsSection({ form, setForm }: KeyboardShortcutsSec
|
|||||||
const hintId = `${inputId}Hint`;
|
const hintId = `${inputId}Hint`;
|
||||||
return (
|
return (
|
||||||
<div className="shortcut-row" key={action}>
|
<div className="shortcut-row" key={action}>
|
||||||
<label htmlFor={inputId}>{t(`settings.keyboardShortcuts.action.${action}`, getShortcutActionLabel(action))}</label>
|
<div className="settings-field-label-row">
|
||||||
|
<label htmlFor={inputId}>{t(`settings.keyboardShortcuts.action.${action}`, getShortcutActionLabel(action))}</label>
|
||||||
|
<SettingsHelpTip settingKey={inputId}>{t("settings.keyboardShortcuts.rowHint", "Default: {{default}}. Leave blank to disable.", { default: DEFAULT_DASHBOARD_KEYBOARD_SHORTCUTS[action] })}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<ShortcutCaptureInput
|
<ShortcutCaptureInput
|
||||||
id={inputId}
|
id={inputId}
|
||||||
value={shortcutValues[action]}
|
value={shortcutValues[action]}
|
||||||
@@ -56,11 +65,8 @@ export function KeyboardShortcutsSection({ form, setForm }: KeyboardShortcutsSec
|
|||||||
describedById={hintId}
|
describedById={hintId}
|
||||||
onChange={(value) => updateShortcut(action, value)}
|
onChange={(value) => updateShortcut(action, value)}
|
||||||
/>
|
/>
|
||||||
<small id={hintId}>
|
{/* 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
|
<small id={hintId}>{parsed.valid ? null : parsed.error}</small>
|
||||||
? t("settings.keyboardShortcuts.rowHint", "Default: {{default}}. Leave blank to disable.", { default: DEFAULT_DASHBOARD_KEYBOARD_SHORTCUTS[action] })
|
|
||||||
: parsed.error}
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import "./McpServersCard.css";
|
import "./McpServersCard.css";
|
||||||
import { Download, Pencil, Play, Plus, RefreshCw, Trash2, Upload } from "lucide-react";
|
import { Download, Pencil, Play, Plus, RefreshCw, Trash2, Upload } from "lucide-react";
|
||||||
|
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -540,11 +541,14 @@ export function McpServersCard({ scope, form, setForm, globalSettings, projectId
|
|||||||
<button type="button" className="btn btn-primary touch-target" onClick={() => { setEditor(draftFromServer()); setEditorError(null); }}><Plus aria-hidden="true" size={MCP_BUTTON_ICON_SIZE_MD} /> {t("settings.mcp.addServer", "Add server")}</button>
|
<button type="button" className="btn btn-primary touch-target" onClick={() => { setEditor(draftFromServer()); setEditorError(null); }}><Plus aria-hidden="true" size={MCP_BUTTON_ICON_SIZE_MD} /> {t("settings.mcp.addServer", "Add server")}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="checkbox-label mcp-enabled-toggle">
|
{/* 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. */}
|
||||||
<input type="checkbox" checked={settings.enabled === true} onChange={(event) => setEnabled(event.target.checked)} />
|
<div className="settings-field-label-row">
|
||||||
{t("settings.mcp.enabled", "Enable MCP servers for this scope")}
|
<label className="checkbox-label mcp-enabled-toggle">
|
||||||
</label>
|
<input type="checkbox" checked={settings.enabled === true} onChange={(event) => setEnabled(event.target.checked)} />
|
||||||
<small className="settings-description">{t("settings.mcp.enabledHint", "Default: disabled, with no servers configured.")}</small>
|
{t("settings.mcp.enabled", "Enable MCP servers for this scope")}
|
||||||
|
</label>
|
||||||
|
<SettingsHelpTip settingKey={`mcp-enabled-${scope}`}>{t("settings.mcp.enabledHint", "Default: disabled, with no servers configured.")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mcp-discovery card" data-testid={`mcp-discovery-${scope}`}>
|
<div className="mcp-discovery card" data-testid={`mcp-discovery-${scope}`}>
|
||||||
<div className="mcp-discovery__header">
|
<div className="mcp-discovery__header">
|
||||||
|
|||||||
@@ -66,9 +66,13 @@ export function MemorySection({ form, setForm, memory }: MemorySectionProps) {
|
|||||||
dreams: "Dreams",
|
dreams: "Dreams",
|
||||||
};
|
};
|
||||||
return (<>
|
return (<>
|
||||||
<h4 className="settings-section-heading">{t("settings.memory.memory", "Memory")}</h4>
|
{/*
|
||||||
<div className="form-group">
|
FNXC:SettingsHelp 2026-07-16-12:45:
|
||||||
<small className="settings-muted">{t("settings.memory.memoryLivesIn", " Memory lives in ")}<code>.fusion/memory/</code>{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. ")}</small>
|
Section intro moved behind the shared "?" beside the heading — operator requirement: no inline description paragraphs in Settings.
|
||||||
|
*/}
|
||||||
|
<div className="settings-field-label-row">
|
||||||
|
<h4 className="settings-section-heading">{t("settings.memory.memory", "Memory")}</h4>
|
||||||
|
<SettingsHelpTip settingKey="memory-section">{t("settings.memory.memoryLivesIn", " Memory lives in ")}<code>.fusion/memory/</code>{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. ")}</SettingsHelpTip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SettingsToggleRow
|
<SettingsToggleRow
|
||||||
@@ -195,10 +199,10 @@ export function MemorySection({ form, setForm, memory }: MemorySectionProps) {
|
|||||||
<Loader2 size={14} className="animate-spin"/>{t("settings.memory.dreaming", " Dreaming\u2026 ")}</>) : (t("settings.memory.dreamNow", "Dream Now"))}
|
<Loader2 size={14} className="animate-spin"/>{t("settings.memory.dreaming", " Dreaming\u2026 ")}</>) : (t("settings.memory.dreamNow", "Dream Now"))}
|
||||||
</button>
|
</button>
|
||||||
{/*
|
{/*
|
||||||
FNXC:SettingsHelp 2026-07-15-21:40:
|
FNXC:SettingsHelp 2026-07-16-12:45:
|
||||||
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.
|
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.
|
||||||
*/}
|
*/}
|
||||||
<small>{t("settings.memory.manuallyTriggerDreamProcessingNow", "Manually trigger dream processing now.")}</small>
|
<SettingsHelpTip settingKey="memory-dream-now">{t("settings.memory.manuallyTriggerDreamProcessingNow", "Manually trigger dream processing now.")}</SettingsHelpTip>
|
||||||
</div>
|
</div>
|
||||||
</>)}
|
</>)}
|
||||||
|
|
||||||
@@ -244,7 +248,14 @@ export function MemorySection({ form, setForm, memory }: MemorySectionProps) {
|
|||||||
|
|
||||||
{memoryLoading ? (<div className="settings-empty-state"><LoadingSpinner label={t("settings.memory.loadingMemory", "Loading memory\u2026")} /></div>) : (<div className="memory-editor-section">
|
{memoryLoading ? (<div className="settings-empty-state"><LoadingSpinner label={t("settings.memory.loadingMemory", "Loading memory\u2026")} /></div>) : (<div className="memory-editor-section">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="memoryFilePath">{t("settings.memory.memoryFile", "Memory File")}</label>
|
{/*
|
||||||
|
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.
|
||||||
|
*/}
|
||||||
|
<div className="settings-field-label-row">
|
||||||
|
<label htmlFor="memoryFilePath">{t("settings.memory.memoryFile", "Memory File")}</label>
|
||||||
|
<SettingsHelpTip settingKey="memoryFilePath">Choose any project memory file to view or edit. Dreams is selected by default.</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<select id="memoryFilePath" value={selectedMemoryPath} onChange={(e) => {
|
<select id="memoryFilePath" value={selectedMemoryPath} onChange={(e) => {
|
||||||
setSelectedMemoryPath(e.target.value);
|
setSelectedMemoryPath(e.target.value);
|
||||||
setMemoryDirty(false);
|
setMemoryDirty(false);
|
||||||
@@ -253,15 +264,7 @@ export function MemorySection({ form, setForm, memory }: MemorySectionProps) {
|
|||||||
{formatMemoryFileOptionLabel(file)}
|
{formatMemoryFileOptionLabel(file)}
|
||||||
</option>))}
|
</option>))}
|
||||||
</select>
|
</select>
|
||||||
{/*
|
{memoryDirty && (<small>Save or discard the current edits before switching files.</small>)}
|
||||||
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 `<small>` stays visible rather than splitting one string across two affordances by state.
|
|
||||||
*/}
|
|
||||||
<small>
|
|
||||||
{memoryDirty
|
|
||||||
? "Save or discard the current edits before switching files."
|
|
||||||
: "Choose any project memory file to view or edit. Dreams is selected by default."}
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
{selectedMemoryFile && (<div className="memory-file-summary">
|
{selectedMemoryFile && (<div className="memory-file-summary">
|
||||||
<span>{memoryLayerNames[selectedMemoryFile.layer]}</span>
|
<span>{memoryLayerNames[selectedMemoryFile.layer]}</span>
|
||||||
@@ -271,17 +274,19 @@ export function MemorySection({ form, setForm, memory }: MemorySectionProps) {
|
|||||||
</small>
|
</small>
|
||||||
</div>)}
|
</div>)}
|
||||||
<div className="form-group memory-editor-form-group">
|
<div className="form-group memory-editor-form-group">
|
||||||
<label>{selectedMemoryFile?.label || "Memory Editor"}</label>
|
|
||||||
{/*
|
{/*
|
||||||
FNXC:SettingsHelp 2026-07-15-21:40:
|
FNXC:SettingsHelp 2026-07-16-12:45:
|
||||||
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.
|
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.
|
||||||
*/}
|
*/}
|
||||||
<small>
|
<div className="settings-field-label-row">
|
||||||
{selectedMemoryFile?.layer === "long-term" && "Curated durable decisions, conventions, constraints, and pitfalls promoted from dreams."}
|
<label>{selectedMemoryFile?.label || "Memory Editor"}</label>
|
||||||
{selectedMemoryFile?.layer === "daily" && "Raw daily observations, open loops, and running context for dream processing."}
|
<SettingsHelpTip settingKey="memory-editor">
|
||||||
{selectedMemoryFile?.layer === "dreams" && "Synthesized patterns and open loops promoted from daily memory."}
|
{selectedMemoryFile?.layer === "long-term" && "Curated durable decisions, conventions, constraints, and pitfalls promoted from dreams."}
|
||||||
{!selectedMemoryFile && "Edits the selected memory file."}
|
{selectedMemoryFile?.layer === "daily" && "Raw daily observations, open loops, and running context for dream processing."}
|
||||||
</small>
|
{selectedMemoryFile?.layer === "dreams" && "Synthesized patterns and open loops promoted from daily memory."}
|
||||||
|
{!selectedMemoryFile && "Edits the selected memory file."}
|
||||||
|
</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<div className="memory-editor-frame">
|
<div className="memory-editor-frame">
|
||||||
<FileEditor content={memoryContent} onChange={(content) => {
|
<FileEditor content={memoryContent} onChange={(content) => {
|
||||||
setMemoryContent(content);
|
setMemoryContent(content);
|
||||||
@@ -295,11 +300,12 @@ export function MemorySection({ form, setForm, memory }: MemorySectionProps) {
|
|||||||
<button type="button" className="btn btn-secondary btn-sm" onClick={onCompactMemory} disabled={!isEditingAllowed || memoryDirty || memoryCompactLoading}>
|
<button type="button" className="btn btn-secondary btn-sm" onClick={onCompactMemory} disabled={!isEditingAllowed || memoryDirty || memoryCompactLoading}>
|
||||||
{memoryCompactLoading ? t("settings.memory.compacting", "Compacting…") : t("settings.memory.compactSelectedFile", "Compact Selected File")}
|
{memoryCompactLoading ? t("settings.memory.compacting", "Compacting…") : t("settings.memory.compactSelectedFile", "Compact Selected File")}
|
||||||
</button>
|
</button>
|
||||||
<small>
|
{/*
|
||||||
{memoryDirty
|
FNXC:SettingsHelp 2026-07-16-12:45:
|
||||||
? "Save or discard edits before compacting this file."
|
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.`}
|
*/}
|
||||||
</small>
|
<SettingsHelpTip settingKey="memory-compact-file">{`Compacts ${selectedMemoryPath} and writes the result back to the same file.`}</SettingsHelpTip>
|
||||||
|
{memoryDirty && (<small>Save or discard edits before compacting this file.</small>)}
|
||||||
</div>)}
|
</div>)}
|
||||||
|
|
||||||
{memoryDirty && isEditingAllowed && (<div className="form-group">
|
{memoryDirty && isEditingAllowed && (<div className="form-group">
|
||||||
|
|||||||
@@ -193,8 +193,11 @@ export function MergeSection({ form, setForm, integrationBranchOptions, integrat
|
|||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<div className="form-group" data-testid="legacy-automerge-stamp-cleanup-panel">
|
<div className="form-group" data-testid="legacy-automerge-stamp-cleanup-panel">
|
||||||
<h5 className="settings-section-heading">{t("settings.merge.legacyAutoMergeStampCleanup", "Legacy auto-merge stamp cleanup")}</h5>
|
{/* 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 `<small>`s below stay inline: they are dynamic feedback, not help copy. */}
|
||||||
<small>{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. ")}</small>
|
<div className="settings-field-label-row">
|
||||||
|
<h5 className="settings-section-heading">{t("settings.merge.legacyAutoMergeStampCleanup", "Legacy auto-merge stamp cleanup")}</h5>
|
||||||
|
<SettingsHelpTip settingKey="legacy-automerge-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. ")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
{legacyStampLoading ? (<small aria-live="polite">{t("settings.merge.checkingForLegacyAutoMergeStamps", "Checking for legacy auto-merge stamps\u2026")}</small>) : legacyStampCandidates.length === 0 ? (<small data-testid="legacy-automerge-stamp-empty-state">{t("settings.merge.noLegacyAutoMergeStampsToCleanUp", " No legacy auto-merge stamps to clean up. ")}</small>) : (<>
|
{legacyStampLoading ? (<small aria-live="polite">{t("settings.merge.checkingForLegacyAutoMergeStamps", "Checking for legacy auto-merge stamps\u2026")}</small>) : legacyStampCandidates.length === 0 ? (<small data-testid="legacy-automerge-stamp-empty-state">{t("settings.merge.noLegacyAutoMergeStampsToCleanUp", " No legacy auto-merge stamps to clean up. ")}</small>) : (<>
|
||||||
<small>{legacyStampCandidates.length}{t("settings.merge.legacyAutoMergeStamp", " legacy auto-merge stamp")}{legacyStampCandidates.length === 1 ? "" : "s"}{t("settings.merge.readyToCleanUp", " ready to clean up.")}</small>
|
<small>{legacyStampCandidates.length}{t("settings.merge.legacyAutoMergeStamp", " legacy auto-merge stamp")}{legacyStampCandidates.length === 1 ? "" : "s"}{t("settings.merge.readyToCleanUp", " ready to clean up.")}</small>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import type { ModelPricing, ModelPricingOverrides } from "@fusion/core";
|
import type { ModelPricing, ModelPricingOverrides } from "@fusion/core";
|
||||||
import { api } from "../../../api";
|
import { api } from "../../../api";
|
||||||
|
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||||
import type { ToastType } from "../../../hooks/useToast";
|
import type { ToastType } from "../../../hooks/useToast";
|
||||||
import type { SetSettingsForm, SettingsFormState } from "./context";
|
import type { SetSettingsForm, SettingsFormState } from "./context";
|
||||||
import "./ModelPricingSection.css";
|
import "./ModelPricingSection.css";
|
||||||
@@ -231,10 +232,17 @@ export function ModelPricingSection({ form, setForm, addToast, projectId }: Mode
|
|||||||
<section className="model-pricing-section" aria-label={t("settings.modelPricing.title", "Model pricing overrides")}>
|
<section className="model-pricing-section" aria-label={t("settings.modelPricing.title", "Model pricing overrides")}>
|
||||||
<div className="model-pricing-section__header">
|
<div className="model-pricing-section__header">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.modelPricing.title", "Model Pricing")}</h4>
|
{/*
|
||||||
<p className="settings-description">
|
FNXC:SettingsHelp 2026-07-16-12:45:
|
||||||
{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).")}
|
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.
|
||||||
</p>
|
*/}
|
||||||
|
<div className="settings-field-label-row">
|
||||||
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.modelPricing.title", "Model Pricing")}</h4>
|
||||||
|
<SettingsHelpTip settingKey="model-pricing-section">
|
||||||
|
{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.")}
|
||||||
|
</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<p className="settings-muted model-pricing-section__meta">
|
<p className="settings-muted model-pricing-section__meta">
|
||||||
{form.modelPricingFetchedAt
|
{form.modelPricingFetchedAt
|
||||||
? t("settings.modelPricing.pricesAsOf", "Prices as of {{date}}", { date: new Date(form.modelPricingFetchedAt).toLocaleString() })
|
? 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.viewTable", "View pricing table")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<small>{t("settings.modelPricing.saveHint", "Manual edits are saved with the rest of Global settings.")}</small>
|
|
||||||
{renderPricingTableModal()}
|
{renderPricingTableModal()}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { NodeInfo } from "../../../api";
|
import type { NodeInfo } from "../../../api";
|
||||||
import { NodeHealthDot } from "../../NodeHealthDot";
|
import { NodeHealthDot } from "../../NodeHealthDot";
|
||||||
import { SettingsSelectRow } from "../SettingsSelectRow";
|
import { SettingsSelectRow } from "../SettingsSelectRow";
|
||||||
|
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||||
import type { SettingsFormState, SetSettingsForm } from "./context";
|
import type { SettingsFormState, SetSettingsForm } from "./context";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
function getNodeStatusLabel(status: "online" | "offline" | "connecting" | "error", t: ReturnType<typeof useTranslation<"app">>["t"]): string {
|
function getNodeStatusLabel(status: "online" | "offline" | "connecting" | "error", t: ReturnType<typeof useTranslation<"app">>["t"]): string {
|
||||||
@@ -29,10 +30,16 @@ export function NodeRoutingSection({ form, setForm, nodes }: NodeRoutingSectionP
|
|||||||
<p className="settings-node-routing-note">{t("settings.nodeRouting.theseSettingsApplyAtTheProjectLevel", "These settings apply at the project level.")}</p>
|
<p className="settings-node-routing-note">{t("settings.nodeRouting.theseSettingsApplyAtTheProjectLevel", "These settings apply at the project level.")}</p>
|
||||||
{/*
|
{/*
|
||||||
FNXC:SettingsStyling 2026-07-15-17:35:
|
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.
|
||||||
*/}
|
*/}
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="defaultNodeId">{t("settings.nodeRouting.defaultExecutionNode", "Default Execution Node")}</label>
|
<div className="settings-field-label-row">
|
||||||
|
<label htmlFor="defaultNodeId">{t("settings.nodeRouting.defaultExecutionNode", "Default Execution Node")}</label>
|
||||||
|
<SettingsHelpTip settingKey="defaultNodeId">{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).")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<select id="defaultNodeId" className="select" value={typeof form.defaultNodeId === "string" ? form.defaultNodeId : ""} onChange={(e) => {
|
<select id="defaultNodeId" className="select" value={typeof form.defaultNodeId === "string" ? form.defaultNodeId : ""} onChange={(e) => {
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
setForm((f) => ({ ...f, defaultNodeId: val || undefined } as SettingsFormState));
|
setForm((f) => ({ ...f, defaultNodeId: val || undefined } as SettingsFormState));
|
||||||
@@ -51,7 +58,6 @@ export function NodeRoutingSection({ form, setForm, nodes }: NodeRoutingSectionP
|
|||||||
<NodeHealthDot status={selectedNode.status} showLabel/>
|
<NodeHealthDot status={selectedNode.status} showLabel/>
|
||||||
</div>);
|
</div>);
|
||||||
})()}
|
})()}
|
||||||
<small>{t("settings.nodeRouting.usedWhenATaskHasNoNodeOverride", "Used when a task has no node override. Node status is shown for safer routing selection. No default \u2014 unset (local execution).")}</small>
|
|
||||||
</div>
|
</div>
|
||||||
<SettingsSelectRow
|
<SettingsSelectRow
|
||||||
descriptor={{
|
descriptor={{
|
||||||
|
|||||||
@@ -181,8 +181,8 @@ export function NotificationsSection({ form, setForm, testNotificationLoading, t
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
{/*
|
{/*
|
||||||
FNXC:SettingsHelp 2026-07-15-21:40:
|
FNXC:SettingsHelp 2026-07-16-12:45:
|
||||||
The per-event `<small>`s stay inline. They are OPTION descriptions inside one multi-select control — the thing an operator compares while ticking boxes — not help for 14 separate settings. Putting a "?" on every option would replace one scannable list with fourteen closed bubbles, which is the opposite of what moving help off the row is for.
|
(Supersedes the 2026-07-15-21:40 note that kept the per-event `<small>`s inline as option descriptions.) Operator decision 2026-07-16: ALL inline description/help text in Settings moves behind the shared "?" affordance — including these per-event descriptions. Each option now carries a SettingsHelpTip beside its checkbox label, keyed `ntfy-event-${event}` / `webhook-event-${event}` so bubble DOM ids stay unique across the two lists.
|
||||||
*/}
|
*/}
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>{t("settings.notifications.notifyOnEvents", "Notify on events")}</label>
|
<label>{t("settings.notifications.notifyOnEvents", "Notify on events")}</label>
|
||||||
@@ -190,17 +190,20 @@ export function NotificationsSection({ form, setForm, testNotificationLoading, t
|
|||||||
{NOTIFICATION_EVENT_OPTIONS.map(({ event, label, description }) => {
|
{NOTIFICATION_EVENT_OPTIONS.map(({ event, label, description }) => {
|
||||||
const checked = form.ntfyEvents?.includes(event) ?? true;
|
const checked = form.ntfyEvents?.includes(event) ?? true;
|
||||||
return (<div key={`ntfy-${event}`}>
|
return (<div key={`ntfy-${event}`}>
|
||||||
<label className="checkbox-label">
|
{/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. */}
|
||||||
<input type="checkbox" checked={checked} onChange={(e) => {
|
<div className="settings-field-label-row">
|
||||||
const current = form.ntfyEvents ?? [...DEFAULT_NTFY_EVENTS];
|
<label className="checkbox-label">
|
||||||
const newEvents = e.target.checked
|
<input type="checkbox" checked={checked} onChange={(e) => {
|
||||||
? (current.includes(event) ? current : [...current, event])
|
const current = form.ntfyEvents ?? [...DEFAULT_NTFY_EVENTS];
|
||||||
: current.filter((ev): ev is NtfyNotificationEvent => ev !== event);
|
const newEvents = e.target.checked
|
||||||
setForm((f) => ({ ...f, ntfyEvents: newEvents.length > 0 ? newEvents : undefined }));
|
? (current.includes(event) ? current : [...current, event])
|
||||||
}}/>
|
: current.filter((ev): ev is NtfyNotificationEvent => ev !== event);
|
||||||
{label}
|
setForm((f) => ({ ...f, ntfyEvents: newEvents.length > 0 ? newEvents : undefined }));
|
||||||
</label>
|
}}/>
|
||||||
<small>{description}</small>
|
{label}
|
||||||
|
</label>
|
||||||
|
<SettingsHelpTip settingKey={`ntfy-event-${event}`}>{description}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -300,17 +303,20 @@ export function NotificationsSection({ form, setForm, testNotificationLoading, t
|
|||||||
const currentEvents = form.webhookEvents ?? [...DEFAULT_NTFY_EVENTS];
|
const currentEvents = form.webhookEvents ?? [...DEFAULT_NTFY_EVENTS];
|
||||||
const checked = currentEvents.includes(event);
|
const checked = currentEvents.includes(event);
|
||||||
return (<div key={`webhook-${event}`}>
|
return (<div key={`webhook-${event}`}>
|
||||||
<label className="checkbox-label">
|
{/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings. */}
|
||||||
<input type="checkbox" checked={checked} onChange={(e) => {
|
<div className="settings-field-label-row">
|
||||||
const current = form.webhookEvents ?? [...DEFAULT_NTFY_EVENTS];
|
<label className="checkbox-label">
|
||||||
const newEvents = e.target.checked
|
<input type="checkbox" checked={checked} onChange={(e) => {
|
||||||
? (current.includes(event) ? current : [...current, event])
|
const current = form.webhookEvents ?? [...DEFAULT_NTFY_EVENTS];
|
||||||
: current.filter((ev) => ev !== event);
|
const newEvents = e.target.checked
|
||||||
setForm((f) => ({ ...f, webhookEvents: newEvents.length > 0 ? newEvents : undefined }));
|
? (current.includes(event) ? current : [...current, event])
|
||||||
}}/>
|
: current.filter((ev) => ev !== event);
|
||||||
{label}
|
setForm((f) => ({ ...f, webhookEvents: newEvents.length > 0 ? newEvents : undefined }));
|
||||||
</label>
|
}}/>
|
||||||
<small>{description}</small>
|
{label}
|
||||||
|
</label>
|
||||||
|
<SettingsHelpTip settingKey={`webhook-event-${event}`}>{description}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -425,15 +425,21 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* --- Project Model Lanes --- */}
|
{/* --- Project Model Lanes --- */}
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.modelLanes", "Model Lanes")}</h4>
|
{/* 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. */}
|
||||||
<p className="settings-description">{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. ")}</p>
|
<div className="settings-field-label-row">
|
||||||
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.modelLanes", "Model Lanes")}</h4>
|
||||||
|
<SettingsHelpTip settingKey="project-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. ")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
{modelsLoading ? (<div className="settings-empty-state"><LoadingSpinner label={t("settings.projectModels.loadingAvailableModels", "Loading available models\u2026")} /></div>) : availableModels.length === 0 ? (<div className="settings-empty-state settings-muted">{t("settings.projectModels.noModelsAvailableConfigureAuthenticationFirst", " No models available. Configure authentication first. ")}</div>) : (<>
|
{modelsLoading ? (<div className="settings-empty-state"><LoadingSpinner label={t("settings.projectModels.loadingAvailableModels", "Loading available models\u2026")} /></div>) : availableModels.length === 0 ? (<div className="settings-empty-state settings-muted">{t("settings.projectModels.noModelsAvailableConfigureAuthenticationFirst", " No models available. Configure authentication first. ")}</div>) : (<>
|
||||||
{projectModelLanes.map(renderProjectLane)}
|
{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. */}
|
{/* 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. */}
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.chatHeading", "Chat")}</h4>
|
{/* 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. */}
|
||||||
<p className="settings-description">{t("settings.projectModels.chatDescription", "Choose the default target for new Direct chats and whether New Chat should prompt or immediately use that default.")}</p>
|
<div className="settings-field-label-row">
|
||||||
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.chatHeading", "Chat")}</h4>
|
||||||
|
<SettingsHelpTip settingKey="project-chat-defaults">{t("settings.projectModels.chatDescription", "Choose the default target for new Direct chats and whether New Chat should prompt or immediately use that default.")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
{/*
|
{/*
|
||||||
FNXC:SettingsModels 2026-07-15-17:35:
|
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.
|
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:
|
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.
|
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 `<small>` 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.
|
||||||
*/}
|
*/}
|
||||||
<div className="settings-field-label-row">
|
<div className="settings-field-label-row">
|
||||||
<label htmlFor="chatDefaultModel">{t("settings.projectModels.chatDefaultModel", "Chat Default Model")}</label>
|
<label htmlFor="chatDefaultModel">{t("settings.projectModels.chatDefaultModel", "Chat Default Model")}</label>
|
||||||
@@ -481,7 +487,10 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW
|
|||||||
{chatDefaultCustomized && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.chatDefaultReset", "Reset Chat default")} onClick={resetChatDefaultValue}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
{chatDefaultCustomized && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.chatDefaultReset", "Reset Chat default")} onClick={resetChatDefaultValue}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
||||||
</div>
|
</div>
|
||||||
</div>) : (<div className="form-group" data-testid="project-models-chat-agent">
|
</div>) : (<div className="form-group" data-testid="project-models-chat-agent">
|
||||||
<label htmlFor="chatDefaultAgentId">{t("settings.projectModels.chatDefaultAgent", "Chat Default Agent")}</label>
|
<div className="settings-field-label-row">
|
||||||
|
<label htmlFor="chatDefaultAgentId">{t("settings.projectModels.chatDefaultAgent", "Chat Default Agent")}</label>
|
||||||
|
<SettingsHelpTip settingKey="chatDefaultAgentId">{t("settings.projectModels.chatDefaultAgentHelp", "Agent-mode New Chat starts a Direct chat with the selected durable agent.")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<div className="settings-model-lane-control-row">
|
<div className="settings-model-lane-control-row">
|
||||||
<div className="settings-model-lane-control-main">
|
<div className="settings-model-lane-control-main">
|
||||||
<select id="chatDefaultAgentId" value={form.chatDefaultAgentId ?? ""} disabled={agentsLoading || agents.length === 0} onChange={(event) => setForm((f) => ({ ...f, chatDefaultKind: "agent", chatDefaultAgentId: event.target.value || undefined, chatDefaultModelProvider: undefined, chatDefaultModelId: undefined, chatDefaultThinkingLevel: undefined } as SettingsFormState))}>
|
<select id="chatDefaultAgentId" value={form.chatDefaultAgentId ?? ""} disabled={agentsLoading || agents.length === 0} onChange={(event) => setForm((f) => ({ ...f, chatDefaultKind: "agent", chatDefaultAgentId: event.target.value || undefined, chatDefaultModelProvider: undefined, chatDefaultModelId: undefined, chatDefaultThinkingLevel: undefined } as SettingsFormState))}>
|
||||||
@@ -491,13 +500,16 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW
|
|||||||
</div>
|
</div>
|
||||||
{chatDefaultCustomized && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.chatDefaultReset", "Reset Chat default")} onClick={resetChatDefaultValue}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
{chatDefaultCustomized && (<button type="button" className="btn btn-ghost btn-sm" title={t("settings.projectModels.chatDefaultReset", "Reset Chat default")} onClick={resetChatDefaultValue}>{t("settings.projectModels.reset", " Reset ")}</button>)}
|
||||||
</div>
|
</div>
|
||||||
<small>{agents.length === 0 && !agentsLoading ? t("settings.projectModels.chatDefaultAgentEmpty", "No agents are available for this project yet.") : t("settings.projectModels.chatDefaultAgentHelp", "Agent-mode New Chat starts a Direct chat with the selected durable agent.")}</small>
|
{agents.length === 0 && !agentsLoading ? (<small>{t("settings.projectModels.chatDefaultAgentEmpty", "No agents are available for this project yet.")}</small>) : null}
|
||||||
</div>)}
|
</div>)}
|
||||||
|
|
||||||
{/* --- Default workflow model lanes --- */}
|
{/* --- Default workflow model lanes --- */}
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.defaultWorkflowModelLanes", "Default workflow model lanes")}</h4>
|
{/* 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. */}
|
||||||
<p className="settings-description">
|
<div className="settings-field-label-row">
|
||||||
{t("settings.movedStub.modelLanes", "Per-phase model lanes (execution, planning, reviewer, and their fallbacks) now live on the workflow.")}{t("settings.projectModels.theseProjectOverridesApplyToTheActiveDefault", " These project overrides apply to the active default workflow. ")}</p>
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.defaultWorkflowModelLanes", "Default workflow model lanes")}</h4>
|
||||||
|
<SettingsHelpTip settingKey="default-workflow-model-lanes">
|
||||||
|
{t("settings.movedStub.modelLanes", "Per-phase model lanes (execution, planning, reviewer, and their fallbacks) now live on the workflow.")}{t("settings.projectModels.theseProjectOverridesApplyToTheActiveDefault", " These project overrides apply to the active default workflow. ")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
{!projectId ? (<div className="settings-empty-state settings-muted">{t("settings.projectModels.openAProjectToEditWorkflowModelLanes", "Open a project to edit workflow model lanes.")}</div>) : workflowLoading ? (<div className="settings-empty-state"><LoadingSpinner label={t("settings.projectModels.loadingWorkflowModelLanes", "Loading workflow model lanes\u2026")} /></div>) : availableModels.length === 0 ? (<div className="settings-empty-state settings-muted">{t("settings.projectModels.noModelsAvailableConfigureAuthenticationBeforeSelectingWorkflow", " No models available. Configure authentication before selecting workflow model lanes. ")}</div>) : (<>
|
{!projectId ? (<div className="settings-empty-state settings-muted">{t("settings.projectModels.openAProjectToEditWorkflowModelLanes", "Open a project to edit workflow model lanes.")}</div>) : workflowLoading ? (<div className="settings-empty-state"><LoadingSpinner label={t("settings.projectModels.loadingWorkflowModelLanes", "Loading workflow model lanes\u2026")} /></div>) : availableModels.length === 0 ? (<div className="settings-empty-state settings-muted">{t("settings.projectModels.noModelsAvailableConfigureAuthenticationBeforeSelectingWorkflow", " No models available. Configure authentication before selecting workflow model lanes. ")}</div>) : (<>
|
||||||
{workflowModelPairs.map((pair) => {
|
{workflowModelPairs.map((pair) => {
|
||||||
const value = modelPairValue(effectiveWorkflowValues, pair);
|
const value = modelPairValue(effectiveWorkflowValues, pair);
|
||||||
@@ -671,8 +683,20 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW
|
|||||||
|
|
||||||
{/* --- AI Title and Git Commit Message Summarization --- */}
|
{/* --- AI Title and Git Commit Message Summarization --- */}
|
||||||
<section data-testid="project-models-ai-summarization">
|
<section data-testid="project-models-ai-summarization">
|
||||||
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.aITitleAndGitCommitMessageSummarization", " AI Title and Git Commit Message Summarization ")}</h4>
|
{/*
|
||||||
<p className="settings-description">{t("settings.projectModels.configuresTheModelUsedForTwoShortSummary", " Configures the model used for two short-summary jobs: auto-generating task titles from long descriptions, and generating merge commit summaries from step commits and diff stats. ")}</p>
|
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.
|
||||||
|
The formerly separate conditional paragraph (shown once any summarization feature is on) rides in the same tip as a conditional fragment; SettingsHelpTip's ReactNode children carry conditional copy, so the condition is preserved verbatim.
|
||||||
|
*/}
|
||||||
|
<div className="settings-field-label-row">
|
||||||
|
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.projectModels.aITitleAndGitCommitMessageSummarization", " AI Title and Git Commit Message Summarization ")}</h4>
|
||||||
|
<SettingsHelpTip settingKey="project-ai-summarization">
|
||||||
|
{t("settings.projectModels.configuresTheModelUsedForTwoShortSummary", " Configures the model used for two short-summary jobs: auto-generating task titles from long descriptions, and generating merge commit summaries from step commits and diff stats. ")}
|
||||||
|
{(form.autoSummarizeTitles || form.useAiMergeCommitSummary || form.githubTrackingEnabledByDefault || false)
|
||||||
|
? t("settings.movedStub.summarizerModelInline", "These summarization model controls govern title auto-summarization, merge commit summaries, GitHub tracking titles, and PR metadata generation.")
|
||||||
|
: ""}
|
||||||
|
</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
{modelsLoading ? (<div className="settings-empty-state"><LoadingSpinner label={t("settings.projectModels.loadingAvailableModels", "Loading available models…")} /></div>) : availableModels.length === 0 ? (<div className="settings-empty-state settings-muted">{t("settings.projectModels.noModelsAvailableConfigureAuthenticationFirst", " No models available. Configure authentication first. ")}</div>) : (<>
|
{modelsLoading ? (<div className="settings-empty-state"><LoadingSpinner label={t("settings.projectModels.loadingAvailableModels", "Loading available models…")} /></div>) : availableModels.length === 0 ? (<div className="settings-empty-state settings-muted">{t("settings.projectModels.noModelsAvailableConfigureAuthenticationFirst", " No models available. Configure authentication first. ")}</div>) : (<>
|
||||||
{summarizationLane ? renderProjectLane(summarizationLane) : null}
|
{summarizationLane ? renderProjectLane(summarizationLane) : null}
|
||||||
{/* FNXC:Settings-ThinkingLevel 2026-07-10-12:08: Title-summarizer fallback provider/model/thinking settings are project-scoped, not workflow-declared. Render it with the summarization controls so saves use project null-as-delete semantics instead of the workflow-values API. */}
|
{/* FNXC:Settings-ThinkingLevel 2026-07-10-12:08: Title-summarizer fallback provider/model/thinking settings are project-scoped, not workflow-declared. Render it with the summarization controls so saves use project null-as-delete semantics instead of the workflow-values API. */}
|
||||||
@@ -721,10 +745,6 @@ export function ProjectModelsSection({ form, setForm, models, projectId, onOpenW
|
|||||||
onChange={(v) => setForm((f) => ({ ...f, useAiMergeCommitSummary: v === true }))}
|
onChange={(v) => setForm((f) => ({ ...f, useAiMergeCommitSummary: v === true }))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(form.autoSummarizeTitles || form.useAiMergeCommitSummary || form.githubTrackingEnabledByDefault || false) && (<p className="settings-description">
|
|
||||||
{t("settings.movedStub.summarizerModelInline", "These summarization model controls govern title auto-summarization, merge commit summaries, GitHub tracking titles, and PR metadata generation.")}
|
|
||||||
</p>)}
|
|
||||||
|
|
||||||
<SettingsTextareaRow
|
<SettingsTextareaRow
|
||||||
descriptor={{
|
descriptor={{
|
||||||
key: "prTitlePromptInstructions",
|
key: "prTitlePromptInstructions",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { AgentPromptsConfig } from "@fusion/core";
|
import type { AgentPromptsConfig } from "@fusion/core";
|
||||||
import { AgentPromptsManager } from "../../AgentPromptsManager";
|
import { AgentPromptsManager } from "../../AgentPromptsManager";
|
||||||
|
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||||
import { MovedSettingsStub } from "./MovedSettingsStub";
|
import { MovedSettingsStub } from "./MovedSettingsStub";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
|
||||||
@@ -23,14 +24,18 @@ export function PromptsSection({ form, setForm, onOpenWorkflowSettings }: Prompt
|
|||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4 className="settings-section-heading">{t("settings.nav.prompts", "Prompts")}</h4>
|
{/*
|
||||||
<div className="form-group">
|
FNXC:SettingsHelp 2026-07-16-12:45:
|
||||||
<small>
|
Section intro moved behind the shared "?" beside the heading - operator requirement: no inline description paragraphs in Settings.
|
||||||
|
*/}
|
||||||
|
<div className="settings-field-label-row">
|
||||||
|
<h4 className="settings-section-heading">{t("settings.nav.prompts", "Prompts")}</h4>
|
||||||
|
<SettingsHelpTip settingKey="prompts-section">
|
||||||
{t(
|
{t(
|
||||||
"settings.prompts.surfaceExplanation",
|
"settings.prompts.surfaceExplanation",
|
||||||
"Use this section for agent role system prompt templates, role assignments, and global PromptKey segment overrides. Per-workflow step prompts for prompt and gate nodes are edited in the Workflow Editor. No default \u2014 unset (built-in role prompts apply until overridden).",
|
"Use this section for agent role system prompt templates, role assignments, and global PromptKey segment overrides. Per-workflow step prompts for prompt and gate nodes are edited in the Workflow Editor. No default \u2014 unset (built-in role prompts apply until overridden).",
|
||||||
)}
|
)}
|
||||||
</small>
|
</SettingsHelpTip>
|
||||||
</div>
|
</div>
|
||||||
<MovedSettingsStub
|
<MovedSettingsStub
|
||||||
message={t(
|
message={t(
|
||||||
|
|||||||
@@ -204,6 +204,10 @@ export function RemoteSection({ form, setForm, remote }: RemoteSectionProps) {
|
|||||||
|
|
||||||
{activeProvider === "tailscale" && (<>
|
{activeProvider === "tailscale" && (<>
|
||||||
<div className="form-group remote-provider-settings">
|
<div className="form-group remote-provider-settings">
|
||||||
|
{/*
|
||||||
|
FNXC:SettingsHelp 2026-07-16-12:45:
|
||||||
|
Stays inline: this is the block-level description for the whole Tailscale provider mode, and the block has no heading or label of its own to host a "?" trigger — the nearest heading ("Remote Access") describes both providers, so a tip there would misattribute provider-specific copy. Same reasoning as the Cloudflare mode `<small>` below, which is additionally live state (its text switches with Quick Tunnel mode).
|
||||||
|
*/}
|
||||||
<small>{t("settings.remote.tailscaleFunnelWillExposeThisDashboardOnYour", "Tailscale Funnel will expose this dashboard on your tailnet's public ")}{`https://<machine>.<tailnet>.ts.net/`}{t("settings.remote.uRLNoHostnameOrPortConfigurationNeeded", " URL \u2014 no hostname or port configuration needed.")}</small>
|
<small>{t("settings.remote.tailscaleFunnelWillExposeThisDashboardOnYour", "Tailscale Funnel will expose this dashboard on your tailnet's public ")}{`https://<machine>.<tailnet>.ts.net/`}{t("settings.remote.uRLNoHostnameOrPortConfigurationNeeded", " URL \u2014 no hostname or port configuration needed.")}</small>
|
||||||
</div>
|
</div>
|
||||||
<SettingsToggleRow
|
<SettingsToggleRow
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ Four groups deliberately keep their bespoke markup because they are not plain la
|
|||||||
|
|
||||||
FNXC:SettingsHelp 2026-07-15-21:40:
|
FNXC:SettingsHelp 2026-07-15-21:40:
|
||||||
The bespoke rows that are still one control + one help string — the built-in provider radio and each limits field — hang that help off the same "?" as the migrated rows above (`.settings-field-label-row` + `SettingsHelpTip`), so a limits grid of five "Default: N." paragraphs no longer sits beside rows whose help is behind an icon.
|
The bespoke rows that are still one control + one help string — the built-in provider radio and each limits field — hang that help off the same "?" as the migrated rows above (`.settings-field-label-row` + `SettingsHelpTip`), so a limits grid of five "Default: N." paragraphs no longer sits beside rows whose help is behind an icon.
|
||||||
Two kinds of copy stay inline here: the Enabled Sources hints, which annotate a checkbox grid rather than describe one control, and the credential empty-state/alert notes, which are live credential status plus a navigation button the operator must actually see.
|
|
||||||
|
FNXC:SettingsHelp 2026-07-16-12:45:
|
||||||
|
The Enabled Sources per-source hints now ride the same "?" too, beside each option's label (settingKey = the input id) — operator requirement: no inline description paragraphs in Settings. The tip is a sibling of the checkbox label, never inside it.
|
||||||
|
Only the credential empty-state/alert notes stay inline: they are live credential status plus a navigation button the operator must actually see. The locked Web Search "Always on" span is a status tag, not help, and also stays.
|
||||||
*/
|
*/
|
||||||
export function ResearchGlobalSection({ form, setForm, authProviders, onNavigateToSection, }: ResearchGlobalSectionProps) {
|
export function ResearchGlobalSection({ form, setForm, authProviders, onNavigateToSection, }: ResearchGlobalSectionProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
@@ -174,17 +177,24 @@ export function ResearchGlobalSection({ form, setForm, authProviders, onNavigate
|
|||||||
<label htmlFor="research-global-source-webSearch" className="checkbox-label settings-research-source-locked">
|
<label htmlFor="research-global-source-webSearch" className="checkbox-label settings-research-source-locked">
|
||||||
<input id="research-global-source-webSearch" type="checkbox" checked disabled readOnly/>{t("settings.researchGlobal.webSearch", " Web Search ")}<span className="settings-muted">{t("settings.researchGlobal.alwaysOn", "Always on")}</span>
|
<input id="research-global-source-webSearch" type="checkbox" checked disabled readOnly/>{t("settings.researchGlobal.webSearch", " Web Search ")}<span className="settings-muted">{t("settings.researchGlobal.alwaysOn", "Always on")}</span>
|
||||||
</label>
|
</label>
|
||||||
|
{/* 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). */}
|
||||||
<div className="settings-research-source-grid">
|
<div className="settings-research-source-grid">
|
||||||
<label htmlFor="research-global-source-github" className="checkbox-label">
|
<div className="settings-field-label-row">
|
||||||
<input id="research-global-source-github" type="checkbox" checked={form.researchGlobalGitHubEnabled ?? false} onChange={(event) => setForm((current) => ({
|
<label htmlFor="research-global-source-github" className="checkbox-label">
|
||||||
|
<input id="research-global-source-github" type="checkbox" checked={form.researchGlobalGitHubEnabled ?? false} onChange={(event) => setForm((current) => ({
|
||||||
...current,
|
...current,
|
||||||
researchGlobalGitHubEnabled: event.target.checked,
|
researchGlobalGitHubEnabled: event.target.checked,
|
||||||
}))}/>{t("settings.researchGlobal.gitHub", " GitHub ")}<small>{t("settings.researchGlobal.gitHubSourceHint", " Default: disabled. ")}</small></label>
|
}))}/>{t("settings.researchGlobal.gitHub", " GitHub ")}</label>
|
||||||
<label htmlFor="research-global-source-local-docs" className="checkbox-label">
|
<SettingsHelpTip settingKey="research-global-source-github">{t("settings.researchGlobal.gitHubSourceHint", " Default: disabled. ")}</SettingsHelpTip>
|
||||||
<input id="research-global-source-local-docs" type="checkbox" checked={form.researchGlobalLocalDocsEnabled ?? true} onChange={(event) => setForm((current) => ({
|
</div>
|
||||||
|
<div className="settings-field-label-row">
|
||||||
|
<label htmlFor="research-global-source-local-docs" className="checkbox-label">
|
||||||
|
<input id="research-global-source-local-docs" type="checkbox" checked={form.researchGlobalLocalDocsEnabled ?? true} onChange={(event) => setForm((current) => ({
|
||||||
...current,
|
...current,
|
||||||
researchGlobalLocalDocsEnabled: event.target.checked,
|
researchGlobalLocalDocsEnabled: event.target.checked,
|
||||||
}))}/>{t("settings.researchGlobal.localDocs", " Local Docs ")}<small>{t("settings.researchGlobal.localDocsSourceHint", " Default: enabled. ")}</small></label>
|
}))}/>{t("settings.researchGlobal.localDocs", " Local Docs ")}</label>
|
||||||
|
<SettingsHelpTip settingKey="research-global-source-local-docs">{t("settings.researchGlobal.localDocsSourceHint", " Default: enabled. ")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{hasMissingResearchCredential && (<div className="settings-empty-state" role="alert">{t("settings.researchGlobal.missingCredentialsForTheSelectedResearchProvider", " Missing credentials for the selected research provider. ")}<button type="button" className="btn btn-sm" onClick={() => onNavigateToSection("authentication")}>{t("settings.researchGlobal.openAuthentication", " Open Authentication ")}</button>
|
{hasMissingResearchCredential && (<div className="settings-empty-state" role="alert">{t("settings.researchGlobal.missingCredentialsForTheSelectedResearchProvider", " Missing credentials for the selected research provider. ")}<button type="button" className="btn btn-sm" onClick={() => onNavigateToSection("authentication")}>{t("settings.researchGlobal.openAuthentication", " Open Authentication ")}</button>
|
||||||
|
|||||||
@@ -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.
|
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:
|
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:
|
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.
|
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) {
|
export function ResearchProjectSection({ form, setForm, researchLimitError }: ResearchProjectSectionProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
@@ -46,18 +49,22 @@ export function ResearchProjectSection({ form, setForm, researchLimitError }: Re
|
|||||||
/>
|
/>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>{t("settings.researchProject.enabledSources", "Enabled Sources")}</label>
|
<label>{t("settings.researchProject.enabledSources", "Enabled Sources")}</label>
|
||||||
<label htmlFor="research-project-source-webSearch" className="checkbox-label settings-research-source-locked">
|
{/* 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. */}
|
||||||
<input id="research-project-source-webSearch" type="checkbox" checked disabled readOnly/>{t("settings.researchProject.webSearch", " Web Search ")}<span className="settings-muted">{t("settings.researchProject.alwaysOn", "Always on")}</span>
|
<div className="settings-field-label-row">
|
||||||
</label>
|
<label htmlFor="research-project-source-webSearch" className="checkbox-label settings-research-source-locked">
|
||||||
<small className="settings-muted">{t("settings.researchProject.webSearchIsAlwaysEnabledConfigureTheSearch", " Web search is always enabled. Configure the search provider under Research Defaults. ")}</small>
|
<input id="research-project-source-webSearch" type="checkbox" checked disabled readOnly/>{t("settings.researchProject.webSearch", " Web Search ")}<span className="settings-muted">{t("settings.researchProject.alwaysOn", "Always on")}</span>
|
||||||
|
</label>
|
||||||
|
<SettingsHelpTip settingKey="research-project-source-webSearch">{t("settings.researchProject.webSearchIsAlwaysEnabledConfigureTheSearch", " Web search is always enabled. Configure the search provider under Research Defaults. ")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<div className="settings-research-source-grid">
|
<div className="settings-research-source-grid">
|
||||||
{[
|
{[
|
||||||
["pageFetch", t("settings.researchProject.pageFetch", "Page Fetch"), "Default: enabled."],
|
["pageFetch", t("settings.researchProject.pageFetch", "Page Fetch"), "Default: enabled."],
|
||||||
["github", t("settings.researchProject.github", "GitHub"), "Default: disabled."],
|
["github", t("settings.researchProject.github", "GitHub"), "Default: disabled."],
|
||||||
["localDocs", t("settings.researchProject.localDocs", "Local Docs"), "Default: enabled."],
|
["localDocs", t("settings.researchProject.localDocs", "Local Docs"), "Default: enabled."],
|
||||||
["llmSynthesis", t("settings.researchProject.llmSynthesis", "LLM Synthesis"), "Default: enabled."],
|
["llmSynthesis", t("settings.researchProject.llmSynthesis", "LLM Synthesis"), "Default: enabled."],
|
||||||
].map(([key, label, defaultHint]) => (<label key={key} htmlFor={`research-project-source-${key}`} className="checkbox-label">
|
].map(([key, label, defaultHint]) => (<div key={key} className="settings-field-label-row">
|
||||||
<input id={`research-project-source-${key}`} type="checkbox" checked={sources?.[key as keyof NonNullable<typeof sources>] ?? false} onChange={(event) => setForm((current) => ({
|
<label htmlFor={`research-project-source-${key}`} className="checkbox-label">
|
||||||
|
<input id={`research-project-source-${key}`} type="checkbox" checked={sources?.[key as keyof NonNullable<typeof sources>] ?? false} onChange={(event) => setForm((current) => ({
|
||||||
...current,
|
...current,
|
||||||
researchSettings: {
|
researchSettings: {
|
||||||
...(current.researchSettings ?? {}),
|
...(current.researchSettings ?? {}),
|
||||||
@@ -67,9 +74,10 @@ export function ResearchProjectSection({ form, setForm, researchLimitError }: Re
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))}/>
|
}))}/>
|
||||||
{label}
|
{label}
|
||||||
<small>{defaultHint}</small>
|
</label>
|
||||||
</label>))}
|
<SettingsHelpTip settingKey={`research-project-source-${key}`}>{defaultHint}</SettingsHelpTip>
|
||||||
|
</div>))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { MovedSettingsStub } from "./MovedSettingsStub";
|
|||||||
import { SettingsToggleRow } from "../SettingsToggleRow";
|
import { SettingsToggleRow } from "../SettingsToggleRow";
|
||||||
import { SettingsSelectRow } from "../SettingsSelectRow";
|
import { SettingsSelectRow } from "../SettingsSelectRow";
|
||||||
import { SettingsNumberRow } from "../SettingsNumberRow";
|
import { SettingsNumberRow } from "../SettingsNumberRow";
|
||||||
|
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||||
import type { SettingsFormState, SetSettingsForm } from "./context";
|
import type { SettingsFormState, SetSettingsForm } from "./context";
|
||||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||||
const AUTO_ARCHIVE_DEFAULT_AFTER_DAYS = 2;
|
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.
|
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:
|
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 `<code>` 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 `<code>` 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 `<small>`.
|
||||||
*/
|
*/
|
||||||
export function SchedulingSection({ form, setForm, concurrencyLoading = false, onOverlapIgnorePathChange, onOpenOverlapPathPicker, onRemoveOverlapIgnorePath, onAddOverlapIgnorePath, onOpenWorkflowSettings, }: SchedulingSectionProps) {
|
export function SchedulingSection({ form, setForm, concurrencyLoading = false, onOverlapIgnorePathChange, onOpenOverlapPathPicker, onRemoveOverlapIgnorePath, onAddOverlapIgnorePath, onOpenWorkflowSettings, }: SchedulingSectionProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
@@ -301,9 +302,11 @@ export function SchedulingSection({ form, setForm, concurrencyLoading = false, o
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="form-group settings-overlap-ignore-group">
|
<div className="form-group settings-overlap-ignore-group">
|
||||||
<label>{t("settings.scheduling.ignoredOverlapPaths", "Ignored overlap paths")}</label>
|
{/* 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 `<code>` fragments ride along verbatim because SettingsHelpTip takes ReactNode. */}
|
||||||
<small>{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 ")}<code>docs/</code>{t("settings.scheduling.or", " or ")}<code>generated/*</code>{t("settings.scheduling.closeParenPeriod", ").")}
|
<div className="settings-field-label-row">
|
||||||
</small>
|
<label>{t("settings.scheduling.ignoredOverlapPaths", "Ignored overlap paths")}</label>
|
||||||
|
<SettingsHelpTip settingKey="overlapIgnorePaths">{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 ")}<code>docs/</code>{t("settings.scheduling.or", " or ")}<code>generated/*</code>{t("settings.scheduling.closeParenPeriod", ").")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<div className="settings-overlap-ignore-list">
|
<div className="settings-overlap-ignore-list">
|
||||||
{(form.overlapIgnorePaths && form.overlapIgnorePaths.length > 0 ? form.overlapIgnorePaths : [""]).map((path, index) => (<div key={`overlap-ignore-${index}`} className="settings-overlap-ignore-row">
|
{(form.overlapIgnorePaths && form.overlapIgnorePaths.length > 0 ? form.overlapIgnorePaths : [""]).map((path, index) => (<div key={`overlap-ignore-${index}`} className="settings-overlap-ignore-row">
|
||||||
<div className="settings-overlap-ignore-path-controls">
|
<div className="settings-overlap-ignore-path-controls">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { TrackingRepoSelect, type TrackingRepoOption } from "../../TrackingRepoSelect";
|
import { TrackingRepoSelect, type TrackingRepoOption } from "../../TrackingRepoSelect";
|
||||||
import { SettingsSelectRow } from "../SettingsSelectRow";
|
import { SettingsSelectRow } from "../SettingsSelectRow";
|
||||||
import { SettingsTextRow } from "../SettingsTextRow";
|
import { SettingsTextRow } from "../SettingsTextRow";
|
||||||
|
import { SettingsHelpTip } from "../SettingsHelpTip";
|
||||||
import type { SectionBaseProps } from "./context";
|
import type { SectionBaseProps } from "./context";
|
||||||
|
|
||||||
type GlobalGitlabSettings = Pick<GlobalSettings, "gitlabEnabled" | "gitlabInstanceUrl" | "gitlabApiBaseUrl" | "gitlabAuthToken" | "gitlabAuthTokenType">;
|
type GlobalGitlabSettings = Pick<GlobalSettings, "gitlabEnabled" | "gitlabInstanceUrl" | "gitlabApiBaseUrl" | "gitlabAuthToken" | "gitlabAuthTokenType">;
|
||||||
@@ -34,10 +35,13 @@ export function SourceControlGlobalSection({ form, setForm, globalSettings, onGl
|
|||||||
*/
|
*/
|
||||||
const globalGitlab = globalSettings ?? form;
|
const globalGitlab = globalSettings ?? form;
|
||||||
return (<>
|
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. */}
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="globalGithubTrackingDefaultRepo">{t("settings.globalGeneral.globalDefaultTrackingRepo", "Global default tracking repo")}</label>
|
<div className="settings-field-label-row">
|
||||||
|
<label htmlFor="globalGithubTrackingDefaultRepo">{t("settings.globalGeneral.globalDefaultTrackingRepo", "Global default tracking repo")}</label>
|
||||||
|
<SettingsHelpTip settingKey="globalGithubTrackingDefaultRepo">{t("settings.globalGeneral.projectsInheritThisValueWhenTheyDoNot", "Projects inherit this value when they do not set a project default tracking repo. No default — unset.")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<TrackingRepoSelect id="globalGithubTrackingDefaultRepo" ariaLabel="Global default tracking repo" value={form.githubTrackingDefaultRepo ?? ""} options={globalTrackingRepoOptions} loading={globalTrackingRepoLoading} error={globalTrackingRepoError ?? undefined} placeholder={t("settings.globalGeneral.ownerRepo", "owner/repo")} onChange={(nextValue) => setForm((f) => ({ ...f, githubTrackingDefaultRepo: nextValue || undefined }))}/>
|
<TrackingRepoSelect id="globalGithubTrackingDefaultRepo" ariaLabel="Global default tracking repo" value={form.githubTrackingDefaultRepo ?? ""} options={globalTrackingRepoOptions} loading={globalTrackingRepoLoading} error={globalTrackingRepoError ?? undefined} placeholder={t("settings.globalGeneral.ownerRepo", "owner/repo")} onChange={(nextValue) => setForm((f) => ({ ...f, githubTrackingDefaultRepo: nextValue || undefined }))}/>
|
||||||
<small>{t("settings.globalGeneral.projectsInheritThisValueWhenTheyDoNot", "Projects inherit this value when they do not set a project default tracking repo. No default — unset.")}</small>
|
|
||||||
</div>
|
</div>
|
||||||
{/*
|
{/*
|
||||||
FNXC:GitLabEnablement 2026-07-02-00:00:
|
FNXC:GitLabEnablement 2026-07-02-00:00:
|
||||||
@@ -50,8 +54,15 @@ export function SourceControlGlobalSection({ form, setForm, globalSettings, onGl
|
|||||||
<input id="globalGitlabEnabled" type="checkbox" checked={globalGitlab.gitlabEnabled !== false} onChange={(e) => onGlobalGitlabSettingsChange({ gitlabEnabled: e.target.checked })}/>
|
<input id="globalGitlabEnabled" type="checkbox" checked={globalGitlab.gitlabEnabled !== false} onChange={(e) => onGlobalGitlabSettingsChange({ gitlabEnabled: e.target.checked })}/>
|
||||||
{t("settings.globalGeneral.enableGitLabIntegration", "Enable GitLab integration")}
|
{t("settings.globalGeneral.enableGitLabIntegration", "Enable GitLab integration")}
|
||||||
</label>
|
</label>
|
||||||
|
{/*
|
||||||
|
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.
|
||||||
|
*/}
|
||||||
|
<span onClick={(event) => event.stopPropagation()}>
|
||||||
|
<SettingsHelpTip settingKey="global-gitlab-configuration">{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).")}</SettingsHelpTip>
|
||||||
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<small className="settings-description">{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).")}</small>
|
|
||||||
<div className="settings-gitlab-disclosure__body" aria-disabled={globalGitlab.gitlabEnabled === false}>
|
<div className="settings-gitlab-disclosure__body" aria-disabled={globalGitlab.gitlabEnabled === false}>
|
||||||
<SettingsTextRow
|
<SettingsTextRow
|
||||||
descriptor={{
|
descriptor={{
|
||||||
|
|||||||
@@ -141,8 +141,15 @@ export function SourceControlSection({ form, setForm, projectTrackingRepoOptions
|
|||||||
<input id="gitlabEnabled" type="checkbox" checked={form.gitlabEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, gitlabEnabled: e.target.checked }))}/>
|
<input id="gitlabEnabled" type="checkbox" checked={form.gitlabEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, gitlabEnabled: e.target.checked }))}/>
|
||||||
{t("settings.general.enableGitLabIntegration", "Enable GitLab integration")}
|
{t("settings.general.enableGitLabIntegration", "Enable GitLab integration")}
|
||||||
</label>
|
</label>
|
||||||
|
{/*
|
||||||
|
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.
|
||||||
|
*/}
|
||||||
|
<span onClick={(event) => event.stopPropagation()}>
|
||||||
|
<SettingsHelpTip settingKey="project-gitlab-configuration">{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).")}</SettingsHelpTip>
|
||||||
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<small className="settings-description">{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).")}</small>
|
|
||||||
<div className="settings-gitlab-disclosure__body" aria-disabled={form.gitlabEnabled === false}>
|
<div className="settings-gitlab-disclosure__body" aria-disabled={form.gitlabEnabled === false}>
|
||||||
<SettingsTextRow
|
<SettingsTextRow
|
||||||
descriptor={{
|
descriptor={{
|
||||||
@@ -174,9 +181,14 @@ export function SourceControlSection({ form, setForm, projectTrackingRepoOptions
|
|||||||
|
|
||||||
FNXC:SourceControl 2026-07-15-20:30:
|
FNXC:SourceControl 2026-07-15-20:30:
|
||||||
The auth block keeps BOTH its own heading and its own enable/disable hint after the merge: the URL hint above describes what disabling does to imports/refresh, while this one describes the PRIVATE-TOKEN auth contract and the token's global fallback. Neither string is a paraphrase of the other, so collapsing them into one would delete operator-facing copy rather than deduplicate it.
|
The auth block keeps BOTH its own heading and its own enable/disable hint after the merge: the URL hint above describes what disabling does to imports/refresh, while this one describes the PRIVATE-TOKEN auth contract and the token's global fallback. Neither string is a paraphrase of the other, so collapsing them into one would delete operator-facing copy rather than deduplicate it.
|
||||||
|
|
||||||
|
FNXC:SettingsHelp 2026-07-16-12:45:
|
||||||
|
That hint now rides the shared "?" beside the auth heading instead of an inline paragraph — operator requirement: no inline description paragraphs in Settings. The copy stays conditional on `gitlabEnabled` inside the tip.
|
||||||
*/}
|
*/}
|
||||||
<h5 className="settings-section-heading">{t("settings.merge.gitLabAuthentication", "GitLab Authentication")}</h5>
|
<div className="settings-field-label-row">
|
||||||
<small className="settings-description">{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).")}</small>
|
<h5 className="settings-section-heading">{t("settings.merge.gitLabAuthentication", "GitLab Authentication")}</h5>
|
||||||
|
<SettingsHelpTip settingKey="project-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).")}</SettingsHelpTip>
|
||||||
|
</div>
|
||||||
<SettingsSelectRow
|
<SettingsSelectRow
|
||||||
descriptor={{
|
descriptor={{
|
||||||
key: "gitlabAuthTokenType",
|
key: "gitlabAuthTokenType",
|
||||||
|
|||||||
Reference in New Issue
Block a user