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>
216 lines
16 KiB
TypeScript
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><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>
|
|
<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;
|