Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/browser-back-button-spa-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---

Enable browser back-button navigation within the SPA dashboard. Previously, the back button would leave the dashboard entirely. Now it dismisses the top modal or reverts to the previous view, matching standard SPA behavior on both desktop and mobile.
4 changes: 2 additions & 2 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ jobs:
- name: Install Bun
uses: oven-sh/setup-bun@v2

- name: Build plugins
run: pnpm -r --filter './plugins/**' build
- name: Build plugins and core packages
run: pnpm build

- name: Lint
run: pnpm lint
Expand Down
152 changes: 72 additions & 80 deletions packages/dashboard/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,8 @@ function AppInner() {
const viewportMode = useViewportMode();
const isMobile = viewportMode === "mobile";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 isMobile is now a dead variable

All isMobile ? ... : ... ternaries were removed by this PR, leaving isMobile (and potentially viewportMode / the useViewportMode() call) without any remaining consumers. This is dead code and will fail a build with noUnusedLocals: true.

Suggested change
const isMobile = viewportMode === "mobile";
const viewportMode = useViewportMode();

If viewportMode is also no longer used elsewhere in this component, the useViewportMode() call can be removed entirely as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch to check, but isMobile is still actively used in 4 places: keyboard handling (line 327), mobile keyboard open guard (line 334), MobileNavBar mobileNavEnabled prop (line 1194), and project-content CSS class (line 1256). It's not dead code — only the isMobile ? historyHandler : plainHandler ternaries were removed.


// Navigation history for mobile back button / iOS swipe-back.
const { pushNav, replaceCurrent } = useNavigationHistory({ enabled: isMobile });
// Navigation history for browser back button (desktop + mobile).
const { pushNav, replaceCurrent } = useNavigationHistory({ enabled: true });

// View state must be defined before useTasks since useTasks depends on taskView for SSE gating
const { viewMode, setViewMode, taskView, handleChangeTaskView } = useViewState({
Expand All @@ -245,7 +245,7 @@ function AppInner() {

const { views: pluginDashboardViews } = usePluginDashboardViews(currentProject?.id);

// History-aware view change handler — pushes nav entry on mobile.
// History-aware view change handler — pushes nav entry on back-navigation stack.
const handleTaskViewChange = useCallback((newView: TaskView) => {
if (newView === "missions") {
setMissionResumeSessionId(undefined);
Expand All @@ -254,7 +254,6 @@ function AppInner() {
}
const previousView = taskView;
handleChangeTaskView(newView);
// pushNav reads enabledRef internally; isMobile not needed in deps.
if (previousView !== newView) {
pushNav({ type: "view", revert: () => handleChangeTaskView(previousView) });
}
Expand Down Expand Up @@ -498,7 +497,7 @@ function AppInner() {
const {
handleSelectProject,
handleViewAllProjects,
handleOpenSettings,
handleOpenSettings: _handleOpenSettings,
handleAddProject,
handleSetupComplete,
handleModelOnboardingComplete,
Expand Down Expand Up @@ -614,13 +613,13 @@ function AppInner() {
[workflowSteps],
);

const handleOpenNodes = useCallback(() => {
const _handleOpenNodes = useCallback(() => {
if (!nodesEnabled) return;
setNodesOpen((prev) => !prev);
}, [nodesEnabled]);

// History-aware nodes toggle — pushes nav entry only when opening
const handleOpenNodesWithHistory = useCallback(() => {
const handleOpenNodesWithNav = useCallback(() => {
if (!nodesEnabled) return;
if (!nodesOpen) {
setNodesOpen(true);
Expand All @@ -630,50 +629,48 @@ function AppInner() {
}
}, [nodesEnabled, nodesOpen, pushNav]);

// History-aware modal open handlers — push nav entries on mobile only.
// Desktop (isMobile=false): pushNav/replaceCurrent are no-ops.
const openDetailTaskWithHistory = useCallback((task: Task | TaskDetail, tab?: Parameters<typeof modalManager.openDetailTask>[1], opts?: { origin?: DetailTaskOrigin }) => {
// History-aware modal open handlers — push nav entries for back-navigation.
const openDetailTask = useCallback((task: Task | TaskDetail, tab?: Parameters<typeof modalManager.openDetailTask>[1], opts?: { origin?: DetailTaskOrigin }) => {
modalManager.openDetailTask(task, tab, opts);
pushNav({ type: "modal", close: modalManager.closeDetailTask });
}, [modalManager, pushNav]);

const openSettingsWithHistory = useCallback((section?: Parameters<typeof modalManager.openSettings>[0]) => {
const openSettingsWithNav = useCallback((section?: Parameters<typeof modalManager.openSettings>[0]) => {
modalManager.openSettings(section);
pushNav({ type: "modal", close: handleSettingsClose });
}, [modalManager, pushNav, handleSettingsClose]);

const openNewTaskWithHistory = useCallback(() => {
const openNewTaskWithNav = useCallback(() => {
modalManager.openNewTask();
pushNav({ type: "modal", close: modalManager.closeNewTask });
}, [modalManager, pushNav]);

const openPlanningWithHistory = useCallback(() => {
const openPlanningWithNav = useCallback(() => {
modalManager.openPlanning();
pushNav({ type: "modal", close: modalManager.closePlanning });
}, [modalManager, pushNav]);

const openPlanningWithInitialPlanWithHistory = useCallback((initialPlan: string) => {
const openPlanningWithInitialPlanWithNav = useCallback((initialPlan: string) => {
modalManager.openPlanningWithInitialPlan(initialPlan);
pushNav({ type: "modal", close: modalManager.closePlanning });
}, [modalManager, pushNav]);

const resumePlanningWithHistory = useCallback(() => {
const resumePlanningWithNav = useCallback(() => {
modalManager.resumePlanning();
pushNav({ type: "modal", close: modalManager.closePlanning });
}, [modalManager, pushNav]);

const openSubtaskBreakdownWithHistory = useCallback((description: string) => {
const openSubtaskBreakdownWithNav = useCallback((description: string) => {
modalManager.openSubtaskBreakdown(description);
pushNav({ type: "modal", close: modalManager.closeSubtask });
}, [modalManager, pushNav]);

const openGitHubImportWithHistory = useCallback(() => {
const openGitHubImportWithNav = useCallback(() => {
modalManager.openGitHubImport();
pushNav({ type: "modal", close: modalManager.closeGitHubImport });
}, [modalManager, pushNav]);

const toggleTerminalWithHistory = useCallback(() => {
// Only push if terminal is currently closed (opening)
const toggleTerminalWithNav = useCallback(() => {
if (!modalManager.terminalOpen) {
modalManager.toggleTerminal();
pushNav({ type: "modal", close: modalManager.closeTerminal });
Expand All @@ -682,59 +679,59 @@ function AppInner() {
}
}, [modalManager, pushNav]);

const openFilesWithHistory = useCallback(() => {
const openFilesWithNav = useCallback(() => {
modalManager.openFiles();
pushNav({ type: "modal", close: modalManager.closeFiles });
}, [modalManager, pushNav]);

const openTodosWithHistory = useCallback(() => {
const openTodosWithNav = useCallback(() => {
modalManager.openTodos();
pushNav({ type: "modal", close: modalManager.closeTodos });
}, [modalManager, pushNav]);

const openActivityLogWithHistory = useCallback(() => {
const openActivityLogWithNav = useCallback(() => {
modalManager.openActivityLog();
pushNav({ type: "modal", close: modalManager.closeActivityLog });
}, [modalManager, pushNav]);

const openGitManagerWithHistory = useCallback(() => {
const openGitManagerWithNav = useCallback(() => {
modalManager.openGitManager();
pushNav({ type: "modal", close: modalManager.closeGitManager });
}, [modalManager, pushNav]);

const openSystemStatsWithHistory = useCallback(() => {
const openSystemStatsWithNav = useCallback(() => {
modalManager.openSystemStats();
pushNav({ type: "modal", close: modalManager.closeSystemStats });
}, [modalManager, pushNav]);

const openSchedulesWithHistory = useCallback(() => {
const openSchedulesWithNav = useCallback(() => {
modalManager.openSchedules();
pushNav({ type: "modal", close: modalManager.closeSchedules });
}, [modalManager, pushNav]);

const openScriptsWithHistory = useCallback(() => {
const openScriptsWithNav = useCallback(() => {
modalManager.openScripts();
pushNav({ type: "modal", close: modalManager.closeScripts });
}, [modalManager, pushNav]);

const openWorkflowStepsWithHistory = useCallback(() => {
const openWorkflowStepsWithNav = useCallback(() => {
modalManager.openWorkflowSteps();
pushNav({ type: "modal", close: modalManager.closeWorkflowSteps });
}, [modalManager, pushNav]);

const openUsageWithHistory = useCallback((anchorRect?: DOMRect | null) => {
const openUsageWithNav = useCallback((anchorRect?: DOMRect | null) => {
modalManager.openUsage(anchorRect);
pushNav({ type: "modal", close: modalManager.closeUsage });
}, [modalManager, pushNav]);

// Modal-to-modal transition: scripts -> terminal uses replaceCurrent
const runScriptWithHistory = useCallback(async (name: string, command: string) => {
const runScriptWithNav = useCallback(async (name: string, command: string) => {
await modalManager.runScript(name, command);
replaceCurrent({ type: "modal", close: modalManager.closeTerminal });
}, [modalManager, replaceCurrent]);

// Modal-to-modal transition: settings -> onboarding uses replaceCurrent
const reopenOnboardingWithHistory = useCallback(() => {
const reopenOnboardingWithNav = useCallback(() => {
modalManager.closeSettings();
modalManager.openModelOnboarding();
replaceCurrent({ type: "modal", close: modalManager.closeModelOnboarding });
Expand Down Expand Up @@ -878,14 +875,12 @@ function AppInner() {
projectId: currentProject?.id,
tasks: isRemote && remoteData.tasks.length > 0 ? remoteData.tasks : tasks,
workflowSteps,
openTaskDetail: isMobile
? (task: Task | TaskDetail, initialTab?: DetailTaskTab) => openDetailTaskWithHistory(task, initialTab)
: (task: Task | TaskDetail, initialTab?: DetailTaskTab) => modalManager.openDetailTask(task, initialTab),
openTaskDetail: (task: Task | TaskDetail, initialTab?: DetailTaskTab) => openDetailTask(task, initialTab),
renderTaskCard: (task: Task | TaskDetail) => (
<TaskCard
task={task}
projectId={currentProject?.id}
onOpenDetail={isMobile ? (value: Task | TaskDetail) => openDetailTaskWithHistory(value) : (value: Task | TaskDetail) => modalManager.openDetailTask(value)}
onOpenDetail={(value: Task | TaskDetail) => openDetailTask(value)}
addToast={addToast}
workflowStepNameLookup={workflowStepNameLookup}
/>
Expand Down Expand Up @@ -964,7 +959,7 @@ function AppInner() {
projectId={currentProject?.id}
onSelectTask={(taskId) => {
const task = tasks.find((t) => t.id === taskId);
if (task) (isMobile ? openDetailTaskWithHistory : modalManager.openDetailTask)(task as TaskDetail);
if (task) openDetailTask(task as TaskDetail);
}}
availableTasks={tasks.map((t) => ({ id: t.id, title: t.title }))}
resumeSessionId={missionResumeSessionId}
Expand Down Expand Up @@ -998,7 +993,7 @@ function AppInner() {
<DocumentsView
projectId={currentProject?.id}
addToast={addToast}
onOpenDetail={isMobile ? openDetailTaskWithHistory : modalManager.openDetailTask}
onOpenDetail={openDetailTask}
/>
</Suspense>
</PageErrorBoundary>
Expand Down Expand Up @@ -1076,12 +1071,12 @@ function AppInner() {
maxConcurrent={maxConcurrent}
onMoveTask={moveTask}
onPauseTask={pauseTask}
onOpenDetail={isMobile ? openDetailTaskWithHistory : modalManager.openDetailTask}
onOpenDetail={openDetailTask}
addToast={addToast}
onQuickCreate={handleBoardQuickCreate}
onNewTask={isMobile ? openNewTaskWithHistory : modalManager.openNewTask}
onPlanningMode={isMobile ? openPlanningWithInitialPlanWithHistory : modalManager.openPlanningWithInitialPlan}
onSubtaskBreakdown={isMobile ? openSubtaskBreakdownWithHistory : modalManager.openSubtaskBreakdown}
onNewTask={openNewTaskWithNav}
onPlanningMode={openPlanningWithInitialPlanWithNav}
onSubtaskBreakdown={openSubtaskBreakdownWithNav}
autoMerge={autoMerge}
onToggleAutoMerge={toggleAutoMerge}
globalPaused={globalPaused}
Expand Down Expand Up @@ -1119,13 +1114,13 @@ function AppInner() {
onMergeTask={mergeTask}
onResetTask={resetTask}
onDuplicateTask={duplicateTask}
onOpenDetail={isMobile ? (task, options) => openDetailTaskWithHistory(task, undefined, options) : (task, options) => modalManager.openDetailTask(task, undefined, options)}
onOpenDetail={(task, options) => openDetailTask(task, undefined, options)}
addToast={addToast}
globalPaused={globalPaused}
onNewTask={isMobile ? openNewTaskWithHistory : modalManager.openNewTask}
onNewTask={openNewTaskWithNav}
onQuickCreate={handleBoardQuickCreate}
onPlanningMode={isMobile ? openPlanningWithInitialPlanWithHistory : modalManager.openPlanningWithInitialPlan}
onSubtaskBreakdown={isMobile ? openSubtaskBreakdownWithHistory : modalManager.openSubtaskBreakdown}
onPlanningMode={openPlanningWithInitialPlanWithNav}
onSubtaskBreakdown={openSubtaskBreakdownWithNav}
availableModels={availableModels}
favoriteProviders={favoriteProviders}
favoriteModels={favoriteModels}
Expand Down Expand Up @@ -1160,27 +1155,27 @@ function AppInner() {
<>
<Header
isElectron={isElectron}
onOpenSettings={isMobile ? openSettingsWithHistory : handleOpenSettings}
onOpenGitHubImport={isMobile ? openGitHubImportWithHistory : modalManager.openGitHubImport}
onOpenPlanning={isMobile ? openPlanningWithHistory : modalManager.openPlanning}
onResumePlanning={isMobile ? resumePlanningWithHistory : modalManager.resumePlanning}
onOpenSettings={openSettingsWithNav}
onOpenGitHubImport={openGitHubImportWithNav}
onOpenPlanning={openPlanningWithNav}
onResumePlanning={resumePlanningWithNav}
activePlanningSessionCount={bgPlanningSessions.length}
onOpenUsage={isMobile ? openUsageWithHistory : modalManager.openUsage}
onOpenActivityLog={isMobile ? openActivityLogWithHistory : modalManager.openActivityLog}
onOpenSystemStats={isMobile ? openSystemStatsWithHistory : modalManager.openSystemStats}
onOpenUsage={openUsageWithNav}
onOpenActivityLog={openActivityLogWithNav}
onOpenSystemStats={openSystemStatsWithNav}
onOpenMailbox={() => handleTaskViewChange("mailbox")}
mailboxUnreadCount={mailboxUnreadCount}
onOpenSchedules={isMobile ? openSchedulesWithHistory : modalManager.openSchedules}
onOpenGitManager={isMobile ? openGitManagerWithHistory : modalManager.openGitManager}
onOpenNodes={isMobile ? handleOpenNodesWithHistory : handleOpenNodes}
onOpenSchedules={openSchedulesWithNav}
onOpenGitManager={openGitManagerWithNav}
onOpenNodes={handleOpenNodesWithNav}
showNodesButton={nodesEnabled}
onOpenWorkflowSteps={isMobile ? openWorkflowStepsWithHistory : modalManager.openWorkflowSteps}
onOpenScripts={isMobile ? openScriptsWithHistory : modalManager.openScripts}
onRunScript={isMobile ? runScriptWithHistory : modalManager.runScript}
onToggleTerminal={isMobile ? toggleTerminalWithHistory : modalManager.toggleTerminal}
onOpenFiles={isMobile ? openFilesWithHistory : modalManager.openFiles}
onOpenWorkflowSteps={openWorkflowStepsWithNav}
onOpenScripts={openScriptsWithNav}
onRunScript={runScriptWithNav}
onToggleTerminal={toggleTerminalWithNav}
onOpenFiles={openFilesWithNav}
filesOpen={modalManager.filesOpen}
onOpenTodos={isMobile ? openTodosWithHistory : modalManager.openTodos}
onOpenTodos={openTodosWithNav}
todosOpen={modalManager.todosOpen}
todosEnabled={todosEnabled}
globalPaused={globalPaused}
Expand Down Expand Up @@ -1285,27 +1280,27 @@ function AppInner() {
footerVisible={viewMode === "project" && !!currentProject}
modalOpen={modalManager.anyModalOpen}
keyboardOpen={mobileKeyboardOpen}
onOpenSettings={isMobile ? openSettingsWithHistory : handleOpenSettings}
onOpenActivityLog={isMobile ? openActivityLogWithHistory : modalManager.openActivityLog}
onOpenSystemStats={isMobile ? openSystemStatsWithHistory : modalManager.openSystemStats}
onOpenSettings={openSettingsWithNav}
onOpenActivityLog={openActivityLogWithNav}
onOpenSystemStats={openSystemStatsWithNav}
onOpenMailbox={() => handleTaskViewChange("mailbox")}
onOpenNodes={isMobile ? handleOpenNodesWithHistory : handleOpenNodes}
onOpenNodes={handleOpenNodesWithNav}
mailboxUnreadCount={mailboxUnreadCount}
onOpenGitManager={isMobile ? openGitManagerWithHistory : modalManager.openGitManager}
onOpenWorkflowSteps={isMobile ? openWorkflowStepsWithHistory : modalManager.openWorkflowSteps}
onOpenSchedules={isMobile ? openSchedulesWithHistory : modalManager.openSchedules}
onOpenScripts={isMobile ? openScriptsWithHistory : modalManager.openScripts}
onToggleTerminal={isMobile ? toggleTerminalWithHistory : modalManager.toggleTerminal}
onOpenFiles={isMobile ? openFilesWithHistory : modalManager.openFiles}
onOpenTodos={isMobile ? openTodosWithHistory : modalManager.openTodos}
onOpenGitManager={openGitManagerWithNav}
onOpenWorkflowSteps={openWorkflowStepsWithNav}
onOpenSchedules={openSchedulesWithNav}
onOpenScripts={openScriptsWithNav}
onToggleTerminal={toggleTerminalWithNav}
onOpenFiles={openFilesWithNav}
onOpenTodos={openTodosWithNav}
todosOpen={modalManager.todosOpen}
onOpenGitHubImport={isMobile ? openGitHubImportWithHistory : modalManager.openGitHubImport}
onOpenPlanning={isMobile ? openPlanningWithHistory : modalManager.openPlanning}
onResumePlanning={isMobile ? resumePlanningWithHistory : modalManager.resumePlanning}
onOpenGitHubImport={openGitHubImportWithNav}
onOpenPlanning={openPlanningWithNav}
onResumePlanning={resumePlanningWithNav}
activePlanningSessionCount={bgPlanningSessions.length}
onOpenUsage={isMobile ? () => openUsageWithHistory(null) : () => modalManager.openUsage(null)}
onOpenUsage={() => openUsageWithNav(null)}
onViewAllProjects={handleViewAllProjects}
onRunScript={isMobile ? runScriptWithHistory : modalManager.runScript}
onRunScript={runScriptWithNav}
projectId={currentProject?.id}
showSkillsTab={skillsEnabled}
experimentalFeatures={{
Expand Down Expand Up @@ -1357,10 +1352,7 @@ function AppInner() {
deepLink={{ handleDetailClose }}
settings={{ prAuthAvailable, themeMode, colorTheme, dashboardFontScalePct, setThemeMode, setColorTheme, setDashboardFontScalePct }}
onSettingsClose={handleSettingsClose}
onReopenOnboarding={isMobile ? reopenOnboardingWithHistory : () => {
modalManager.closeSettings();
modalManager.openModelOnboarding();
}}
onReopenOnboarding={reopenOnboardingWithNav}
/>
<AuthTokenRecoveryDialog open={authTokenRecoveryOpen} />
{shellApi && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1167,8 +1167,11 @@ describe("PlanningModeModal", () => {
expect(screen.getByText("What is the scope?")).toBeDefined();
});

fireEvent.click(screen.getByText("Medium"));
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
const mediumOption = await screen.findByText("Medium");
fireEvent.click(mediumOption);

const continueBtn = await screen.findByRole("button", { name: "Continue" });
fireEvent.click(continueBtn);

await waitFor(() => {
expect(screen.getByText("What are the key requirements?")).toBeDefined();
Expand Down
Loading
Loading