diff --git a/.changeset/fn-9111-quick-add-enter.md b/.changeset/fn-9111-quick-add-enter.md
new file mode 100644
index 0000000000..0ff443d073
--- /dev/null
+++ b/.changeset/fn-9111-quick-add-enter.md
@@ -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.
diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index 6c3fc1357d..367a395d36 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -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.
+
diff --git a/docs/settings-reference.md b/docs/settings-reference.md
index 5b90697ae1..ce7d6980a5 100644
--- a/docs/settings-reference.md
+++ b/docs/settings-reference.md
@@ -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. |
diff --git a/packages/core/src/__tests__/settings-defaults.test.ts b/packages/core/src/__tests__/settings-defaults.test.ts
index f8f9dd629d..a25afeab3d 100644
--- a/packages/core/src/__tests__/settings-defaults.test.ts
+++ b/packages/core/src/__tests__/settings-defaults.test.ts
@@ -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");
diff --git a/packages/core/src/config/settings-schema.ts b/packages/core/src/config/settings-schema.ts
index bd0b9303c3..440840ef17 100644
--- a/packages/core/src/config/settings-schema.ts
+++ b/packages/core/src/config/settings-schema.ts
@@ -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,
diff --git a/packages/core/src/types/settings/settings-scope.ts b/packages/core/src/types/settings/settings-scope.ts
index d278183938..425254c2b6 100644
--- a/packages/core/src/types/settings/settings-scope.ts
+++ b/packages/core/src/types/settings/settings-scope.ts
@@ -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"). */
diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx
index 766554c42c..7eeaed9def 100644
--- a/packages/dashboard/app/App.tsx
+++ b/packages/dashboard/app/App.tsx
@@ -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 (
+
@@ -2288,6 +2291,7 @@ function AppInner() {
+
);
diff --git a/packages/dashboard/app/components/QuickEntryBox.tsx b/packages/dashboard/app/components/QuickEntryBox.tsx
index b063295cc3..62ab7011ef 100644
--- a/packages/dashboard/app/components/QuickEntryBox.tsx
+++ b/packages/dashboard/app/components/QuickEntryBox.tsx
@@ -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,
],
);
diff --git a/packages/dashboard/app/components/__tests__/QuickEntryBox.submit-on-enter.test.tsx b/packages/dashboard/app/components/__tests__/QuickEntryBox.submit-on-enter.test.tsx
new file mode 100644
index 0000000000..61e1b4e4e4
--- /dev/null
+++ b/packages/dashboard/app/components/__tests__/QuickEntryBox.submit-on-enter.test.tsx
@@ -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 = ;
+ render(enabled === undefined ? entry : {entry});
+ 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();
+ });
+});
diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx
index 31ecfff413..862b4be59a 100644
--- a/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx
+++ b/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx
@@ -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;
+ expect(globalPayload.quickAddSubmitOnEnter).toBe(false);
+ if (mockUpdateSettings.mock.calls.length > 0) {
+ const projectPayload = mockUpdateSettings.mock.calls[0]?.[0] as Record;
+ expect(projectPayload.quickAddSubmitOnEnter).toBeUndefined();
+ }
+ });
+
it("saves persistAgentToolOutput only via global settings payload", async () => {
renderModal({ initialSection: "global-general" });
await waitForSettingsModalReady();
diff --git a/packages/dashboard/app/components/settings/save-split.ts b/packages/dashboard/app/components/settings/save-split.ts
index 84da30b4bb..17598ec6ab 100644
--- a/packages/dashboard/app/components/settings/save-split.ts
+++ b/packages/dashboard/app/components/settings/save-split.ts
@@ -149,6 +149,7 @@ export const GLOBAL_SECTION_KEYS: Record> = {
"language",
"dismissModalsOnOutsideClick",
"skipConfirmationDialogs",
+ "quickAddSubmitOnEnter",
"persistAgentToolOutput",
"agentToolOutputMaxChars",
"proactiveTaskChatEnabled",
diff --git a/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.search.ts b/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.search.ts
index b5f8725153..a563e6f7df 100644
--- a/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.search.ts
+++ b/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.search.ts
@@ -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",
diff --git a/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx b/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx
index f0bb9a2035..adf7e19db7 100644
--- a/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx
+++ b/packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx
@@ -47,6 +47,16 @@ export function GlobalGeneralSection({ form, setForm }: GlobalGeneralSectionProp
value={form.skipConfirmationDialogs === true}
onChange={(v) => setForm((f) => ({ ...f, skipConfirmationDialogs: v === true }))}
/>
+ setForm((f) => ({ ...f, quickAddSubmitOnEnter: v === true }))}
+ />
= {
gitlabAuthToken: "globalGeneral.gitLabAuthTokenHint",
dismissModalsOnOutsideClick: "globalGeneral.dismissModalsByClickingOutsideHint",
skipConfirmationDialogs: "globalGeneral.skipConfirmationDialogsHint",
+ quickAddSubmitOnEnter: "globalGeneral.quickAddSubmitOnEnterHint",
persistAgentToolOutput: "globalGeneral.whenDisabledToolRowsAreStillLoggedBut",
agentToolOutputMaxChars: "globalGeneral.agentToolOutputLimitHint",
proactiveTaskChatEnabled: "globalGeneral.enableProactiveTaskChatHint",
diff --git a/packages/dashboard/app/hooks/__tests__/useQuickAddSubmitOnEnter.test.tsx b/packages/dashboard/app/hooks/__tests__/useQuickAddSubmitOnEnter.test.tsx
new file mode 100644
index 0000000000..c0d79dd128
--- /dev/null
+++ b/packages/dashboard/app/hooks/__tests__/useQuickAddSubmitOnEnter.test.tsx
@@ -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 ;
+}
+
+describe("useQuickAddSubmitOnEnter", () => {
+ it("defaults to enabled without a provider", () => {
+ render();
+ expect(screen.getByText("true")).toBeInTheDocument();
+ });
+
+ it("reads a disabled provider value", () => {
+ render();
+ expect(screen.getByText("false")).toBeInTheDocument();
+ });
+
+ it("reads an enabled provider value", () => {
+ render();
+ expect(screen.getByText("true")).toBeInTheDocument();
+ });
+});
diff --git a/packages/dashboard/app/hooks/useAppSettings.ts b/packages/dashboard/app/hooks/useAppSettings.ts
index 9c40c6a28d..17c9afccb9 100644
--- a/packages/dashboard/app/hooks/useAppSettings.ts
+++ b/packages/dashboard/app/hooks/useAppSettings.ts
@@ -39,6 +39,7 @@ export interface UseAppSettingsResult {
quickChatCloseOnOutsideClick: boolean;
dashboardKeyboardShortcuts: Required;
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>(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,
diff --git a/packages/dashboard/app/hooks/useQuickAddSubmitOnEnter.ts b/packages/dashboard/app/hooks/useQuickAddSubmitOnEnter.ts
new file mode 100644
index 0000000000..e2509731a6
--- /dev/null
+++ b/packages/dashboard/app/hooks/useQuickAddSubmitOnEnter.ts
@@ -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);
+}
diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json
index 19e5120eaa..c8de983c21 100644
--- a/packages/i18n/locales/en/app.json
+++ b/packages/i18n/locales/en/app.json
@@ -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 ",
diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json
index 3299cf0a7e..d460f4ffd3 100644
--- a/packages/i18n/locales/es/app.json
+++ b/packages/i18n/locales/es/app.json
@@ -6121,7 +6121,9 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
- "noLimitOnAgentToolOutputHint": ""
+ "noLimitOnAgentToolOutputHint": "",
+ "quickAddSubmitOnEnter": "",
+ "quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"allow": "",
diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json
index 285e4c653b..6859235677 100644
--- a/packages/i18n/locales/fr/app.json
+++ b/packages/i18n/locales/fr/app.json
@@ -6121,7 +6121,9 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
- "noLimitOnAgentToolOutputHint": ""
+ "noLimitOnAgentToolOutputHint": "",
+ "quickAddSubmitOnEnter": "",
+ "quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"allow": "",
diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json
index 9d4ddbede1..9a731dfd50 100644
--- a/packages/i18n/locales/ko/app.json
+++ b/packages/i18n/locales/ko/app.json
@@ -6121,7 +6121,9 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
- "noLimitOnAgentToolOutputHint": ""
+ "noLimitOnAgentToolOutputHint": "",
+ "quickAddSubmitOnEnter": "",
+ "quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"allow": "",
diff --git a/packages/i18n/locales/pt-BR/app.json b/packages/i18n/locales/pt-BR/app.json
index 5eb98e8e28..dd365c44dd 100644
--- a/packages/i18n/locales/pt-BR/app.json
+++ b/packages/i18n/locales/pt-BR/app.json
@@ -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)",
diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json
index d7e7626b9c..49a41aa114 100644
--- a/packages/i18n/locales/zh-CN/app.json
+++ b/packages/i18n/locales/zh-CN/app.json
@@ -6121,7 +6121,9 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
- "noLimitOnAgentToolOutputHint": ""
+ "noLimitOnAgentToolOutputHint": "",
+ "quickAddSubmitOnEnter": "",
+ "quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"allow": "",
diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json
index 785d28ad34..29d664eae5 100644
--- a/packages/i18n/locales/zh-TW/app.json
+++ b/packages/i18n/locales/zh-TW/app.json
@@ -6121,7 +6121,9 @@
"agentToolOutputLimit": "",
"agentToolOutputLimitHint": "",
"noLimitOnAgentToolOutput": "",
- "noLimitOnAgentToolOutputHint": ""
+ "noLimitOnAgentToolOutputHint": "",
+ "quickAddSubmitOnEnter": "",
+ "quickAddSubmitOnEnterHint": ""
},
"globalModels": {
"allow": "",
diff --git a/packages/i18n/src/resources.d.ts b/packages/i18n/src/resources.d.ts
index af4ee872ec..b47dc707a6 100644
--- a/packages/i18n/src/resources.d.ts
+++ b/packages/i18n/src/resources.d.ts
@@ -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",