diff --git a/.changeset/FN-7680-quick-add-height-parity.md b/.changeset/FN-7680-quick-add-height-parity.md
new file mode 100644
index 0000000000..e57595ccdf
--- /dev/null
+++ b/.changeset/FN-7680-quick-add-height-parity.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Fix Quick Add action-row buttons (Save, Attach, Fast, workflow trigger) rendering at mismatched heights.
+category: fix
+dev: Adds a scoped `min-height` on `.quick-entry-actions .btn` and `.wf-optional-steps-dropdown-trigger` in `QuickEntryBox.css` (desktop base rule, alongside the existing mobile touch-target block) so every action-row control resolves one uniform box height regardless of icon-only vs text content or `.dep-trigger` padding differences. No shared `.btn`/`.btn-sm`/`.btn-icon`/`.btn-task-create`/`.dep-trigger` rules in `styles.css` were touched.
diff --git a/packages/dashboard/app/__tests__/quick-entry-action-row-height-parity.test.tsx b/packages/dashboard/app/__tests__/quick-entry-action-row-height-parity.test.tsx
new file mode 100644
index 0000000000..3af7f5c7c0
--- /dev/null
+++ b/packages/dashboard/app/__tests__/quick-entry-action-row-height-parity.test.tsx
@@ -0,0 +1,256 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { loadAllAppCss, loadAllAppCssBaseOnly, loadStylesCss } from "../test/cssFixture";
+import { render, screen, act } from "@testing-library/react";
+import { QuickEntryBox } from "../components/QuickEntryBox";
+import type { Task } from "@fusion/core";
+import type { BoardWorkflowDefinition } from "../api";
+import { fetchAgents } from "../api";
+
+/*
+FNXC:QuickAddActionRow 2026-07-08-00:00:
+FN-7680 regression coverage. Save (`btn btn-task-create btn-sm`) resolved a
+different box height than its `.quick-entry-actions` siblings (workflow
+trigger, Attach, Fast, Priority, Deps, GitHub toggle, Subtask) because of two
+independent drift sources: text buttons get an 18px line box from the
+inherited body line-height at 12px font-size while icon-only `.btn-icon`
+buttons collapse to their bare icon height (`line-height: 0`), and
+`.dep-trigger` buttons (Priority, Deps, and — pre-FN-7677 — the workflow
+trigger) inherit the shared `.dep-trigger, .inline-create-model-trigger`
+rule's shorter 3px/8px padding instead of `.btn-sm`'s 4px/10px. These tests
+assert a single scoped `min-height` on `.quick-entry-actions .btn` (plus the
+non-`.btn` optional-steps trigger) normalizes every variant to the same total
+box height at desktop widths (the existing `@media (max-width: 768px)`
+touch-target block already did this for mobile), and that the shared global
+`.btn`, `.btn-sm`, `.btn-icon`, `.btn-task-create`, and `.dep-trigger` rules in
+styles.css remain untouched for other surfaces (InlineCreateCard,
+NewTaskModal, TaskDetailModal, TaskForm).
+*/
+
+const mockTasks: Task[] = [
+ {
+ id: "FN-001",
+ title: "Test task 1",
+ description: "First test task",
+ column: "todo",
+ dependencies: [],
+ steps: [],
+ currentStep: 0,
+ log: [],
+ createdAt: "2026-01-01T00:00:00Z",
+ updatedAt: "2026-01-01T00:00:00Z",
+ },
+];
+
+const WORKFLOW_A: BoardWorkflowDefinition = {
+ id: "builtin:coding",
+ name: "Coding",
+ columns: [],
+};
+
+const WORKFLOW_B: BoardWorkflowDefinition = {
+ id: "wf-custom-long-name",
+ name: "A Rather Long Custom Workflow Name That Should Truncate",
+ columns: [],
+};
+
+vi.mock("../api", () => ({
+ fetchModels: vi.fn().mockResolvedValue({
+ models: [],
+ favoriteProviders: [],
+ favoriteModels: [],
+ }),
+ fetchSettings: vi.fn().mockResolvedValue({
+ modelPresets: [],
+ autoSelectModelPreset: false,
+ defaultPresetBySize: {},
+ maxConcurrent: 2,
+ maxWorktrees: 4,
+ pollIntervalMs: 30000,
+ groupOverlappingFiles: true,
+ autoMerge: true,
+ }),
+ fetchAgents: vi.fn().mockResolvedValue([]),
+ fetchWorkflowOptionalSteps: vi.fn().mockResolvedValue([]),
+ uploadAttachment: vi.fn().mockResolvedValue({}),
+ updateGlobalSettings: vi.fn().mockResolvedValue({}),
+}));
+
+vi.mock("lucide-react", () => ({
+ Link: () => null,
+ Paperclip: () => null,
+ Brain: () => null,
+ Lightbulb: () => null,
+ ListTree: () => null,
+ Sparkles: () => null,
+ Save: () => null,
+ X: () => null,
+ ChevronDown: () => null,
+ ChevronUp: () => null,
+ ChevronRight: () => null,
+ Bot: () => null,
+ Server: () => null,
+ Flag: () => null,
+ Maximize2: () => null,
+ Minimize2: () => null,
+}));
+
+vi.mock("../components/ModelSelectionModal", () => ({
+ ModelSelectionModal: () => null,
+}));
+
+vi.mock("../components/CustomModelDropdown", () => ({
+ CustomModelDropdown: ({
+ value,
+ label,
+ }: {
+ value: string;
+ onChange: (value: string) => void;
+ label: string;
+ }) =>
{value || "none"}
,
+}));
+
+function mockDesktopViewport() {
+ Object.defineProperty(window, "innerWidth", { value: 1280, configurable: true });
+ return vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ }));
+}
+
+function renderQuickEntryBox(props: Record = {}) {
+ const defaultProps = {
+ onCreate: vi.fn().mockResolvedValue(undefined),
+ addToast: vi.fn(),
+ tasks: mockTasks,
+ projectId: "test-proj",
+ workflowId: WORKFLOW_A.id,
+ defaultWorkflowId: WORKFLOW_A.id,
+ workflowOptions: [WORKFLOW_A, WORKFLOW_B],
+ };
+ return render();
+}
+
+describe("quick-entry action row height parity (FN-7680)", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.useFakeTimers({ shouldAdvanceTime: true });
+ localStorage.clear();
+ vi.mocked(fetchAgents).mockResolvedValue([]);
+ Object.defineProperty(URL, "createObjectURL", {
+ configurable: true,
+ writable: true,
+ value: vi.fn(() => "blob:mock"),
+ });
+ Object.defineProperty(URL, "revokeObjectURL", {
+ configurable: true,
+ writable: true,
+ value: vi.fn(),
+ });
+ });
+
+ afterEach(async () => {
+ await act(async () => {
+ vi.runOnlyPendingTimers();
+ });
+ vi.useRealTimers();
+ localStorage.clear();
+ });
+
+ it("asserts a base (non-media) uniform min-height on .quick-entry-actions .btn covering desktop widths", () => {
+ const baseOnlyCss = loadAllAppCssBaseOnly();
+
+ const match = baseOnlyCss.match(
+ /\.quick-entry-actions \.btn,\s*\n\s*\.quick-entry-actions \.wf-optional-steps-dropdown-trigger\s*\{[^}]*min-height:\s*([^;]+);/,
+ );
+ expect(match).not.toBeNull();
+ // Not the mobile touch-target literal (calc(var(--space-2xl) + var(--space-xs)));
+ // this must be a distinct desktop-width value declared outside any @media block.
+ expect(match![1].trim()).toBe("calc(var(--space-xl) + var(--space-xs))");
+ });
+
+ it("keeps the existing ≤768px touch-target min-height block applying to all .quick-entry-actions .btn (including Save)", () => {
+ const cssContent = loadAllAppCss();
+
+ // Isolate the known FN-1140/FN-6153/FN-6160 mobile touch-target section by
+ // its marker comment so this assertion cannot accidentally cross into an
+ // unrelated @media block or the desktop base rule further up the file.
+ const sectionStart = cssContent.indexOf("Quick Entry Mobile Touch + Overflow Fixes");
+ expect(sectionStart).toBeGreaterThan(-1);
+ const section = cssContent.slice(sectionStart, sectionStart + 800);
+
+ expect(section).toContain("max-width: 768px");
+ const mobileBlockMatch = section.match(
+ /\.quick-entry-actions \.btn,\s*\n\s*\.quick-entry-actions \.wf-optional-steps-dropdown-trigger\s*\{[^}]*min-height:\s*([^;]+);/,
+ );
+ expect(mobileBlockMatch).not.toBeNull();
+ expect(mobileBlockMatch![1].trim()).toBe("calc(var(--space-2xl) + var(--space-xs))");
+ });
+
+ it("does not modify the shared global .btn, .btn-sm, .btn-icon, .btn-task-create, or .dep-trigger rules in styles.css", () => {
+ const stylesCssContent = loadStylesCss();
+
+ const btnMatch = stylesCssContent.match(/\.btn\s*\{[^}]*padding:\s*([^;]+);/);
+ expect(btnMatch).not.toBeNull();
+ expect(btnMatch![1].trim()).toBe("var(--btn-padding)");
+
+ const btnSmMatch = stylesCssContent.match(/\.btn-sm\s*\{[^}]*padding:\s*([^;]+);/);
+ expect(btnSmMatch).not.toBeNull();
+ expect(btnSmMatch![1].trim()).toBe("4px 10px");
+
+ const btnIconMatch = stylesCssContent.match(/\.btn-icon\s*\{[^}]*line-height:\s*([^;]+);/);
+ expect(btnIconMatch).not.toBeNull();
+ expect(btnIconMatch![1].trim()).toBe("0");
+
+ const btnTaskCreateMatch = stylesCssContent.match(/\.btn-task-create\s*\{[^}]*background:\s*([^;]+);/);
+ expect(btnTaskCreateMatch).not.toBeNull();
+ expect(btnTaskCreateMatch![1].trim()).toBe("var(--cta-bg)");
+
+ const depTriggerMatches = stylesCssContent.match(/\.dep-trigger,\s*\n\s*\.inline-create-model-trigger\s*\{/g);
+ expect(depTriggerMatches).not.toBeNull();
+ expect(depTriggerMatches!.length).toBe(1);
+ const depTriggerMatch = stylesCssContent.match(
+ /\.dep-trigger,\s*\n\s*\.inline-create-model-trigger\s*\{[^}]*padding:\s*([^;]+);/,
+ );
+ expect(depTriggerMatch).not.toBeNull();
+ expect(depTriggerMatch![1].trim()).toBe("3px 8px");
+ });
+
+ it("renders Save, workflow trigger, Attach, Fast, Priority, and Deps as sibling .btn elements in the same .quick-entry-actions row", () => {
+ mockDesktopViewport();
+ renderQuickEntryBox();
+
+ const actionsRow = screen.getByTestId("quick-entry-actions");
+ expect(actionsRow.classList.contains("quick-entry-actions")).toBe(true);
+
+ const save = screen.getByTestId("quick-entry-save");
+ const trigger = screen.getByTestId("quick-entry-workflow-trigger");
+ const attach = screen.getByTestId("quick-entry-attach");
+ const fast = screen.getByTestId("quick-entry-fast-toggle");
+ const priority = screen.getByTestId("quick-entry-priority-button");
+ const deps = screen.getByTestId("quick-entry-deps");
+
+ for (const el of [save, trigger, attach, fast, priority, deps]) {
+ expect(el.classList.contains("btn")).toBe(true);
+ expect(actionsRow.contains(el)).toBe(true);
+ }
+ });
+
+ it("does not render an empty shell for the workflow trigger when fewer than 2 real workflow options exist", () => {
+ mockDesktopViewport();
+ renderQuickEntryBox({ workflowOptions: [WORKFLOW_A] });
+
+ expect(screen.queryByTestId("quick-entry-workflow-trigger")).toBeNull();
+ // Every other action-row control still renders and remains a .btn sibling.
+ const actionsRow = screen.getByTestId("quick-entry-actions");
+ const save = screen.getByTestId("quick-entry-save");
+ const attach = screen.getByTestId("quick-entry-attach");
+ expect(actionsRow.contains(save)).toBe(true);
+ expect(actionsRow.contains(attach)).toBe(true);
+ });
+});
diff --git a/packages/dashboard/app/components/QuickEntryBox.css b/packages/dashboard/app/components/QuickEntryBox.css
index dccdf124b7..a8a8cb8dc9 100644
--- a/packages/dashboard/app/components/QuickEntryBox.css
+++ b/packages/dashboard/app/components/QuickEntryBox.css
@@ -184,6 +184,37 @@ The global `.description-with-refine textarea { padding-right: 70px }` (styles.c
flex: 1 1 auto;
}
+/*
+FNXC:QuickAddActionRow 2026-07-08-00:00:
+FN-7680 — Save (`btn btn-task-create btn-sm`) rendered a different box height
+than its `.quick-entry-actions` siblings. Root cause is content-height drift
+across button variants, not one single property: text buttons (Save, Fast,
+Subtask, the workflow/priority/deps `.dep-trigger` triggers) get an 18px line
+box from the inherited body line-height at 12px font-size, while icon-only
+buttons (Attach, GitHub toggle) collapse to their bare ~14px icon because
+`.btn-icon` sets `line-height: 0`; separately, `.dep-trigger` buttons (Priority,
+Deps, and — pre-FN-7677 — the workflow trigger) inherit the shared
+`.dep-trigger, .inline-create-model-trigger` rule's shorter 3px/8px padding
+instead of `.btn-sm`'s 4px/10px. Rather than chase each contributor
+individually (padding, line-height, icon size) per button variant, assert one
+uniform `min-height` scoped to this row: with the project-wide
+`box-sizing: border-box` reset, min-height normalizes every `.btn` variant in
+`.quick-entry-actions` (plus the non-`.btn` optional-steps trigger) to the same
+total box height regardless of its individual content/padding, without editing
+the shared `.btn`, `.btn-sm`, `.btn-icon`, `.btn-task-create`, or `.dep-trigger`
+rules in styles.css (those still back InlineCreateCard/NewTaskModal/
+TaskDetailModal/TaskForm). This is the desktop-width counterpart to the
+existing mobile touch-target min-height block further down this file (the
+"max-width: 768px" rule) — that block already applies the same
+normalization pattern for mobile; this rule closes the gap for desktop widths
+so the invariant holds at both breakpoints (FN-5751 lesson: no mobile-only or
+desktop-only fix).
+*/
+.quick-entry-actions .btn,
+.quick-entry-actions .wf-optional-steps-dropdown-trigger {
+ min-height: calc(var(--space-xl) + var(--space-xs));
+}
+
/*
FNXC:QuickAddWorkflow 2026-06-30-00:00:
Quick-add workflow targeting sits in the action row as a compact existing-button dropdown. Use tokenized sizing/colors so the selector wraps with Save/Plan controls on narrow Board and List surfaces without introducing a separate visual hierarchy.