Files
fusion/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx
gsxdsm d4d13e2fa5 FN-9111: Add global Quick Add Enter-save preference
Add an operator-level setting that controls whether plain Enter saves a Quick Add task while preserving Cmd/Ctrl+Enter submission.

- add the global quickAddSubmitOnEnter setting, defaults, settings UI, and app context
- update Quick Add keyboard handling with multiline and duplicate/in-flight safeguards
- add focused settings and keyboard regression coverage, localization, documentation, and a release changeset

Files changed:
 .changeset/fn-9111-quick-add-enter.md              |   7 ++
 docs/dashboard-guide.md                            |   2 +
 docs/settings-reference.md                         |   1 +
 .../core/src/__tests__/settings-defaults.test.ts   |  10 ++
 packages/core/src/config/settings-schema.ts        |   5 +
 packages/core/src/types/settings/settings-scope.ts |   5 +
 packages/dashboard/app/App.tsx                     |   4 +
 .../dashboard/app/components/QuickEntryBox.tsx     |  24 ++++-
 .../QuickEntryBox.submit-on-enter.test.tsx         | 118 +++++++++++++++++++++
 .../__tests__/SettingsModal.general.test.tsx       |  18 ++++
 .../app/components/settings/save-split.ts          |   1 +
 .../sections/GlobalGeneralSection.search.ts        |   9 ++
 .../settings/sections/GlobalGeneralSection.tsx     |  10 ++
 .../settings-default-descriptions.test.tsx         |   1 +
 .../__tests__/useQuickAddSubmitOnEnter.test.tsx    |  24 +++++
 packages/dashboard/app/hooks/useAppSettings.ts     |   4 +
 .../app/hooks/useQuickAddSubmitOnEnter.ts          |  21 ++++
 packages/i18n/locales/en/app.json                  |   2 +
 packages/i18n/locales/es/app.json                  |   4 +-
 packages/i18n/locales/fr/app.json                  |   4 +-
 packages/i18n/locales/ko/app.json                  |   4 +-
 packages/i18n/locales/pt-BR/app.json               |   4 +-
 packages/i18n/locales/zh-CN/app.json               |   4 +-
 packages/i18n/locales/zh-TW/app.json               |   4 +-
 packages/i18n/src/resources.d.ts                   |   4 +-
 25 files changed, 284 insertions(+), 10 deletions(-)

Fusion-Task-Id: FN-9111

Fusion-Task-Lineage: 246153a8-5127-48df-838c-d9b60920cd91

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-15 21:24:25 -07:00

216 lines
16 KiB
TypeScript

import { resolvePersistAgentThinkingLog } from "@fusion/core";
import { SettingsToggleRow } from "../SettingsToggleRow";
import { SettingsNumberRow } from "../SettingsNumberRow";
import { SettingsSelectRow } from "../SettingsSelectRow";
import { SettingsHelpTip } from "../SettingsHelpTip";
import type { SectionBaseProps } from "./context";
import { useTranslation } from "react-i18next";
export type GlobalGeneralSectionProps = SectionBaseProps;
/*
FNXC:SettingsStyling 2026-07-15-17:35:
Plain settings rows render through the shared primitives rather than hand-rolled `form-group` + `checkbox-label` markup, so labels, help copy, and padding come from one type scale. `.form-group` stays global and untouched — 35 non-settings files style forms with it.
The migrated keys are all global-tier (DEFAULT_GLOBAL_SETTINGS), so each carries a "global" badge stating that it travels between projects.
Rows that stay bespoke are the ones whose copy a single-string descriptor cannot carry without rewording it: the `fn` binary check, the update-check toggle, and the thinking-log group all build label or help from `t()` fragments interleaved with `<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) {
const { t } = useTranslation("app");
return (<>
<h4 className="settings-section-heading">{t("settings.globalGeneral.general", "General")}</h4>
{/*
FNXC:SourceControl 2026-07-15-20:30:
The global GitLab disclosure and the global default tracking repo moved to "Source Control · Global" (SourceControlGlobalSection.tsx), paired with the project source-control section under the Integrations nav group. They are forge integration settings, not general app preferences.
*/}
{/*
FNXC:SettingsNavigation 2026-07-16-01:00:
The `fn` CLI binary panel moved OUT of this section to its own advanced-only "CLI Binary" section at the bottom of the nav. It used to render first here, so a binary install/version/path panel was the first thing an operator saw on opening Settings — machine plumbing above the preferences they came for. Do not move it back.
*/}
<SettingsToggleRow
descriptor={{
key: "dismissModalsOnOutsideClick",
label: t("settings.globalGeneral.dismissModalsByClickingOutside", " Dismiss modals by clicking outside "),
help: t("settings.globalGeneral.dismissModalsByClickingOutsideHint", " When enabled, clicking or tapping a modal backdrop closes the modal. Default: disabled, to prevent accidental dismissal. "),
scope: "global",
}}
value={form.dismissModalsOnOutsideClick === true}
onChange={(v) => setForm((f) => ({ ...f, dismissModalsOnOutsideClick: v === true }))}
/>
<SettingsToggleRow
descriptor={{
key: "skipConfirmationDialogs",
label: t("settings.globalGeneral.skipConfirmationDialogs", " Skip confirmation dialogs for critical actions "),
help: t("settings.globalGeneral.skipConfirmationDialogsHint", " When enabled, destructive actions such as deleting a task or resetting progress run immediately without a prompt. Default: disabled"),
scope: "global",
}}
value={form.skipConfirmationDialogs === true}
onChange={(v) => setForm((f) => ({ ...f, skipConfirmationDialogs: v === true }))}
/>
<SettingsToggleRow
descriptor={{
key: "quickAddSubmitOnEnter",
label: t("settings.globalGeneral.quickAddSubmitOnEnter", " Press Enter to save a task in Quick Add "),
help: t("settings.globalGeneral.quickAddSubmitOnEnterHint", " Default: enabled. When disabled, Enter inserts a newline and Cmd/Ctrl+Enter saves. "),
scope: "global",
}}
value={form.quickAddSubmitOnEnter !== false}
onChange={(v) => setForm((f) => ({ ...f, quickAddSubmitOnEnter: v === true }))}
/>
<SettingsToggleRow
descriptor={{
key: "persistAgentToolOutput",
label: t("settings.globalGeneral.saveToolOutputInAgentLogs", " Save tool output in agent logs "),
help: t("settings.globalGeneral.whenDisabledToolRowsAreStillLoggedBut", " When disabled, tool rows are still logged but detailed tool payloads are omitted. Very large tool payloads may still be clipped even when this stays enabled. Default: disabled. "),
scope: "global",
}}
value={form.persistAgentToolOutput === true}
onChange={(v) => setForm((f) => ({ ...f, persistAgentToolOutput: v === true }))}
/>
<SettingsToggleRow
descriptor={{
key: "agentToolOutputMaxCharsNoLimit",
label: t("settings.globalGeneral.noLimitOnAgentToolOutput", " No limit on agent tool output "),
help: t("settings.globalGeneral.noLimitOnAgentToolOutputHint", " Disable the shared tool-output clamp. A single tool result can consume the agent context window. Default: disabled; when unset, the budget inherits the 16,000-character engine default. "),
scope: "global",
}}
value={form.agentToolOutputMaxChars === 0}
onChange={(v) => setForm((f) => ({ ...f, agentToolOutputMaxChars: v ? 0 : null }))}
/>
<SettingsNumberRow
descriptor={{
key: "agentToolOutputMaxChars",
label: t("settings.globalGeneral.agentToolOutputLimit", " Agent tool-output limit "),
help: t("settings.globalGeneral.agentToolOutputLimitHint", " Maximum characters returned from each engine-injected tool result. When unset, inherits the 16,000-character engine default. Leave empty to use the default. "),
scope: "global",
min: 1,
step: 1000,
placeholder: "16000",
disabled: form.agentToolOutputMaxChars === 0,
}}
value={form.agentToolOutputMaxChars === 0 ? null : (form.agentToolOutputMaxChars ?? null)}
clearable
onChange={(v) => setForm((f) => ({ ...f, agentToolOutputMaxChars: v }))}
/>
<SettingsToggleRow
descriptor={{
key: "proactiveTaskChatEnabled",
label: t("settings.globalGeneral.enableProactiveTaskChat", " Enable proactive task-chat updates "),
help: t("settings.globalGeneral.enableProactiveTaskChatHint", " When enabled, Task chat reports step progress, failures, reviews, and rollbacks in real time. Default: disabled. "),
scope: "global",
}}
value={form.proactiveTaskChatEnabled === true}
onChange={(v) => setForm((f) => ({ ...f, proactiveTaskChatEnabled: v === true }))}
/>
<div className="form-group">
{/* 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">
<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">
<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>
</div>
<div className="form-group">
{/* 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). */}
<div className="settings-field-label-row">
<label htmlFor="fnBinaryCheckEnabled" className="checkbox-label">
<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>
<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>&lt;bin&gt; --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>
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.globalGeneral.updates", "Updates")}</h4>
<div className="form-group">
{/* 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). */}
<div className="settings-field-label-row">
<label htmlFor="updateCheckEnabled" className="checkbox-label">
<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>
{/*
FNXC:SettingsGlobalGeneral 2026-07-15-17:35:
Frequency is disabled rather than hidden while auto-check is off: it describes a cadence that is
not running, and an operator turning checks back on needs to see which cadence will take effect.
*/}
<SettingsSelectRow
descriptor={{
key: "updateCheckFrequency",
label: t("settings.globalGeneral.frequency", "Frequency"),
help: t("settings.globalGeneral.controlsHowOftenTheDashboardReFetchesThe", " Controls how often the dashboard re-fetches the npm registry. Use the version + refresh control in the header to trigger an immediate check at any time. Default: daily. "),
scope: "global",
disabled: form.updateCheckEnabled === false,
options: [
{ value: "manual", label: t("settings.globalGeneral.manualOnlyNeverAutoCheck", "Manual only \u2014 never auto-check") },
{ value: "on-startup", label: t("settings.globalGeneral.onStartupOncePerServerLaunch", "On startup \u2014 once per server launch") },
{ value: "daily", label: t("settings.globalGeneral.dailyRecommended", "Daily (recommended)") },
{ value: "weekly", label: t("settings.globalGeneral.weekly", "Weekly") },
],
}}
value={form.updateCheckFrequency ?? "daily"}
onChange={(v) => setForm((f) => ({
...f,
updateCheckFrequency: v as "manual" | "on-startup" | "daily" | "weekly",
}))}
/>
{/*
FNXC:UpdateChannels 2026-07-19-12:50:
Release track selector. `stable` follows the npm `latest` dist-tag; `beta` follows
the semver-max of `latest` and `beta` so beta users also receive promoted stables.
Deliberately NOT disabled with auto-check off: the channel also governs manual
"Check now" and `fn update`. Switching beta → stable never downgrades — the install
stays on its beta until the next stable overtakes it.
*/}
<SettingsSelectRow
descriptor={{
key: "updateChannel",
label: t("settings.globalGeneral.releaseChannel", "Release channel"),
help: t("settings.globalGeneral.releaseChannelHelp", " Stable follows official releases. Beta follows pre-releases cut from main (versions like 0.73.0-beta.2) and also picks up each stable release once it overtakes the beta. Switching back to Stable never downgrades; you stay on the installed beta until the next stable release passes it. Default: stable. "),
scope: "global",
options: [
{ value: "stable", label: t("settings.globalGeneral.channelStable", "Stable (recommended)") },
{ value: "beta", label: t("settings.globalGeneral.channelBeta", "Beta — early builds from main") },
],
}}
value={form.updateChannel ?? "stable"}
onChange={(v) => setForm((f) => ({
...f,
updateChannel: v as "stable" | "beta",
}))}
/>
{/*
FNXC:AutoUpdate 2026-07-25-10:05:
Operator opt-in for unattended updates, placed directly under the release channel it
follows. Default OFF: Fusion must not replace its own install and bounce the process
without being told to. Only effective on a supervised host (`fn dashboard`), which is
the default launch path; the server-side watcher logs and skips otherwise.
*/}
<SettingsToggleRow
descriptor={{
key: "autoUpdateAndRestart",
label: t("settings.globalGeneral.autoUpdateAndRestart", " Auto-update and restart "),
help: t("settings.globalGeneral.autoUpdateAndRestartHelp", " When enabled, Fusion installs available updates on the selected release channel by itself and then restarts to apply them — the same install + restart the \"Update now\" button performs, without asking. Requires a supervising parent (the default for `fn dashboard`); hosts started with --no-supervise skip the install. Default: disabled. "),
scope: "global",
}}
value={form.autoUpdateAndRestart === true}
onChange={(v) => setForm((f) => ({ ...f, autoUpdateAndRestart: v === true }))}
/>
<SettingsToggleRow
descriptor={{
key: "autoReloadOnVersionChange",
label: t("settings.globalGeneral.autoReloadDashboardOnVersionChange", " Auto-reload dashboard on version change "),
help: t("settings.globalGeneral.whenEnabledDefaultTheDashboardAutomaticallyReloadsWhen", " When enabled (default), the dashboard automatically reloads when it detects a new build version \u2014 either from server rebuilds or service worker updates. Disable this to stay on the current version until you manually refresh. Default: enabled. "),
scope: "global",
}}
value={form.autoReloadOnVersionChange !== false}
onChange={(v) => setForm((f) => ({ ...f, autoReloadOnVersionChange: v === true }))}
/>
</>);
}
export default GlobalGeneralSection;