diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index 049097d9f6..1775102e9e 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -339,8 +339,9 @@ Import Tasks documentation distinguishes Add comment (an upstream GitHub mutatio
-->
5. In an issue detail, use **Add comment** to write a new upstream GitHub comment. The composer remains available for open and closed issues; posting adds the comment to the preview thread immediately. This is separate from **Import as task**, which creates a Fusion resolve-feedback task from an existing GitHub comment.
Expected outcome: Fusion posts the new comment to GitHub, keeps its inline composer available for another response, and shows a success or retryable error message without leaving Import Tasks.
-6. Select the import action in the detail window. Pull requests use **Resolve feedback**, which creates a task to resolve reviewer feedback and address failed CI checks; issues keep **Import**. Each GitHub issue and pull-request comment also has **Import as task**, which creates a separate resolve-feedback task quoting that comment and linking its source without closing the detail window.
- Expected outcome: Fusion creates the requested task, preserves GitHub provenance/tracking metadata, and returns the completed PR/issue import to the list while leaving comment imports available for further feedback.
+
+6. In a GitHub **issue** detail, choose **Import as task** to create the board task directly with GitHub provenance/tracking metadata, or choose **Plan** to close Import Tasks and open Planning Mode with the issue title, body, and source URL as the initial plan. The Planning path does not establish GitHub source-issue tracking or direct-import deduplication. Pull requests continue to use **Resolve feedback**, which creates a task to resolve reviewer feedback and address failed CI checks. Each GitHub issue and pull-request comment also has **Import as task**, which creates a separate resolve-feedback task quoting that comment and linking its source without closing the detail window.
+ Expected outcome: direct import creates the requested tracked task and returns the completed PR/issue import to the list; Plan opens the docked Planning Mode interview with the issue context; comment imports remain available for further feedback.
Leaving and returning to **Import Tasks** (for example switching to Board and back) restores the prior context for the current project — provider (GitHub/GitLab), active Issues/PRs tab, label filter, selected repository/remote, GitLab project/group inputs, the **Hide imported** preference, and the previously selected issue/PR — instead of resetting to defaults. When GitLab integration is disabled in Settings, the GitLab provider tab is hidden and any restored GitLab provider preference opens on GitHub instead; saved GitLab URLs and tokens remain configured. The restored selection re-validates against the freshly reloaded list; a selection that no longer exists (e.g. the issue was closed upstream) clears gracefully rather than showing a stuck or empty preview. First-time opens with no prior state keep the existing default-remote auto-detect behavior. State is scoped per project and does not leak across projects.
@@ -348,8 +349,8 @@ Use GitHub import on mobile:
1. Open the compact Header actions or bottom **More** sheet and select **Import from GitHub**.
Expected outcome: the same import workflow opens in the mobile modal layout.
-2. Choose the repository, issue/PR tab, candidate row, and import action.
- Expected outcome: Fusion creates the board task with the same GitHub provenance/tracking metadata as the desktop/tablet **Import Tasks** view.
+2. Choose the repository, issue/PR tab, candidate row, and detail action. For GitHub issues, choose **Import as task** for direct tracked creation or **Plan** to start Planning Mode with the issue context.
+ Expected outcome: direct import creates the board task with the same GitHub provenance/tracking metadata as the desktop/tablet **Import Tasks** view; Plan opens the Planning Mode interview without source-issue tracking.
3. While a candidate detail sheet is open, use the platform Back gesture or control.
Expected outcome: the first Back dismisses only the issue, pull request, or GitLab detail and returns to the import candidate list; a second Back dismisses the import form.
diff --git a/packages/dashboard/app/components/AppModals.tsx b/packages/dashboard/app/components/AppModals.tsx
index 38bdc3e3a9..3d000797f0 100644
--- a/packages/dashboard/app/components/AppModals.tsx
+++ b/packages/dashboard/app/components/AppModals.tsx
@@ -393,6 +393,7 @@ export function AppModals({
isOpen={modalManager.githubImportOpen}
onClose={closeGitHubImportWithNav}
onImport={taskHandlers.handleGitHubImport}
+ onPlanningMode={onPlanningMode}
tasks={tasks}
projectId={projectId}
/>
diff --git a/packages/dashboard/app/components/GitHubImportModal.tsx b/packages/dashboard/app/components/GitHubImportModal.tsx
index 4b0d4089d6..52d19be2b1 100644
--- a/packages/dashboard/app/components/GitHubImportModal.tsx
+++ b/packages/dashboard/app/components/GitHubImportModal.tsx
@@ -50,6 +50,8 @@ interface GitHubImportModalProps {
isOpen: boolean;
onClose: () => void;
onImport: (task: Task) => void;
+ /** Optional because callers without Planning Mode retain the direct-import-only surface. */
+ onPlanningMode?: (initialPlan: string, workflowId?: string | null) => void;
tasks: Task[];
projectId?: string;
/*
@@ -368,6 +370,23 @@ const ISSUES_PAGE_SIZE = 30;
* FN-8110 lets an operator create a repair task from a failed PR check without retyping its context.
* Keep this prompt composition pure so every check row carries its repository, PR, branch, status, and details-link evidence.
*/
+/*
+FNXC:GitHubImport 2026-07-30-00:00:
+Operators can choose direct task import or Planning Mode for GitHub issues. Planning receives a
+self-contained issue seed, including the source URL, but intentionally does not establish GitHub
+sourceIssue tracking or deduplication; those remain exclusive to direct import.
+*/
+export function buildIssuePlanningSeed(issue: GitHubIssue): string {
+ return [
+ `Plan work for GitHub issue: ${issue.title}`,
+ "",
+ "Issue description:",
+ issue.body?.trim() || "(no description)",
+ "",
+ `Source: ${issue.html_url}`,
+ ].join("\n");
+}
+
export function buildCheckFixTaskPrompt(
pull: GitHubPull,
check: GitHubPullDetail["checks"][number],
@@ -391,7 +410,7 @@ export function buildCheckFixTaskPrompt(
};
}
-export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, presentation = "modal" }: GitHubImportModalProps) {
+export function GitHubImportModal({ isOpen, onClose, onImport, onPlanningMode, tasks, projectId, presentation = "modal" }: GitHubImportModalProps) {
const { isEmbedded, scrollLockEnabled, resizePersistEnabled, escapeEnabled } = useEmbeddedPresentation(presentation);
useMobileScrollLock(isOpen && scrollLockEnabled);
const { t, i18n } = useTranslation("app");
@@ -1160,6 +1179,18 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
}
}, [activeTab, selectedIssueNumber, selectedPullNumber, issues, pulls, owner, repo, projectId, onImport, returnToIssueListAfterSuccess, clearDetailSelection]);
+ const handlePlanIssue = useCallback(() => {
+ const selectedIssue = activeTab === "issues"
+ ? issues.find((issue) => issue.number === selectedIssueNumber)
+ : undefined;
+ if (!selectedIssue || !onPlanningMode || importing || isUrlImported(selectedIssue.html_url)) return;
+
+ const seed = buildIssuePlanningSeed(selectedIssue);
+ // FNXC:GitHubImport 2026-07-30-00:00: Embedded close navigates to Board, so close first and open Planning last to preserve Planning as the final destination.
+ onClose();
+ onPlanningMode(seed);
+ }, [activeTab, importing, isUrlImported, issues, onClose, onPlanningMode, selectedIssueNumber]);
+
const fetchPullDetail = useCallback((force: boolean) => {
const requestId = ++pullDetailRequestRef.current;
if (activeTab !== "pulls" || selectedPullNumber === null || !owner.trim() || !repo.trim()) {
@@ -2280,6 +2311,17 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
{closingIssue ? : t("git.closeIssue", "Close issue")}
)}
+ {activeTab === "issues" && selectedIssue && onPlanningMode && (
+
+ )}
diff --git a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx
index 05001ad5bc..85f83f07c8 100644
--- a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx
+++ b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx
@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { useEffect, type ReactNode } from "react";
import { act, render, screen, fireEvent, waitFor, within } from "@testing-library/react";
-import { GitHubImportModal } from "../GitHubImportModal";
+import { buildIssuePlanningSeed, GitHubImportModal } from "../GitHubImportModal";
import { ConfirmDialogProvider } from "../../hooks/useConfirm";
import { NavigationHistoryProvider, useNavigationHistory } from "../../hooks/useNavigationHistory";
import {
@@ -282,6 +282,85 @@ describe("GitHubImportModal", () => {
,
);
+ it("builds a Planning Mode seed with the GitHub issue context", () => {
+ expect(buildIssuePlanningSeed({
+ number: 42,
+ title: "Plan import",
+ body: "Capture the original issue context.",
+ html_url: "https://github.com/owner/repo/issues/42",
+ labels: [],
+ state: "open",
+ })).toContain("Plan import");
+ expect(buildIssuePlanningSeed({
+ number: 42,
+ title: "Plan import",
+ body: "Capture the original issue context.",
+ html_url: "https://github.com/owner/repo/issues/42",
+ labels: [],
+ state: "open",
+ })).toContain("Capture the original issue context.");
+ expect(buildIssuePlanningSeed({
+ number: 42,
+ title: "Plan import",
+ body: "Capture the original issue context.",
+ html_url: "https://github.com/owner/repo/issues/42",
+ labels: [],
+ state: "open",
+ })).toContain("https://github.com/owner/repo/issues/42");
+ });
+
+ it("plans a selected issue after closing the embedded import surface", async () => {
+ const issue = { number: 42, title: "Plan import", body: "Capture the original issue context.", html_url: "https://github.com/owner/repo/issues/42", labels: [], state: "open" };
+ const sequence: string[] = [];
+ let destination = "import";
+ const onPlanningMode = vi.fn((seed: string) => {
+ sequence.push("planning");
+ destination = "planning";
+ expect(seed).toContain(issue.title);
+ expect(seed).toContain(issue.body);
+ expect(seed).toContain(issue.html_url);
+ });
+ const closeToBoard = vi.fn(() => {
+ sequence.push("board");
+ destination = "board";
+ });
+ vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote);
+ vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce([issue]);
+
+ render();
+ fireEvent.click(await screen.findByRole("button", { name: /Select issue #42/i }));
+ expect(screen.getByTestId("github-import-action-plan")).toBeEnabled();
+ expect(screen.getByTestId("github-import-action-top")).toHaveTextContent("Import as task");
+
+ fireEvent.click(screen.getByTestId("github-import-action-plan"));
+
+ // FNXC:GitHubImport 2026-07-30-00:00: The embedded close routes to Board; Planning must run second so it remains the final destination.
+ expect(sequence).toEqual(["board", "planning"]);
+ expect(destination).toBe("planning");
+ expect(onPlanningMode).toHaveBeenCalledTimes(1);
+ expect(apiImportGitHubIssue).not.toHaveBeenCalled();
+ });
+
+ it("renders Plan only for selectable GitHub issues with Planning Mode", async () => {
+ const issue = { number: 43, title: "Optional plan", body: "Issue body", html_url: "https://github.com/owner/repo/issues/43", labels: [], state: "open" };
+ vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote);
+ vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce([issue]);
+
+ const view = render();
+ fireEvent.click(await screen.findByRole("button", { name: /Select issue #43/i }));
+ expect(screen.getByTestId("github-import-action-plan")).toBeEnabled();
+
+ view.rerender();
+ expect(screen.getByTestId("github-import-action-plan")).toBeDisabled();
+
+ view.unmount();
+ vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote);
+ vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce([issue]);
+ render();
+ fireEvent.click(await screen.findByRole("button", { name: /Select issue #43/i }));
+ expect(screen.queryByTestId("github-import-action-plan")).toBeNull();
+ });
+
const dispatchDetailBack = (delivery: "popstate" | "native") => {
if (delivery === "native") {
const event = new CustomEvent("fusion:native-back", { cancelable: true, detail: { source: "android-back" } });
@@ -728,7 +807,7 @@ describe("GitHubImportModal", () => {
vi.mocked(apiImportGitLabGroupIssue).mockResolvedValueOnce({ ...mockTask, id: "FN-100", title: "Group issue" });
vi.mocked(apiImportGitLabMergeRequest).mockResolvedValueOnce({ ...mockTask, id: "FN-101", title: "Review MR !5: Review me" });
- render();
+ render();
fireEvent.click(await screen.findByRole("button", { name: "GitLab" }));
fireEvent.click(screen.getByRole("tab", { name: "Group issues" }));
@@ -737,6 +816,7 @@ describe("GitHubImportModal", () => {
expect(await screen.findByText(/#7 Group issue/)).toBeInTheDocument();
fireEvent.click(screen.getByText(/#7 Group issue/));
expect(screen.getByTestId("gitlab-import-preview-body")).toHaveTextContent("(no description)");
+ expect(screen.queryByTestId("github-import-action-plan")).toBeNull();
fireEvent.click(screen.getAllByRole("button", { name: "Import" })[0]);
await waitFor(() => expect(apiImportGitLabGroupIssue).toHaveBeenCalledWith(expect.objectContaining({ iid: 7 }), "group", undefined));
@@ -1737,13 +1817,14 @@ describe("GitHubImportModal", () => {
vi.mocked(fetchGitRemotes).mockResolvedValue(singleRemote);
vi.mocked(apiFetchGitHubPulls).mockResolvedValue([{ number: 23, title: "Action PR", body: "body", html_url: "https://github.com/owner/repo/pull/23", headBranch: "feature", baseBranch: "main" }]);
vi.mocked(apiFetchGitHubIssues).mockResolvedValue([{ number: 24, title: "Action issue", body: "body", html_url: "https://github.com/owner/repo/issues/24", labels: [], state: "open", author: "owner" }]);
- render();
+ render();
fireEvent.click(await screen.findByRole("tab", { name: /Pull Requests/i }));
fireEvent.click(await screen.findByRole("button", { name: /Select pull request #23/i }));
expect(await screen.findByRole("button", { name: "Resolve feedback" })).toBeTruthy();
+ expect(screen.queryByTestId("github-import-action-plan")).toBeNull();
fireEvent.click(screen.getByRole("tab", { name: "Issues" }));
fireEvent.click(await screen.findByRole("button", { name: /Select issue #24/i }));
- expect(await screen.findByRole("button", { name: "Import" })).toBeTruthy();
+ expect(await screen.findByRole("button", { name: "Import as task" })).toBeTruthy();
});
// FNXC:GitHubImport 2026-06-23-03:30: The Human filter hides bot comments; All (default) shows both.
@@ -2947,7 +3028,7 @@ describe("GitHubImportModal — detail actions sit at the bottom (operator repor
bottom of the flex panel. Geometry itself was verified in a real browser at 412px.
*/
expect(content!.compareDocumentPosition(bar) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
- expect(within(bar).getByRole("button", { name: /^Import$/i })).toBeTruthy();
+ expect(within(bar).getByRole("button", { name: /^Import as task$/i })).toBeTruthy();
expect(within(bar).getByTestId("github-import-issue-close")).toBeTruthy();
});
diff --git a/packages/dashboard/app/components/dashboard/MainContent.tsx b/packages/dashboard/app/components/dashboard/MainContent.tsx
index 282c0b4109..af03ed2a04 100644
--- a/packages/dashboard/app/components/dashboard/MainContent.tsx
+++ b/packages/dashboard/app/components/dashboard/MainContent.tsx
@@ -662,6 +662,7 @@ export function MainContent({
isOpen={true}
onClose={() => handleChangeTaskView("board")}
onImport={handleGitHubImport}
+ onPlanningMode={openPlanningWithInitialPlanWithNav}
tasks={tasks}
projectId={currentProject?.id}
presentation="embedded"