diff --git a/.changeset/fix-mobile-card-swipe-scroll.md b/.changeset/fix-mobile-card-swipe-scroll.md
new file mode 100644
index 0000000000..7295ef65e6
--- /dev/null
+++ b/.changeset/fix-mobile-card-swipe-scroll.md
@@ -0,0 +1,5 @@
+---
+"@runfusion/fusion": patch
+---
+
+Fixed unreliable horizontal scrolling when swiping across task cards on the mobile board. Native HTML5 drag is now disabled on touch-primary devices (where it never worked anyway), so the browser no longer hijacks swipe-to-scroll gestures that start on a card.
diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx
index b45013dea9..b90fafcd2d 100644
--- a/packages/dashboard/app/components/TaskCard.tsx
+++ b/packages/dashboard/app/components/TaskCard.tsx
@@ -18,6 +18,7 @@ import { PrCreateModal } from "./PrCreateModal";
import { ProviderIcon } from "./ProviderIcon";
import { PluginSlot } from "./PluginSlot";
import { useBadgeWebSocket } from "../hooks/useBadgeWebSocket";
+import { useCoarsePointer } from "../hooks/useCoarsePointer";
import { getFreshBatchData } from "../hooks/useBatchBadgeFetch";
import { useTaskDiffStats } from "../hooks/useTaskDiffStats";
import { useAgentsMapCache } from "../hooks/useAgentsMapCache";
@@ -983,7 +984,12 @@ function TaskCardComponent({
const isAwaitingInput = task.status === "awaiting-user-input";
const isArchived = task.column === "archived";
const isAgentActive = !globalPaused && !queued && !isFailed && !isPaused && !isStuck && !isAwaitingApproval && !isAwaitingInput && (task.column === "in-progress" || ACTIVE_STATUSES.has(visualStatus as string));
- const isDraggable = !disableDrag && !queued && !isPaused && !isEditing && !isArchived; // Disable drag during edit/archived or host embedding
+ // Native HTML5 drag is desktop-mouse only — it doesn't move cards via touch.
+ // On touch-primary devices the `draggable` attribute still arms the browser's
+ // touch-drag heuristic, which intermittently hijacks horizontal swipes meant
+ // to scroll the board. Drop drag on coarse pointers so panning stays reliable.
+ const isCoarsePointer = useCoarsePointer();
+ const isDraggable = !disableDrag && !queued && !isPaused && !isEditing && !isArchived && !isCoarsePointer; // Disable drag during edit/archived, host embedding, or touch
// Check if this card can be edited inline
const canEdit = EDITABLE_COLUMNS.has(task.column) && !isAgentActive && !isPaused && !queued && onUpdateTask;
diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx
index 75e346be9b..6f39cd31f4 100644
--- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx
@@ -607,6 +607,33 @@ describe("TaskCard", () => {
expect(card.getAttribute("draggable")).toBe("false");
});
+ // FN-6389 follow-up: native HTML5 drag is desktop-mouse only and doesn't move
+ // cards via touch, but a `draggable` element still arms the browser's touch-drag
+ // heuristic, which intermittently hijacks horizontal swipes meant to scroll the
+ // mobile board. On touch-primary (coarse pointer) devices we drop `draggable`.
+ it("disables native card dragging on touch-primary (coarse pointer) devices", () => {
+ const original = window.matchMedia;
+ window.matchMedia = vi.fn().mockImplementation((query: string) => ({
+ matches: query === "(hover: none) and (pointer: coarse)",
+ media: query,
+ onchange: null,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })) as unknown as typeof window.matchMedia;
+ try {
+ const { container } = render();
+ const card = container.querySelector(".card") as HTMLElement;
+ expect(card.getAttribute("draggable")).toBe("false");
+ // No drag-start handler should be wired on touch (would arm the heuristic).
+ const dragStart = new Event("dragstart", { bubbles: true, cancelable: true });
+ const prevented = !card.dispatchEvent(dragStart);
+ expect(prevented).toBe(false);
+ } finally {
+ window.matchMedia = original;
+ }
+ });
+
it("renders Nx PR badge label when multiple PRs are linked", () => {
render(
(isCoarsePointer);
+
+ useEffect(() => {
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
+
+ const query = window.matchMedia(COARSE_POINTER_MEDIA_QUERY);
+ const update = () => setCoarse(query.matches);
+
+ update();
+ query.addEventListener("change", update);
+ return () => query.removeEventListener("change", update);
+ }, []);
+
+ return coarse;
+}