diff --git a/.changeset/fn-7259-mobile-task-popups.md b/.changeset/fn-7259-mobile-task-popups.md new file mode 100644 index 0000000000..423481f434 --- /dev/null +++ b/.changeset/fn-7259-mobile-task-popups.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add a mobile setting to open board tasks in the existing popup. +category: feature +dev: Adds project setting `openMobileTasksInPopup` and mobile-only board-card routing to task FloatingWindow. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 4ee7b4b2ba..e9475f0b18 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -54,7 +54,10 @@ The **Right Dock Panel** experiment is enabled by default. To disable it, open * When enabled on desktop or tablet project screens, the right dock is a persistent far-right tools sidebar in the project content row. By default it opens as an overlay so the main content does not reflow. Use the dock toolbar pin action to switch into push mode, where the dock becomes an in-flow pane that shrinks the main content beside it; unpinning returns to overlay mode. The selected tool, open/closed state, pinned push-mode state, width, and expanded modal size persist across reloads. -If **Settings → Appearance → Open tasks in the right sidebar** is enabled, board task-card clicks open task detail inside this right dock and keep the board visible. The setting is default off; mobile or hidden/inactive dock states automatically fall back to the existing full-panel task detail, and non-board task-open paths keep their existing behavior. +If **Settings → Appearance → Open tasks in the right sidebar** is enabled, board task-card clicks open task detail inside this right dock and keep the board visible. The setting is default off; mobile or hidden/inactive dock states automatically fall back to the existing full-panel task detail unless the separate mobile popup setting below is enabled, and non-board task-open paths keep their existing behavior. + + +On mobile, **Settings → Appearance → Open mobile tasks as popups** changes only ordinary board task-card clicks. When enabled, those clicks use the existing task popup/FloatingWindow surface instead of the full-panel task detail; deep `changes`/`retries`/`workflow` opens, list/task-detail links, plugin/graph opens, and desktop/tablet routing keep their existing paths. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 92627cf577..b6fa7748df 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -461,6 +461,7 @@ Sandbox backend precedence is: | `recycleWorktrees` | `boolean` | `false` | Default: off (opt-in). Reuse worktrees from a pool for faster startup. | | `showWorktreeGrouping` | `boolean` | `false` | Default: off. When off, WIP/processing columns render plain task cards without worktree group shells or worktree-name labels in both legacy and workflow-mode boards. When on, every WIP/processing column groups tasks by worktree and shows worktree names, including workflow-mode columns flagged as counting toward WIP. | | `openTasksInRightSidebar` | `boolean` | `false` | Default: off. When off, board task-card clicks keep the existing full-panel task detail that replaces the board. When on and the right dock is active on desktop/tablet, board task-card clicks open the task detail in the right sidebar so the board stays visible; mobile or hidden/inactive right-dock states automatically fall back to the full-panel behavior. Non-board task-open paths, including list split detail, right-dock task cards, floating pop-outs, graph/plugin opens, and deep `changes`/`retries`/`workflow` opens, keep their existing behavior. | +| `openMobileTasksInPopup` | `boolean` | `false` | Default: off. When off, mobile board task-card clicks keep the existing full-panel task detail. When on and the dashboard is in mobile viewport mode, ordinary board task-card clicks open the task in the existing task popup/FloatingWindow surface instead. This is mobile-only and board-only: desktop/tablet routing, `openTasksInRightSidebar`, right-dock task cards, list/detail opens, graph/plugin opens, nested task-detail opens, explicit pop-out actions, and deep `changes`/`retries`/`workflow` opens keep their existing behavior. | | `executorAllowSiblingBranchRename` | `boolean` | `false` | Opt back into the legacy executor behavior that silently allocates sibling branches (`fusion/-2`, `-2-2`, …) when the canonical task branch is already checked out elsewhere. When disabled (default), branch conflicts fail loudly and leave the task in `todo` with `status: "failed"` so operators can resolve conflicting branches/worktrees with git tooling before retrying. See [Task Management → Branch conflict handling](./task-management.md#branch-conflict-handling). The dashboard Settings modal exposes the same toggle with warning copy because this legacy mode is discouraged. | | `worktreeNaming` | `"random" \| "task-id" \| "task-title"` | `"random"` | Naming mode for new worktree directories. | diff --git a/packages/core/src/__tests__/settings-defaults.test.ts b/packages/core/src/__tests__/settings-defaults.test.ts index 171192f6ce..8ece9fa3e6 100644 --- a/packages/core/src/__tests__/settings-defaults.test.ts +++ b/packages/core/src/__tests__/settings-defaults.test.ts @@ -150,6 +150,19 @@ describe("settings defaults invariants", () => { }); }); + describe("openMobileTasksInPopup default", () => { + it("keeps openMobileTasksInPopup explicitly false in project defaults", () => { + expect(DEFAULT_PROJECT_SETTINGS.openMobileTasksInPopup).toBe(false); + expect("openMobileTasksInPopup" in DEFAULT_PROJECT_SETTINGS).toBe(true); + expect(PROJECT_SETTINGS_KEYS).toContain("openMobileTasksInPopup"); + }); + + it("keeps openMobileTasksInPopup project-scoped only", () => { + expect("openMobileTasksInPopup" in DEFAULT_GLOBAL_SETTINGS).toBe(false); + expect(GLOBAL_SETTINGS_KEYS).not.toContain("openMobileTasksInPopup"); + }); + }); + describe("quickChatCloseOnOutsideClick default", () => { it("keeps Quick Chat outside-click dismissal explicitly true in project defaults", () => { expect(DEFAULT_PROJECT_SETTINGS.quickChatCloseOnOutsideClick).toBe(true); diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts index 5bfc611a57..8a5c53d4af 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -317,6 +317,11 @@ export const DEFAULT_PROJECT_SETTINGS = { recycleWorktrees: false, showWorktreeGrouping: false, openTasksInRightSidebar: false, + /* + FNXC:MobileTaskPopups 2026-06-29-00:00: + Default off preserves current mobile board-card task detail behavior. The dashboard only consults this project setting for mobile board-card clicks without a deep tab, and reuses the existing task pop-out surface without changing desktop/right-dock routing. + */ + openMobileTasksInPopup: false, executorAllowSiblingBranchRename: false, worktreeNaming: "random", worktrunk: { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 646fddf535..19ecca6d56 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -3717,6 +3717,13 @@ export interface ProjectSettings { * This project-scoped setting is default-off so current board navigation is unchanged. When enabled, only Board card clicks may route to the tablet/desktop right dock; all non-board task-open paths and dock-inactive/mobile states must preserve the full-panel or existing modal behavior. */ openTasksInRightSidebar?: boolean; + /** + * When true, mobile board task-card clicks open task detail in the existing popped-out FloatingWindow task surface instead of the full main-panel task detail. Default: false. + * + * FNXC:MobileTaskPopups 2026-06-29-00:00: + * This project-scoped setting is default-off so mobile board navigation is unchanged until operators opt in. When enabled, it applies only to mobile viewport board-card clicks with no deep initial tab and reuses the existing task pop-out/FloatingWindow path; desktop/tablet right-dock routing and all non-board task-open paths remain governed by their existing settings and handlers. + */ + openMobileTasksInPopup?: boolean; /** When true, restores the legacy behavior of silently creating sibling * branches like `fusion/FN-123-2` when the canonical task branch is already * checked out elsewhere. Default: false. */ diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index 696bb109d0..6771146c80 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -185,6 +185,24 @@ export function shouldOpenBoardTaskInDock(openTasksInRightSidebar: boolean, righ return !initialTab && openTasksInRightSidebar && rightDockActive; } +export type BoardTaskOpenRoute = "popup" | "dock" | "main-panel"; + +export function getBoardTaskOpenRoute(options: { + isMobile: boolean; + openMobileTasksInPopup: boolean; + openTasksInRightSidebar: boolean; + rightDockActive: boolean; + initialTab?: DetailTaskTab; +}): BoardTaskOpenRoute { + if (!options.initialTab && options.isMobile && options.openMobileTasksInPopup) { + return "popup"; + } + if (shouldOpenBoardTaskInDock(options.openTasksInRightSidebar, options.rightDockActive, options.initialTab)) { + return "dock"; + } + return "main-panel"; +} + function AppInner() { const { t } = useTranslation("app"); const { toasts, addToast, removeToast } = useToast(); @@ -539,6 +557,7 @@ function AppInner() { capacityRiskBannerEnabled, capacityRiskTodoThreshold, openTasksInRightSidebar, + openMobileTasksInPopup, quickChatButtonMode, quickChatCloseOnOutsideClick, maxTotalRetriesBeforeFail, @@ -1118,14 +1137,31 @@ function AppInner() { /* FNXC:OpenTasksInRightSidebar 2026-06-28-00:00: Board card clicks are the only task-open path governed by openTasksInRightSidebar. When the project setting is enabled and the tablet/desktop right dock is active, the board keeps its current view and asks the dock controller to render task detail; otherwise the existing full main-panel replacement remains the fallback, including mobile and hidden-footer states. + + FNXC:MobileTaskPopups 2026-06-29-00:00: + Mobile board-card clicks may opt into the existing task pop-out path, but only for ordinary task opens with no deep initial tab. The route is intentionally ordered as mobile popup, then desktop/tablet right dock, then main-panel fallback so the new setting cannot override deep-tab opens, non-board handlers, or desktop right-dock behavior. */ const openBoardTaskDetail = useCallback((task: Task | TaskDetail, initialTab?: DetailTaskTab) => { - if (!shouldOpenBoardTaskInDock(openTasksInRightSidebar, rightDockActive, initialTab)) { - openTaskDetailInMainPanel(task, initialTab); + const route = getBoardTaskOpenRoute({ + isMobile, + openMobileTasksInPopup, + openTasksInRightSidebar, + rightDockActive, + initialTab, + }); + + if (route === "popup") { + popOutTaskDetail(task); return; } - rightDock.openTaskInDock(task); - }, [openTaskDetailInMainPanel, openTasksInRightSidebar, rightDock, rightDockActive]); + + if (route === "dock") { + rightDock.openTaskInDock(task); + return; + } + + openTaskDetailInMainPanel(task, initialTab); + }, [isMobile, openMobileTasksInPopup, openTaskDetailInMainPanel, openTasksInRightSidebar, popOutTaskDetail, rightDock, rightDockActive]); useEffect(() => { if (!openTasksInRightSidebar) { @@ -1598,6 +1634,7 @@ function AppInner() { onClose={close} hideHeader dragHandleSelector=".task-detail-content--embedded > .modal-header" + className="floating-window--task-detail" > { +describe("board task detail routing", () => { it("opens board card clicks in the dock only when the setting and dock surface are both active", () => { expect(shouldOpenBoardTaskInDock(true, true)).toBe(true); expect(shouldOpenBoardTaskInDock(false, true)).toBe(false); @@ -13,4 +13,48 @@ describe("openTasksInRightSidebar board routing", () => { expect(shouldOpenBoardTaskInDock(true, true, "retries")).toBe(false); expect(shouldOpenBoardTaskInDock(true, true, "workflow")).toBe(false); }); + + it("routes mobile board card clicks to the popup only when the mobile popup setting is enabled", () => { + expect(getBoardTaskOpenRoute({ + isMobile: true, + openMobileTasksInPopup: true, + openTasksInRightSidebar: false, + rightDockActive: false, + })).toBe("popup"); + + expect(getBoardTaskOpenRoute({ + isMobile: true, + openMobileTasksInPopup: false, + openTasksInRightSidebar: false, + rightDockActive: false, + })).toBe("main-panel"); + }); + + it("preserves desktop and tablet right-dock routing precedence", () => { + expect(getBoardTaskOpenRoute({ + isMobile: false, + openMobileTasksInPopup: true, + openTasksInRightSidebar: true, + rightDockActive: true, + })).toBe("dock"); + + expect(getBoardTaskOpenRoute({ + isMobile: false, + openMobileTasksInPopup: true, + openTasksInRightSidebar: false, + rightDockActive: true, + })).toBe("main-panel"); + }); + + it("keeps deep-tab opens off the mobile popup path", () => { + for (const initialTab of ["changes", "retries", "workflow"] as const) { + expect(getBoardTaskOpenRoute({ + isMobile: true, + openMobileTasksInPopup: true, + openTasksInRightSidebar: true, + rightDockActive: true, + initialTab, + })).toBe("main-panel"); + } + }); }); diff --git a/packages/dashboard/app/components/FloatingWindow.css b/packages/dashboard/app/components/FloatingWindow.css index 16b16a9c55..aec8f8ff65 100644 --- a/packages/dashboard/app/components/FloatingWindow.css +++ b/packages/dashboard/app/components/FloatingWindow.css @@ -119,6 +119,34 @@ On mobile/narrow app viewports, opening Quick Chat should present the full Chat .floating-window--chat .chat-view { border-radius: 0; } + + /* + FNXC:MobileTaskPopups 2026-06-29-00:00: + Mobile board-card popup opens reuse the existing task-detail FloatingWindow, but the shell must behave like a full-screen mobile sheet. Scope the override to task-detail pop-outs, hide resize handles, and remove drag/resize cursors so no invisible desktop affordances remain on touch layouts. + */ + .floating-window--task-detail { + inset: 0 !important; + width: 100vw !important; + height: 100dvh !important; + min-width: 0 !important; + min-height: 0 !important; + max-width: 100vw !important; + max-height: 100dvh !important; + border: none; + border-radius: 0; + box-shadow: none; + } + + .floating-window--task-detail .floating-window__resize-handle { + display: none; + } + + .floating-window--task-detail .task-detail-content--embedded, + .floating-window--task-detail .task-detail-content--embedded > .modal-header { + border-radius: 0; + cursor: default; + touch-action: auto; + } } .floating-window__title { diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index 8f41c91ff3..d6a7e9e462 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -734,6 +734,7 @@ export function SettingsModal({ recycleWorktrees: false, showWorktreeGrouping: false, openTasksInRightSidebar: false, + openMobileTasksInPopup: false, executorAllowSiblingBranchRename: false, worktreeNaming: "random", worktreeCopyFiles: [], diff --git a/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx b/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx index bc19438a03..6393615d0f 100644 --- a/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx +++ b/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx @@ -61,6 +61,7 @@ describe("FloatingWindow", () => { onClose={() => {}} hideHeader dragHandleSelector=".task-detail-content--embedded > .modal-header" + className="floating-window--task-detail" >
KB-001
@@ -71,12 +72,33 @@ describe("FloatingWindow", () => { expect(screen.queryByTestId("floating-window-drag-handle-task")).toBeNull(); expect(screen.getByTestId("floating-window-task")).toHaveClass("floating-window--headerless"); + expect(screen.getByTestId("floating-window-task")).toHaveClass("floating-window--task-detail"); expect(screen.getByText("KB-001")).toBeInTheDocument(); for (const dir of ["n", "s", "e", "w", "ne", "nw", "se", "sw"]) { expect(screen.getByTestId(`floating-window-resize-${dir}`)).toBeTruthy(); } }); + it("scopes mobile sheet sizing and hidden resize handles to task-detail pop-outs", () => { + expect(floatingWindowCss).toContain("FNXC:MobileTaskPopups 2026-06-29-00:00"); + expect(floatingWindowCss).toContain(".floating-window--task-detail {"); + expect(floatingWindowCss).toContain("width: 100vw !important;"); + expect(floatingWindowCss).toContain("height: 100dvh !important;"); + expect(floatingWindowCss).toContain(".floating-window--task-detail .floating-window__resize-handle"); + expect(floatingWindowCss).toContain("display: none;"); + expect(floatingWindowCss).toContain("cursor: default;"); + expect(floatingWindowCss).toContain("touch-action: auto;"); + }); + + it("does not apply task-detail mobile sizing to chat floating windows", () => { + const taskRuleIndex = floatingWindowCss.indexOf(".floating-window--task-detail {"); + const chatRuleIndex = floatingWindowCss.indexOf(".floating-window--chat {"); + + expect(taskRuleIndex).toBeGreaterThan(-1); + expect(chatRuleIndex).toBeGreaterThan(-1); + expect(taskRuleIndex).not.toBe(chatRuleIndex); + }); + it("focus-to-front: interacting with an older window raises its z-index above the newest", () => { render( <> diff --git a/packages/dashboard/app/components/settings/sections/AppearanceSection.tsx b/packages/dashboard/app/components/settings/sections/AppearanceSection.tsx index 33226333ee..76b9a5f26d 100644 --- a/packages/dashboard/app/components/settings/sections/AppearanceSection.tsx +++ b/packages/dashboard/app/components/settings/sections/AppearanceSection.tsx @@ -44,6 +44,13 @@ export function AppearanceSection({ scopeBanner, form, setForm, themeMode, color {t("settings.appearance.openTasksInRightSidebarHelp", "When enabled, board task cards open detail in the right sidebar when it is available; mobile and hidden-sidebar states keep the full task panel.")}
+
+ + {t("settings.appearance.openMobileTasksInPopupHelp", "When enabled, mobile board task-card clicks use the existing task popup instead of the full-screen task panel. Desktop, right-sidebar, and non-board task opens keep their current behavior.")} +