diff --git a/.changeset/fn-8448-github-import-back-to-issue-list.md b/.changeset/fn-8448-github-import-back-to-issue-list.md
new file mode 100644
index 0000000000..30006296ea
--- /dev/null
+++ b/.changeset/fn-8448-github-import-back-to-issue-list.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Android and browser Back from a GitHub import detail returns to the issue list first.
+category: fix
+dev: Nested import detail history entry (FN-8228 seam) fixed so first Back clears selection on modal and embedded Import Tasks surfaces.
diff --git a/packages/dashboard/app/components/MobileNavBar.tsx b/packages/dashboard/app/components/MobileNavBar.tsx
index 1407ec4d84..747553a044 100644
--- a/packages/dashboard/app/components/MobileNavBar.tsx
+++ b/packages/dashboard/app/components/MobileNavBar.tsx
@@ -245,8 +245,11 @@ export function MobileNavBar({
navigationHistory.pushNav({ type: "modal", close: closeMore });
}, [closeMore, isMoreOpen, navigationHistory]);
- const dismissMore = useCallback(() => {
- navigationHistory?.removeNav(closeMore);
+ const dismissMore = useCallback((forNavigation?: boolean) => {
+ navigationHistory?.removeNav(
+ closeMore,
+ forNavigation === true ? { preserveHistoryPosition: true } : undefined,
+ );
closeMore();
}, [closeMore, navigationHistory]);
@@ -315,9 +318,15 @@ export function MobileNavBar({
}
}, [dismissMore, resetSheetDrag]);
+ /*
+ FNXC:GitHubImportSwipeBack 2026-07-20-23:12:
+ More actions transition directly into their destination. Preserve the
+ current history position while removing More so its asynchronous back
+ consumption cannot dismiss Import or its nested candidate detail afterward.
+ */
const handleMoreAction = useCallback(
(callback?: () => void) => {
- dismissMore();
+ dismissMore(true);
callback?.();
},
[dismissMore],
@@ -521,7 +530,7 @@ export function MobileNavBar({
<>
dismissMore()}
/>
{
,
);
+ function EmbeddedNavigationHarness({ children, onViewRevert }: { children: ReactNode; onViewRevert: () => void }) {
+ const navigationHistory = useNavigationHistory({ enabled: true });
+
+ useEffect(() => {
+ navigationHistory.pushNav({ type: "view", revert: onViewRevert });
+ return () => navigationHistory.removeNav(onViewRevert);
+ }, [navigationHistory, onViewRevert]);
+
+ return {children};
+ }
+
it("builds a Planning Mode seed with the GitHub issue context", () => {
expect(buildIssuePlanningSeed({
number: 42,
@@ -481,6 +492,13 @@ describe("GitHubImportModal", () => {
expect(screen.queryByTestId(surface === "gitlab" ? "gitlab-import-preview-card" : "github-import-preview-card")).toBeNull();
expect(onClose).not.toHaveBeenCalled();
});
+ expect(screen.getByRole("button", {
+ name: surface === "issue"
+ ? /Select issue #72/i
+ : surface === "pull"
+ ? /Select pull request #73/i
+ : /#73 Swipe GitLab issue/i,
+ })).toBeInTheDocument();
window.dispatchEvent(new PopStateEvent("popstate", { state: { navIndex: 0 } }));
expect(onClose).toHaveBeenCalledTimes(1);
@@ -489,6 +507,38 @@ describe("GitHubImportModal", () => {
}
});
+ it("keeps embedded Import Tasks mounted until its parent view receives a second Back", async () => {
+ const leaveImportTasks = vi.fn();
+ const originalBack = window.history.back;
+ window.history.back = vi.fn();
+ try {
+ vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote);
+ vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce([
+ { number: 75, title: "Embedded swipe issue", body: "Body", html_url: "https://github.com/owner/repo/issues/75", labels: [], state: "open" },
+ ]);
+
+ render(
+
+
+ ,
+ );
+ fireEvent.click(await screen.findByRole("button", { name: /Select issue #75/i }));
+ await screen.findByTestId("github-import-preview-card");
+
+ dispatchDetailBack("popstate");
+ await waitFor(() => {
+ expect(screen.queryByTestId("github-import-preview-card")).toBeNull();
+ expect(screen.getByRole("button", { name: /Select issue #75/i })).toBeInTheDocument();
+ expect(leaveImportTasks).not.toHaveBeenCalled();
+ });
+
+ window.dispatchEvent(new PopStateEvent("popstate", { state: { navIndex: 0 } }));
+ expect(leaveImportTasks).toHaveBeenCalledTimes(1);
+ } finally {
+ window.history.back = originalBack;
+ }
+ });
+
it("drains the detail entry when the sheet closes before a rapid reopen", async () => {
const originalBack = window.history.back;
window.history.back = vi.fn();
diff --git a/packages/dashboard/app/components/__tests__/MobileNavBar.swipe-back.test.tsx b/packages/dashboard/app/components/__tests__/MobileNavBar.swipe-back.test.tsx
index 89f4f56b07..01bbd3a2b4 100644
--- a/packages/dashboard/app/components/__tests__/MobileNavBar.swipe-back.test.tsx
+++ b/packages/dashboard/app/components/__tests__/MobileNavBar.swipe-back.test.tsx
@@ -132,8 +132,22 @@ describe("MobileNavBar More sheet navigation history", () => {
await expectProgrammaticCloseConsumesMoreEntry(() => fireEvent.click(document.querySelector(".mobile-more-sheet-backdrop")!));
});
- it("consumes the More entry on item action", async () => {
- await expectProgrammaticCloseConsumesMoreEntry(() => fireEvent.click(screen.getByTestId("mobile-more-item-activity")));
+ it("replaces the More entry before opening Import so delayed Back cannot consume it", async () => {
+ const importClose = vi.fn();
+ const props = createDefaultProps();
+ props.onOpenGitHubImport = () => {
+ navigationHistory?.pushNav({ type: "modal", close: importClose });
+ };
+ renderWithHistory(props);
+ await openMore();
+
+ fireEvent.click(screen.getByTestId("mobile-more-item-github"));
+
+ expect(window.history.back).not.toHaveBeenCalled();
+ expect(screen.queryByTestId("mobile-more-item-activity")).toBeNull();
+
+ dispatchPopState(0);
+ expect(importClose).toHaveBeenCalledOnce();
});
it("consumes the More entry on Escape", async () => {
diff --git a/packages/dashboard/app/hooks/useNavigationHistory.ts b/packages/dashboard/app/hooks/useNavigationHistory.ts
index 308a7d41a1..f5209c85ad 100644
--- a/packages/dashboard/app/hooks/useNavigationHistory.ts
+++ b/packages/dashboard/app/hooks/useNavigationHistory.ts
@@ -34,10 +34,14 @@ export interface UseNavigationHistoryResult {
/** Replace the top-of-stack entry and call history.replaceState. */
replaceCurrent: (entry: NavEntry) => void;
/**
- * Remove a programmatically-dismissed entry and call history.back so the
- * browser history entry created by pushNav is consumed as well.
+ * Remove a programmatically-dismissed entry. By default history.back()
+ * consumes its browser-history entry; `preserveHistoryPosition` instead
+ * replaces the current state for a close-then-navigate transition.
*/
- removeNav: (closeOrRevert: () => void) => void;
+ removeNav: (
+ closeOrRevert: () => void,
+ options?: { preserveHistoryPosition?: boolean },
+ ) => void;
}
const SELF_POP_FALLBACK_CLEAR_MS = 1_000;
@@ -158,7 +162,7 @@ export function useNavigationHistory(
);
const removeNav = useCallback(
- (closeOrRevert: () => void) => {
+ (closeOrRevert: () => void, options?: { preserveHistoryPosition?: boolean }) => {
if (!enabledRef.current) return;
for (let i = stackRef.current.length - 1; i >= 0; i -= 1) {
@@ -166,6 +170,19 @@ export function useNavigationHistory(
if (getEntryCallback(entry) !== closeOrRevert) continue;
stackRef.current.splice(i, 1);
+
+ /*
+ FNXC:GitHubImportSwipeBack 2026-07-20-23:12:
+ A More-sheet action closes one history surface and immediately opens
+ another. Going back asynchronously after removing More can pop that
+ newly pushed Import/detail entry instead. Keep the browser at its
+ current position and rewrite its nav depth for this atomic transition.
+ */
+ if (options?.preserveHistoryPosition) {
+ writeHistoryState("replace", stackRef.current.length);
+ return;
+ }
+
selfPopRef.current = true;
if (selfPopClearTimerRef.current !== null) {
@@ -180,7 +197,7 @@ export function useNavigationHistory(
return;
}
},
- [], // stable — reads from refs
+ [writeHistoryState],
);
// Register popstate listener. Always registers in browser environments but