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>
This commit is contained in:
gsxdsm
2026-08-15 21:24:25 -07:00
parent 0ec3c76faf
commit d4d13e2fa5
25 changed files with 284 additions and 10 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Add a global Quick Add preference for Enter-to-save behavior.
category: feature
dev: Configure `quickAddSubmitOnEnter` in Settings → Global → General.

View File

@@ -643,6 +643,8 @@ Optional workflow steps declared by the active workflow are available from the q
FNXC:PriorityColorCoding 2026-07-11-00:00: Priority glyphs share urgency colors across Quick Add, the New Task inline row, and task-card badges: low=info/blue, normal=muted, high=warning/amber, urgent=error/red. -->
Quick Add and Inline Create model selection include Plan, Executor, Reviewer, and Merger lanes. Each lane can inherit its default or select a task-specific model; Plan, Reviewer, and Merger also provide independent thinking-level overrides.
Quick Add saves with Enter by default. Operators who prefer multi-line descriptions can disable **Settings → Global → General → Press Enter to save a task in Quick Add**; plain Enter then inserts a newline, while Cmd/Ctrl+Enter saves.
<!-- FNXC:QuickAddStart 2026-07-22-17:45: Coding (Ideas) Start is an atomic Todo create, while ordinary Save and Enter remain create-only in Ideas.
FNXC:QuickAddStart 2026-07-24-11:20: Start is now a visible action-row button for eligible workflows instead of a hidden long-press/right-click menu on Save; eligibility, snapshotting, and fail-closed routing are unchanged. -->

View File

@@ -81,6 +81,7 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`.
| `dashboardFontScalePct` | `number` | `100` | Dashboard font scale percentage used by Appearance settings. Valid range: `85` to `125`; applied pre-hydration via document root font-size so board typography (column headers/counts, task cards, and quick-entry text) scales with the setting from first paint. |
| `dismissModalsOnOutsideClick` | `boolean` | `false` | Global dashboard preference for closing fixed modal overlays by clicking/tapping the backdrop. Off by default to prevent accidental modal dismissal; explicit close, cancel, and Escape paths remain available. |
| `skipConfirmationDialogs` | `boolean` | `false` | Global-only operator preference that skips centralized confirmation dialogs for critical actions. When enabled, destructive actions such as deleting a task or resetting progress immediately take the dialog's primary/default action; project settings cannot enable it for shared-project collaborators. |
| `quickAddSubmitOnEnter` | `boolean` | `true` | Global-only operator keyboard preference for Quick Add. Enter saves by default; disabling it makes Enter insert a newline and reserves Cmd/Ctrl+Enter as the save accelerator. |
| `credentialInstanceId` | `string` | `undefined` | Optional named credential instance for a resolved model selection. A named instance takes precedence over the provider default for that session and is scoped to that provider only; fallback providers retain their own default credentials. A deleted/malformed named instance uses the provider default and records `credentialInstanceMissing` with requested/resolved ids on `session:runtime-resolved`; no provider default is a visible resolution error. |
| `defaultProvider` | `string` | `undefined` | Default AI provider. Anthropic has three independent surfaces, all executing on the direct `anthropic` provider except the CLI: (1) **direct OAuth** — a Claude subscription/OAuth login drives `anthropic/*` selections; Fusion sends the OAuth token to `https://api.anthropic.com/v1` with Claude Code identity headers (the same path the Claude Code CLI uses), so a subscription needs no API key. Credentials live under the `anthropic-subscription` auth/status/usage/banner id but are resolved for the direct provider at runtime; they are never stored or resolved as raw `ANTHROPIC_API_KEY` material. (2) **raw API key** — `ANTHROPIC_API_KEY`, a `models.json` `apiKey`, or an `api_key` auth credential uses `x-api-key` on the same direct provider and takes precedence over OAuth by default (see [`anthropicAuthPreference`](#anthropicauthpreference) to invert this). (3) **Claude CLI** — the explicit `pi-claude-cli` model provider runs sessions through the local `claude` CLI. There is no runtime rerouting between these surfaces. |
| `defaultModelId` | `string` | `undefined` | Default AI model ID. |

View File

@@ -291,6 +291,16 @@ describe("settings defaults invariants", () => {
});
});
describe("quickAddSubmitOnEnter default", () => {
it("defaults Quick Add Enter submission on and global-scoped only", () => {
expect(DEFAULT_GLOBAL_SETTINGS.quickAddSubmitOnEnter).toBe(true);
expect(GLOBAL_SETTINGS_KEYS).toContain("quickAddSubmitOnEnter");
expect("quickAddSubmitOnEnter" in DEFAULT_PROJECT_SETTINGS).toBe(false);
expect(PROJECT_SETTINGS_KEYS).not.toContain("quickAddSubmitOnEnter");
expect(isGlobalOnlySettingsKey("quickAddSubmitOnEnter")).toBe(true);
});
});
describe("mergeIntegrationWorktree default", () => {
it("defaults project settings to reuse-task-worktree", () => {
expect(DEFAULT_PROJECT_SETTINGS.mergeIntegrationWorktree).toBe("reuse-task-worktree");

View File

@@ -145,6 +145,11 @@ export const DEFAULT_GLOBAL_SETTINGS = {
Critical-action confirmation dialogs stay enabled by default. This global-only preference may opt an operator into primary/default auto-approval, but project settings cannot enable it for collaborators.
*/
skipConfirmationDialogs: false,
/*
FNXC:QuickEntry 2026-08-16-03:15:
Quick Add keeps its historical Enter-to-submit behavior by default; only an operator's global preference may opt into newline-first entry.
*/
quickAddSubmitOnEnter: true,
language: undefined,
defaultProvider: undefined,
defaultCredentialInstanceId: undefined,

View File

@@ -353,6 +353,11 @@ export interface GlobalSettings {
* This global operator preference defaults to false. When enabled, the dashboard skips centralized critical-action confirmations and proceeds with their primary/default choice. It must never be project-scoped so shared projects cannot force destructive actions without a prompt.
*/
skipConfirmationDialogs?: boolean;
/**
* FNXC:QuickEntry 2026-08-16-03:15:
* This global-only operator keyboard preference defaults to true to preserve Enter-submits behavior. When disabled, Enter inserts a newline and Cmd/Ctrl+Enter submits; shared projects must never change an operator's keyboard behavior.
*/
quickAddSubmitOnEnter?: boolean;
/** Active UI locale (e.g. `"en"`, `"zh-CN"`, `"fr"`). One of `SUPPORTED_LOCALES`.
* When unset, each surface resolves the locale at runtime (browser/env
* detection) and falls back to `DEFAULT_LOCALE` ("en"). */

View File

@@ -44,6 +44,7 @@ import { useModalManager, type DetailTaskOrigin, type DetailTaskTab } from "./ho
import { useAppSettings } from "./hooks/useAppSettings";
import { useDashboardKeyboardShortcuts } from "./hooks/useDashboardKeyboardShortcuts";
import { ModalDismissPreferenceProvider } from "./hooks/useOverlayDismiss";
import { QuickAddSubmitOnEnterProvider } from "./hooks/useQuickAddSubmitOnEnter";
import { useDeepLink } from "./hooks/useDeepLink";
import { useFavorites } from "./hooks/useFavorites";
import { useAuthOnboarding } from "./hooks/useAuthOnboarding";
@@ -864,6 +865,7 @@ function AppInner() {
quickChatCloseOnOutsideClick,
dashboardKeyboardShortcuts,
dismissModalsOnOutsideClick,
quickAddSubmitOnEnter,
skipConfirmationDialogs,
maxTotalRetriesBeforeFail,
prAuthAvailable,
@@ -1861,6 +1863,7 @@ function AppInner() {
return (
<ConfirmDialogProvider skipConfirmations={skipConfirmationDialogs}>
<ModalDismissPreferenceProvider enabled={dismissModalsOnOutsideClick}>
<QuickAddSubmitOnEnterProvider enabled={quickAddSubmitOnEnter}>
<NavigationHistoryProvider value={{ pushNav, replaceCurrent, removeNav }}>
<FileBrowserProvider openFile={openFileInBrowser}>
<RetryWarningProvider value={maxTotalRetriesBeforeFail * RETRY_WARNING_RATIO}>
@@ -2288,6 +2291,7 @@ function AppInner() {
</RetryWarningProvider>
</FileBrowserProvider>
</NavigationHistoryProvider>
</QuickAddSubmitOnEnterProvider>
</ModalDismissPreferenceProvider>
</ConfirmDialogProvider>
);

View File

@@ -24,6 +24,7 @@ import { getPriorityColorVar, getPriorityIcon, getPriorityLabel } from "../utils
import { validateQuickAddStartWorkflow, workflowSupportsQuickAddStart, resolveQuickAddStartInitialColumn, resolveQuickAddStartTargetColumn, type ValidatedQuickAddWorkflow } from "../utils/quickAddStart";
import { computeFixedMenuPosition, getLayoutViewportSize } from "../utils/fixedMenuPosition";
import { isInsidePortaledModelMenu } from "../utils/portalSurfaces";
import { useQuickAddSubmitOnEnter } from "../hooks/useQuickAddSubmitOnEnter";
const STORAGE_KEY = "kb-quick-entry-text";
const ALLOWED_TASK_ATTACHMENT_TYPES = new Set([
@@ -89,6 +90,8 @@ interface QuickEntryBoxProps {
List view renders quick-add as a COMPACT single-line input so the box isn't tall. When true, the textarea stays one line: isExpanded initializes false, focus does NOT auto-expand it, and auto-resize-to-scrollHeight is short-circuited (capped to the one-line min-height). Board/columns omit singleLine, preserving the tall 80px + auto-grow behavior. singleLine governs only textarea height, not the disclosure/controls panel (which List already collapses via defaultExpanded={false}).
*/
singleLine?: boolean;
/** Explicit override of the global Enter-submit preference. */
submitOnEnter?: boolean;
/**
* Favorited provider IDs from shared app-level state.
* When provided (alongside availableModels), the component uses these
@@ -158,8 +161,10 @@ function hasMeaningfulNodeChoice(nodes: NodeInfo[]): boolean {
return nodes.length > 1 || nodes.some((node) => node.type !== "local");
}
export function QuickEntryBox({ onCreate, onMoveTask, addToast, tasks = [], availableModels, onSubtaskBreakdown, workflowId, workflowOptions, defaultWorkflowId, projectId, autoExpand = true, defaultExpanded = true, singleLine = false, favoriteProviders: parentFavoriteProviders, favoriteModels: parentFavoriteModels, onToggleFavorite: parentToggleFavorite, onToggleModelFavorite: parentToggleModelFavorite, onOpenTask }: QuickEntryBoxProps) {
export function QuickEntryBox({ onCreate, onMoveTask, addToast, tasks = [], availableModels, onSubtaskBreakdown, workflowId, workflowOptions, defaultWorkflowId, projectId, autoExpand = true, defaultExpanded = true, singleLine = false, submitOnEnter, favoriteProviders: parentFavoriteProviders, favoriteModels: parentFavoriteModels, onToggleFavorite: parentToggleFavorite, onToggleModelFavorite: parentToggleModelFavorite, onOpenTask }: QuickEntryBoxProps) {
const { t } = useTranslation("app");
const contextSubmitOnEnter = useQuickAddSubmitOnEnter();
const enterSubmits = submitOnEnter ?? contextSubmitOnEnter;
const [description, setDescription] = useState(() => {
if (typeof window !== "undefined") {
return getScopedItem(STORAGE_KEY, projectId) || "";
@@ -1025,11 +1030,23 @@ export function QuickEntryBox({ onCreate, onMoveTask, addToast, tasks = [], avai
}
return;
}
// Enter without Shift submits
e.preventDefault();
if (duplicateMatches || submitInFlightRef.current) {
e.preventDefault();
return;
}
/*
FNXC:QuickEntry 2026-08-16-03:15:
The global Quick Add option defaults on to preserve historical Enter submission. When disabled, plain Enter remains a browser newline while Cmd/Ctrl+Enter stays the explicit save accelerator.
*/
if (e.metaKey || e.ctrlKey) {
e.preventDefault();
handleSubmit();
return;
}
if (!enterSubmits) {
return;
}
e.preventDefault();
handleSubmit();
} else if (e.key === "Escape") {
e.preventDefault();
@@ -1100,6 +1117,7 @@ export function QuickEntryBox({ onCreate, onMoveTask, addToast, tasks = [], avai
projectId,
setIsDisclosureExpanded,
duplicateMatches,
enterSubmits,
singleLine,
],
);

View File

@@ -0,0 +1,118 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createEvent, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { QuickEntryBox } from "../QuickEntryBox";
import { QuickAddSubmitOnEnterProvider } from "../../hooks/useQuickAddSubmitOnEnter";
import { checkDuplicateTasks } from "../../api";
vi.mock("../../api", () => ({
checkDuplicateTasks: vi.fn().mockResolvedValue([]),
fetchModels: vi.fn().mockResolvedValue({ models: [], favoriteProviders: [], favoriteModels: [] }),
fetchSettings: vi.fn().mockResolvedValue({}),
fetchAuthStatus: vi.fn().mockResolvedValue({ providers: [] }),
fetchAgents: vi.fn().mockResolvedValue([]),
fetchWorkflowOptionalSteps: vi.fn().mockResolvedValue([]),
refineText: vi.fn(),
uploadAttachment: vi.fn().mockResolvedValue({}),
updateGlobalSettings: vi.fn().mockResolvedValue({}),
}));
vi.mock("../../hooks/useComposerDictation", () => ({
useComposerDictation: () => ({ micProps: { enabled: false, supported: false, state: "idle", start: vi.fn(), stop: vi.fn() } }),
}));
vi.mock("../../hooks/useNodes", () => ({ useNodes: () => ({ nodes: [], loading: false, error: null }) }));
const duplicate = { id: "FN-existing", title: "Existing", description: "Existing task", column: "todo", score: 0.9 };
function renderEntry({ enabled, singleLine = false }: { enabled?: boolean; singleLine?: boolean } = {}) {
const onCreate = vi.fn().mockResolvedValue(undefined);
const entry = <QuickEntryBox addToast={vi.fn()} onCreate={onCreate} tasks={[]} singleLine={singleLine} defaultExpanded={false} />;
render(enabled === undefined ? entry : <QuickAddSubmitOnEnterProvider enabled={enabled}>{entry}</QuickAddSubmitOnEnterProvider>);
const textarea = screen.getByPlaceholderText("Add a task...") as HTMLTextAreaElement;
fireEvent.change(textarea, { target: { value: "Task description" } });
return { onCreate, textarea };
}
function pressEnter(textarea: HTMLTextAreaElement, options: KeyboardEventInit = {}) {
const event = createEvent.keyDown(textarea, { key: "Enter", ...options });
fireEvent(textarea, event);
return event;
}
afterEach(() => {
vi.mocked(checkDuplicateTasks).mockReset();
vi.mocked(checkDuplicateTasks).mockResolvedValue([]);
vi.restoreAllMocks();
});
describe("QuickEntryBox submit-on-Enter preference", () => {
it.each([{ singleLine: false, host: "Board" }, { singleLine: true, host: "List" }])("submits on Enter by default in $host Quick Add", async ({ singleLine }) => {
const { onCreate, textarea } = renderEntry({ singleLine });
const event = pressEnter(textarea);
expect(event.defaultPrevented).toBe(true);
await waitFor(() => expect(onCreate).toHaveBeenCalledTimes(1));
});
it("uses the enabled provider in the Board host", async () => {
const { onCreate, textarea } = renderEntry({ enabled: true });
expect(pressEnter(textarea).defaultPrevented).toBe(true);
await waitFor(() => expect(onCreate).toHaveBeenCalledTimes(1));
});
it.each([{ singleLine: false, host: "Board" }, { singleLine: true, host: "List" }])("keeps plain Enter as a browser newline when disabled in $host Quick Add", ({ singleLine }) => {
const { onCreate, textarea } = renderEntry({ enabled: false, singleLine });
const event = pressEnter(textarea);
expect(event.defaultPrevented).toBe(false);
// jsdom does not perform textarea's native default action, so model the browser newline after proving it was allowed.
fireEvent.change(textarea, { target: { value: `${textarea.value}\n` } });
expect(textarea).toHaveValue("Task description\n");
expect(onCreate).not.toHaveBeenCalled();
});
it.each([{ metaKey: true, platform: "Cmd" }, { ctrlKey: true, platform: "Ctrl" }])("uses $platform+Enter as the save accelerator when disabled", async (modifier) => {
const { onCreate, textarea } = renderEntry({ enabled: false });
expect(pressEnter(textarea, modifier).defaultPrevented).toBe(true);
await waitFor(() => expect(onCreate).toHaveBeenCalledTimes(1));
});
it.each([{ enabled: true }, { enabled: false }])("keeps Shift+Enter non-submitting and does not expand the single-line textarea when preference is $enabled", ({ enabled }) => {
const { onCreate, textarea } = renderEntry({ enabled, singleLine: true });
expect(textarea.closest(".quick-entry-box")?.className).not.toContain("expanded");
expect(pressEnter(textarea, { shiftKey: true }).defaultPrevented).toBe(false);
expect(onCreate).not.toHaveBeenCalled();
expect(textarea.closest(".quick-entry-box")?.className).not.toContain("expanded");
});
it.each(["", " \n "])("never creates an empty or whitespace-only task (%j)", async (description) => {
const { onCreate, textarea } = renderEntry({ enabled: true });
fireEvent.change(textarea, { target: { value: description } });
expect(pressEnter(textarea).defaultPrevented).toBe(true);
await waitFor(() => expect(onCreate).not.toHaveBeenCalled());
});
it.each([{ enabled: true }, { enabled: false }])("keeps Enter a no-op while duplicate confirmation is open when preference is $enabled", async ({ enabled }) => {
vi.mocked(checkDuplicateTasks).mockResolvedValueOnce([duplicate]);
const { onCreate, textarea } = renderEntry({ enabled });
pressEnter(textarea, enabled ? {} : { ctrlKey: true });
await screen.findByText("Possible duplicates");
const event = pressEnter(textarea);
expect(event.defaultPrevented).toBe(true);
expect(onCreate).not.toHaveBeenCalled();
});
it.each([{ enabled: true }, { enabled: false }])("keeps Enter a no-op while a submission is in flight when preference is $enabled", async ({ enabled }) => {
let resolveDuplicateCheck!: (matches: typeof duplicate[]) => void;
vi.mocked(checkDuplicateTasks).mockImplementationOnce(() => new Promise((resolve) => { resolveDuplicateCheck = resolve; }));
const { onCreate, textarea } = renderEntry({ enabled });
pressEnter(textarea, enabled ? {} : { ctrlKey: true });
const event = pressEnter(textarea);
expect(event.defaultPrevented).toBe(true);
resolveDuplicateCheck([]);
await waitFor(() => expect(onCreate).toHaveBeenCalledTimes(1));
});
it("uses the same disabled Enter behavior at the mobile viewport", () => {
vi.spyOn(window, "innerWidth", "get").mockReturnValue(375);
const { onCreate, textarea } = renderEntry({ enabled: false });
expect(pressEnter(textarea).defaultPrevented).toBe(false);
expect(onCreate).not.toHaveBeenCalled();
});
});

View File

@@ -1194,6 +1194,24 @@ describe("SettingsModal", () => {
}
});
it("saves quickAddSubmitOnEnter only via global settings payload", async () => {
renderModal({ initialSection: "global-general" });
await waitForSettingsModalReady();
vi.useFakeTimers();
fireEvent.click(screen.getByRole("checkbox", { name: "Press Enter to save a task in Quick Add" }));
await act(async () => { await vi.advanceTimersByTimeAsync(500); });
expect(mockUpdateGlobalSettings).toHaveBeenCalled();
vi.useRealTimers();
const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record<string, unknown>;
expect(globalPayload.quickAddSubmitOnEnter).toBe(false);
if (mockUpdateSettings.mock.calls.length > 0) {
const projectPayload = mockUpdateSettings.mock.calls[0]?.[0] as Record<string, unknown>;
expect(projectPayload.quickAddSubmitOnEnter).toBeUndefined();
}
});
it("saves persistAgentToolOutput only via global settings payload", async () => {
renderModal({ initialSection: "global-general" });
await waitForSettingsModalReady();

View File

@@ -149,6 +149,7 @@ export const GLOBAL_SECTION_KEYS: Record<string, ReadonlySet<string>> = {
"language",
"dismissModalsOnOutsideClick",
"skipConfirmationDialogs",
"quickAddSubmitOnEnter",
"persistAgentToolOutput",
"agentToolOutputMaxChars",
"proactiveTaskChatEnabled",

View File

@@ -31,6 +31,15 @@ export const globalGeneralSearchEntries: SettingsSearchEntry[] = [
" When enabled, destructive actions such as deleting a task or resetting progress run immediately without a prompt. Default: disabled",
keywords: ["confirm", "critical action", "delete", "reset", "destructive"],
},
{
sectionId: "global-general",
key: "quickAddSubmitOnEnter",
labelKey: "settings.globalGeneral.quickAddSubmitOnEnter",
labelFallback: " Press Enter to save a task in Quick Add ",
helpKey: "settings.globalGeneral.quickAddSubmitOnEnterHint",
helpFallback: " Default: enabled. When disabled, Enter inserts a newline and Cmd/Ctrl+Enter saves. ",
keywords: ["enter", "keyboard", "quick add", "newline", "submit"],
},
{
sectionId: "global-general",
key: "persistAgentToolOutput",

View File

@@ -47,6 +47,16 @@ export function GlobalGeneralSection({ form, setForm }: GlobalGeneralSectionProp
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",

View File

@@ -80,6 +80,7 @@ const SETTING_DESCRIPTION_KEYS: Record<string, string> = {
gitlabAuthToken: "globalGeneral.gitLabAuthTokenHint",
dismissModalsOnOutsideClick: "globalGeneral.dismissModalsByClickingOutsideHint",
skipConfirmationDialogs: "globalGeneral.skipConfirmationDialogsHint",
quickAddSubmitOnEnter: "globalGeneral.quickAddSubmitOnEnterHint",
persistAgentToolOutput: "globalGeneral.whenDisabledToolRowsAreStillLoggedBut",
agentToolOutputMaxChars: "globalGeneral.agentToolOutputLimitHint",
proactiveTaskChatEnabled: "globalGeneral.enableProactiveTaskChatHint",

View File

@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { QuickAddSubmitOnEnterProvider, useQuickAddSubmitOnEnter } from "../useQuickAddSubmitOnEnter";
function PreferenceValue() {
return <output>{String(useQuickAddSubmitOnEnter())}</output>;
}
describe("useQuickAddSubmitOnEnter", () => {
it("defaults to enabled without a provider", () => {
render(<PreferenceValue />);
expect(screen.getByText("true")).toBeInTheDocument();
});
it("reads a disabled provider value", () => {
render(<QuickAddSubmitOnEnterProvider enabled={false}><PreferenceValue /></QuickAddSubmitOnEnterProvider>);
expect(screen.getByText("false")).toBeInTheDocument();
});
it("reads an enabled provider value", () => {
render(<QuickAddSubmitOnEnterProvider enabled><PreferenceValue /></QuickAddSubmitOnEnterProvider>);
expect(screen.getByText("true")).toBeInTheDocument();
});
});

View File

@@ -39,6 +39,7 @@ export interface UseAppSettingsResult {
quickChatCloseOnOutsideClick: boolean;
dashboardKeyboardShortcuts: Required<DashboardKeyboardShortcutMap>;
dismissModalsOnOutsideClick: boolean;
quickAddSubmitOnEnter: boolean;
skipConfirmationDialogs: boolean;
showQuickChatFAB: boolean;
maxTotalRetriesBeforeFail: number;
@@ -99,6 +100,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
const [quickChatCloseOnOutsideClick, setQuickChatCloseOnOutsideClick] = useState(true);
const [dashboardKeyboardShortcuts, setDashboardKeyboardShortcuts] = useState<Required<DashboardKeyboardShortcutMap>>(DEFAULT_DASHBOARD_KEYBOARD_SHORTCUTS);
const [dismissModalsOnOutsideClick, setDismissModalsOnOutsideClick] = useState(false);
const [quickAddSubmitOnEnter, setQuickAddSubmitOnEnter] = useState(true);
const [skipConfirmationDialogs, setSkipConfirmationDialogs] = useState(false);
const [showQuickChatFAB, setShowQuickChatFAB] = useState(false);
const [maxTotalRetriesBeforeFail, setMaxTotalRetriesBeforeFail] = useState(25);
@@ -167,6 +169,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
setQuickChatCloseOnOutsideClick(settings.quickChatCloseOnOutsideClick !== false);
setDashboardKeyboardShortcuts(resolveDashboardKeyboardShortcuts((settings as GlobalSettings).dashboardKeyboardShortcuts));
setDismissModalsOnOutsideClick(settings.dismissModalsOnOutsideClick === true);
setQuickAddSubmitOnEnter(settings.quickAddSubmitOnEnter !== false);
setSkipConfirmationDialogs(settings.skipConfirmationDialogs === true);
setShowQuickChatFAB(nextQuickChatButtonMode === "floating");
setMaxTotalRetriesBeforeFail(settings.maxTotalRetriesBeforeFail ?? 25);
@@ -363,6 +366,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
quickChatCloseOnOutsideClick,
dashboardKeyboardShortcuts,
dismissModalsOnOutsideClick,
quickAddSubmitOnEnter,
skipConfirmationDialogs,
showQuickChatFAB,
maxTotalRetriesBeforeFail,

View File

@@ -0,0 +1,21 @@
import { createContext, createElement, useContext, type ReactNode } from "react";
/*
FNXC:QuickEntry 2026-08-16-03:15:
Quick Add renders from the deep Column and ListView hosts without a shared settings prop channel. This context mirrors ModalDismissPreferenceProvider so global keyboard preference reaches both hosts while the true default preserves unwrapped components and existing tests.
*/
const QuickAddSubmitOnEnterContext = createContext(true);
export function QuickAddSubmitOnEnterProvider({
enabled,
children,
}: {
enabled: boolean;
children: ReactNode;
}) {
return createElement(QuickAddSubmitOnEnterContext.Provider, { value: enabled }, children);
}
export function useQuickAddSubmitOnEnter(): boolean {
return useContext(QuickAddSubmitOnEnterContext);
}

View File

@@ -6140,6 +6140,8 @@
"dismissModalsByClickingOutsideHint": " When enabled, clicking or tapping a modal backdrop closes the modal. Default: disabled, to prevent accidental dismissal. ",
"skipConfirmationDialogs": " Skip confirmation dialogs for critical actions ",
"skipConfirmationDialogsHint": " When enabled, destructive actions such as deleting a task or resetting progress run immediately without a prompt. Default: disabled",
"quickAddSubmitOnEnter": " Press Enter to save a task in Quick Add ",
"quickAddSubmitOnEnterHint": " Default: enabled. When disabled, Enter inserts a newline and Cmd/Ctrl+Enter saves. ",
"releaseChannel": "Release channel",
"releaseChannelHelp": "Stable follows official releases. Beta follows pre-releases cut from main and also picks up each stable release once it overtakes the beta. Switching back to Stable never downgrades. Default: stable.",
"autoUpdateAndRestart": " Auto-update and restart ",

View File

@@ -6121,7 +6121,9 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": ""
"noLimitOnAgentToolOutputHint": "",
"quickAddSubmitOnEnter": "",
"quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"allow": "",

View File

@@ -6121,7 +6121,9 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": ""
"noLimitOnAgentToolOutputHint": "",
"quickAddSubmitOnEnter": "",
"quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"allow": "",

View File

@@ -6121,7 +6121,9 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": ""
"noLimitOnAgentToolOutputHint": "",
"quickAddSubmitOnEnter": "",
"quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"allow": "",

View File

@@ -6669,7 +6669,9 @@
"autoUpdateAndRestart": " Atualizar e reiniciar automaticamente ",
"autoUpdateAndRestartHelp": "Quando ativado, o Fusion instala automaticamente as atualizações disponíveis no canal de lançamento selecionado e reinicia para aplicá-las. Requer um processo supervisor pai (o padrão para `fn dashboard`); hosts iniciados com --no-supervise pulam a instalação. Padrão: desativado.",
"channelStable": "Stable (recomendado)",
"channelBeta": "Beta — builds iniciais da main"
"channelBeta": "Beta — builds iniciais da main",
"quickAddSubmitOnEnter": "",
"quickAddSubmitOnEnterHint": ""
},
"notifications": {
"accessTokenOptional": "Token de acesso (opcional)",

View File

@@ -6121,7 +6121,9 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": ""
"noLimitOnAgentToolOutputHint": "",
"quickAddSubmitOnEnter": "",
"quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"allow": "",

View File

@@ -6121,7 +6121,9 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
"noLimitOnAgentToolOutputHint": ""
"noLimitOnAgentToolOutputHint": "",
"quickAddSubmitOnEnter": "",
"quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"allow": "",

View File

@@ -6114,7 +6114,7 @@ export default interface Resources {
"whenEnabledFusionChecksOpenAndClosedIssues": " When enabled, Fusion checks open and closed issues in the target repo for likely duplicates (using File Scope paths and key symptoms) before creating a new tracking issue. Uncheck to always create a new issue. Default: enabled. ",
"whenEnabledImportedGitHubIssuesUseTheirSource": "When enabled, GitHub issue imports become tracked tasks that adopt the source issue. This does not turn GitHub tracking on for ordinary new tasks. Default: disabled.",
"workflowsOrChangelogModeWhenContributorsShouldUpdate": " workflows, or changelog mode when contributors should update an existing changelog file. Default: off. ",
"workspaceModeHint": "When enabled, the project root is treated as a workspace containing multiple git sub-repos. Tasks run per-sub-repo and no git repo is created at the root. Disable for single-repo projects. No default — unset (disabled)."
"workspaceModeHint": "Enabling detects git sub-repositories and requires at least one; it creates .fusion/workspace.json. Disabling removes .fusion/workspace.json and returns to single-repo mode. No default — unset (disabled)."
},
"globalGeneral": {
"agentToolOutputLimit": " Agent tool-output limit ",
@@ -6150,6 +6150,8 @@ export default interface Resources {
"onStartupOncePerServerLaunch": "On startup — once per server launch",
"ownerRepo": "owner/repo",
"projectsInheritThisValueWhenTheyDoNot": "Projects inherit this value when they do not set a project default tracking repo. No default — unset.",
"quickAddSubmitOnEnter": " Press Enter to save a task in Quick Add ",
"quickAddSubmitOnEnterHint": " Default: enabled. When disabled, Enter inserts a newline and Cmd/Ctrl+Enter saves. ",
"releaseChannel": "Release channel",
"releaseChannelHelp": "Stable follows official releases. Beta follows pre-releases cut from main and also picks up each stable release once it overtakes the beta. Switching back to Stable never downgrades. Default: stable.",
"reportRoadmapDedupeEnabled": "Global public-roadmap deduplication",