diff --git a/.changeset/fn-7614-planning-badge.md b/.changeset/fn-7614-planning-badge.md new file mode 100644 index 0000000000..e0a2555063 --- /dev/null +++ b/.changeset/fn-7614-planning-badge.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Planning Mode "needs input" now shows a yellow nav badge instead of a top banner. +category: fix +dev: Excludes planning `awaiting_input` sessions from SessionNotificationBanner and adds a `status-dot--pending` dot to the Planning nav destination (LeftSidebarNav + MobileNavBar More item/tab), driven by a new `planningNeedsInput` flag. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 6de422c05d..0e71295775 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -415,6 +415,10 @@ Use **Show worktree grouping on the board** in **Settings → Worktrees** when y Planning is a desktop/tablet left-sidebar main-content destination after **Command Center**. It opens the planning-session list and composer in the main content region; mobile continues to use the compact planning entry points. Planning Mode now includes branch controls on the summary screen before you create a task. + + +When a Planning session is awaiting your input, look for the yellow needs-input dot on the Planning nav destination (desktop left sidebar; mobile More sheet item and More tab icon) rather than a banner — clicking Planning always opens the correct docked Planning view. + Before Planning Mode shows **Planning Complete!** or the final plan summary, it first asks **Would you like to go deeper?**. Select one or more suggested themes to continue the interview, use **Other** to add a custom topic, or choose **No, continue to final summary** to reveal the pending summary and task-creation actions. diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index 8264e175c1..14c135e307 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -92,6 +92,7 @@ import { requiresNativeShellOnboarding, shouldShowFirstEverBootLoader, isSessionNeedingInputForBanner, + isPlanningAwaitingInput, getCliActionDisabledReasonForBanner, executeCliSessionBannerAction, } from "./utils/appLifecycle"; @@ -103,6 +104,7 @@ export { requiresNativeShellOnboarding, shouldShowFirstEverBootLoader, isSessionNeedingInputForBanner, + isPlanningAwaitingInput, getCliActionDisabledReasonForBanner, executeCliSessionBannerAction, } from "./utils/appLifecycle"; @@ -365,9 +367,16 @@ function AppInner() { /* * FNXC:SessionBanner 2026-06-14-19:32: * CLI agent sessions use `waiting_on_input` and `needs_attention` to represent user-actionable states. The banner feed must include those statuses in addition to the legacy planning-session statuses so visible CLI actions cannot be silently hidden from users. + * + * FNXC:SessionBanner 2026-07-05-00:00: + * Planning `awaiting_input` sessions are excluded from the banner feed: the banner's Resume button did not + * reliably redirect into Planning Mode. That signal now surfaces as a yellow `status-dot--pending` nav badge + * (see `planningNeedsInput` below) whose click target is the already-correct `planning` view navigation. + * Planning sessions in `error` status are unaffected and still render in the banner. */ - const sessionsNeedingInput = bgSessions.filter(isSessionNeedingInputForBanner); + const sessionsNeedingInput = bgSessions.filter((s) => isSessionNeedingInputForBanner(s) && !isPlanningAwaitingInput(s)); const sessionBannersHidden = useSessionBannersHidden(); + const planningNeedsInput = bgPlanningSessions.some((s) => s.status === "awaiting_input"); // Modal state/handlers - required before useViewState const modalManager = useModalManager({ @@ -1586,6 +1595,7 @@ function AppInner() { mailboxUnreadCount={mailboxUnreadCount} mailboxPendingApprovalCount={mailboxPendingApprovalCount} chatHasUnreadResponse={chatHasUnreadResponse} + planningNeedsInput={planningNeedsInput} experimentalFeatures={{ insights: insightsEnabled, memoryView: memoryEnabled, @@ -1668,6 +1678,7 @@ function AppInner() { onOpenPlanning={openPlanningWithNav} onResumePlanning={resumePlanningWithNav} activePlanningSessionCount={bgPlanningSessions.length} + planningNeedsInput={planningNeedsInput} onOpenUsage={() => openUsageWithNav(null)} onViewAllProjects={handleViewAllProjects} onRunScript={runScriptWithNav} diff --git a/packages/dashboard/app/components/LeftSidebarNav.tsx b/packages/dashboard/app/components/LeftSidebarNav.tsx index b3c726adc3..219c0e8abb 100644 --- a/packages/dashboard/app/components/LeftSidebarNav.tsx +++ b/packages/dashboard/app/components/LeftSidebarNav.tsx @@ -109,6 +109,13 @@ export interface LeftSidebarNavProps { mailboxUnreadCount?: number; mailboxPendingApprovalCount?: number; chatHasUnreadResponse?: boolean; + /* + FNXC:Navigation 2026-07-05-00:00: + Planning Mode "awaiting input" no longer shows a top-of-board banner (its Resume button did not reliably + redirect). Instead this flag drives a yellow `status-dot--pending` dot on the Planning nav destination, + mirroring `chatHasUnreadResponse` exactly, so the click target is always the working `planning` nav item. + */ + planningNeedsInput?: boolean; experimentalFeatures?: LeftSidebarExperimentalFeatures; pluginDashboardViews?: PluginDashboardViewEntry[]; showAgentsTab?: boolean; @@ -156,6 +163,7 @@ export function LeftSidebarNav({ mailboxUnreadCount = 0, mailboxPendingApprovalCount = 0, chatHasUnreadResponse = false, + planningNeedsInput = false, experimentalFeatures, pluginDashboardViews = [], showAgentsTab = false, @@ -324,6 +332,8 @@ export function LeftSidebarNav({ isActive: view === "planning", icon: Lightbulb, testId: "sidebar-nav-planning", + // FNXC:Navigation 2026-07-05-00:00: mirrors the chat item's `dot` below — replaces the broken-Resume banner. + dot: planningNeedsInput && view !== "planning" ? "pending" : undefined, onSelect: () => onChangeView("planning"), }, { diff --git a/packages/dashboard/app/components/MobileNavBar.css b/packages/dashboard/app/components/MobileNavBar.css index 68ab6b80ac..95cdb5d50f 100644 --- a/packages/dashboard/app/components/MobileNavBar.css +++ b/packages/dashboard/app/components/MobileNavBar.css @@ -263,6 +263,26 @@ Wrap every tab icon in the same token-sized icon slot and keep unread/pending do color: var(--text-muted); } +/* +FNXC:Navigation 2026-07-05-00:00: +Planning "awaiting input" moved from a top-of-board banner (broken Resume redirect) to a yellow needs-input dot on +this More-sheet item, mirroring the mobile-nav-tab icon-wrapper pattern so the dot positions relative to the icon +rather than the full-width row. +*/ +.mobile-more-item-icon-wrapper { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; +} + +.mobile-more-item-icon-dot { + position: absolute; + top: -2px; + right: -2px; +} + .mobile-more-item-badge { margin-left: auto; min-width: 20px; diff --git a/packages/dashboard/app/components/MobileNavBar.tsx b/packages/dashboard/app/components/MobileNavBar.tsx index 9b8556a84d..b27467449c 100644 --- a/packages/dashboard/app/components/MobileNavBar.tsx +++ b/packages/dashboard/app/components/MobileNavBar.tsx @@ -88,6 +88,14 @@ export interface MobileNavBarProps { onOpenPlanning?: () => void; onResumePlanning?: () => void; activePlanningSessionCount?: number; + /* + FNXC:Navigation 2026-07-05-00:00: + Planning Mode "awaiting input" no longer shows a top-of-board banner (its Resume button did not reliably + redirect). Instead this flag drives a yellow `status-dot--pending` dot on the Planning More-sheet item and the + More tab icon, mirroring `chatHasUnreadResponse`'s `mobile-nav-chat-unread-dot`, so the click target is always + the working Planning navigation. + */ + planningNeedsInput?: boolean; onOpenUsage?: () => void; onRunScript?: (name: string, command: string) => void; projectId?: string; @@ -149,6 +157,7 @@ export function MobileNavBar({ onOpenPlanning, onResumePlanning, activePlanningSessionCount = 0, + planningNeedsInput = false, onOpenUsage, onRunScript, projectId, @@ -469,6 +478,9 @@ export function MobileNavBar({ > + {planningNeedsInput && view !== "planning" && !isMoreOpen && ( + + )} {t("nav.more", "More")} @@ -613,7 +625,12 @@ export function MobileNavBar({ data-testid="mobile-more-item-planning" onClick={() => handleMoreAction(planningHandler)} > - + + + {planningNeedsInput && ( + + )} + {t("nav.planning", "Planning")} {activePlanningSessionCount > 0 && ( {formatCount(activePlanningSessionCount)} diff --git a/packages/dashboard/app/components/__tests__/LeftSidebarNav.test.tsx b/packages/dashboard/app/components/__tests__/LeftSidebarNav.test.tsx index efbb08a26e..403b89e675 100644 --- a/packages/dashboard/app/components/__tests__/LeftSidebarNav.test.tsx +++ b/packages/dashboard/app/components/__tests__/LeftSidebarNav.test.tsx @@ -647,4 +647,32 @@ describe("LeftSidebarNav", () => { expect(() => fireEvent.click(screen.getByTestId("sidebar-nav-settings"))).not.toThrow(); }); + + /* + FNXC:Navigation 2026-07-05-00:00: + FN-7614: planning-awaiting-input moved from a top-of-board banner (broken Resume redirect) to a yellow + `status-dot--pending` dot on this Planning nav destination, mirroring `chatHasUnreadResponse` exactly. + */ + describe("planningNeedsInput dot (FN-7614)", () => { + it("shows the pending status dot on the Planning item when planningNeedsInput is true and not on the planning view", () => { + renderSidebar({ planningNeedsInput: true, view: "board" }); + + const planningButton = screen.getByTestId("sidebar-nav-planning"); + expect(planningButton.querySelector(".status-dot.status-dot--pending")).toBeTruthy(); + }); + + it("hides the dot when the user is already on the planning view", () => { + renderSidebar({ planningNeedsInput: true, view: "planning" }); + + const planningButton = screen.getByTestId("sidebar-nav-planning"); + expect(planningButton.querySelector(".status-dot.status-dot--pending")).toBeNull(); + }); + + it("hides the dot when planningNeedsInput is false", () => { + renderSidebar({ planningNeedsInput: false, view: "board" }); + + const planningButton = screen.getByTestId("sidebar-nav-planning"); + expect(planningButton.querySelector(".status-dot.status-dot--pending")).toBeNull(); + }); + }); }); diff --git a/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx b/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx index 1ca8148688..b7794bc14c 100644 --- a/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx +++ b/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx @@ -557,6 +557,47 @@ describe("MobileNavBar", () => { expect(screen.queryByLabelText("Unread chat response")).toBeNull(); }); + /* + FNXC:Navigation 2026-07-05-00:00: + FN-7614: planning-awaiting-input moved from a top-of-board banner (broken Resume redirect) to a yellow + needs-input dot on the Planning More-sheet item and the More tab icon, mirroring the chat unread dot pattern. + The existing activePlanningSessionCount count badge must remain unaffected. + */ + describe("planningNeedsInput dot (FN-7614)", () => { + it("shows a needs-input dot on the More tab icon when planningNeedsInput is true and the sheet is closed", () => { + render(); + expect(screen.getByLabelText("Planning needs your input")).toBeInTheDocument(); + }); + + it("hides the More tab dot when planningNeedsInput is false", () => { + render(); + expect(screen.queryByLabelText("Planning needs your input")).toBeNull(); + }); + + it("hides the More tab dot while already on the planning view", () => { + render(); + expect(screen.queryByLabelText("Planning needs your input")).toBeNull(); + }); + + it("shows a needs-input dot on the Planning More-sheet item and keeps the count badge unaffected", () => { + render(); + fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); + + const planningItem = screen.getByTestId("mobile-more-item-planning"); + expect(planningItem.querySelector(".status-dot.status-dot--pending")).toBeTruthy(); + expect(planningItem).toHaveTextContent("2"); + }); + + it("hides the Planning More-sheet dot when planningNeedsInput is false while the count badge still renders", () => { + render(); + fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); + + const planningItem = screen.getByTestId("mobile-more-item-planning"); + expect(planningItem.querySelector(".status-dot.status-dot--pending")).toBeNull(); + expect(planningItem).toHaveTextContent("3"); + }); + }); + it("skills More-sheet item calls onChangeView with 'skills'", () => { const props = createDefaultProps(); render(); diff --git a/packages/dashboard/app/components/__tests__/SessionNotificationBanner.test.tsx b/packages/dashboard/app/components/__tests__/SessionNotificationBanner.test.tsx index 0fbaec0893..afcfa49864 100644 --- a/packages/dashboard/app/components/__tests__/SessionNotificationBanner.test.tsx +++ b/packages/dashboard/app/components/__tests__/SessionNotificationBanner.test.tsx @@ -2,6 +2,7 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; import { fireEvent, render, screen } from "@testing-library/react"; import type { AiSessionSummary } from "../../api"; import { SessionNotificationBanner, dismissedIds } from "../SessionNotificationBanner"; +import { isPlanningAwaitingInput, isSessionNeedingInputForBanner } from "../../utils/appLifecycle"; function buildSession(overrides: Partial): AiSessionSummary { return { @@ -283,6 +284,49 @@ describe("SessionNotificationBanner", () => { expect(onDismissSession).toHaveBeenCalledWith("error-dismiss"); }); + /* + FNXC:SessionBanner 2026-07-05-00:00: + Symptom Verification (FN-7614): the production banner feed (App.tsx `sessionsNeedingInput`) filters via + `isSessionNeedingInputForBanner(s) && !isPlanningAwaitingInput(s)` before it ever reaches this component. Given a + lone planning awaiting_input session, that filter yields an empty array, so the banner must render NO entry + (and no banner region at all) — reproducing the original broken-Resume-button symptom being fixed by the nav badge. + */ + it("renders no banner entry (and no banner region) for a lone planning awaiting_input session, using the production banner filter", () => { + const planningAwaitingInput = buildSession({ id: "planning-solo", type: "planning", status: "awaiting_input", title: "Solo planning session" }); + const filtered = [planningAwaitingInput].filter((s) => isSessionNeedingInputForBanner(s) && !isPlanningAwaitingInput(s)); + + const { container } = render( + , + ); + + expect(filtered).toEqual([]); + expect(screen.queryByText("Solo planning session")).not.toBeInTheDocument(); + expect(container.firstChild).toBeNull(); + }); + + it("keeps a mixed non-planning awaiting-input session visible while excluding planning-awaiting-input via the production filter", () => { + const planningAwaitingInput = buildSession({ id: "planning-solo2", type: "planning", status: "awaiting_input", title: "Excluded planning session" }); + const cliAwaitingInput = buildSession({ id: "cli-mixed", type: "cli-agent", status: "waiting_on_input", title: "Visible CLI session" }); + const filtered = [planningAwaitingInput, cliAwaitingInput].filter((s) => isSessionNeedingInputForBanner(s) && !isPlanningAwaitingInput(s)); + + render( + , + ); + + expect(screen.queryByText("Excluded planning session")).not.toBeInTheDocument(); + expect(screen.getByText("Visible CLI session")).toBeInTheDocument(); + }); + it("preserves dismissed error sessions when session status changes", () => { const { rerender } = render( & Pick): AiSessionSummary { + return { + id: overrides.id, + type: overrides.type ?? "planning", + status: overrides.status ?? "generating", + title: overrides.title ?? overrides.id, + projectId: overrides.projectId ?? null, + lockedByTab: overrides.lockedByTab ?? null, + updatedAt: overrides.updatedAt ?? "2026-04-08T00:00:00.000Z", + }; +} + +/* +FNXC:SessionBanner 2026-07-05-00:00: +Symptom Verification (FN-7614): planning `awaiting_input` sessions must be excluded from the banner feed +(`isSessionNeedingInputForBanner(s) && !isPlanningAwaitingInput(s)`), while planning `error` and non-planning +awaiting-input sessions must remain — this is the invariant the App.tsx `sessionsNeedingInput` filter relies on. +*/ +describe("isPlanningAwaitingInput", () => { + it("is true only for planning sessions awaiting input", () => { + expect(isPlanningAwaitingInput(makeSession({ id: "p1", type: "planning", status: "awaiting_input" }))).toBe(true); + }); + + it("is false for planning sessions in other statuses", () => { + expect(isPlanningAwaitingInput(makeSession({ id: "p2", type: "planning", status: "generating" }))).toBe(false); + expect(isPlanningAwaitingInput(makeSession({ id: "p3", type: "planning", status: "error" }))).toBe(false); + }); + + it("is false for non-planning sessions even when awaiting input", () => { + expect(isPlanningAwaitingInput(makeSession({ id: "c1", type: "cli-agent", status: "awaiting_input" }))).toBe(false); + }); +}); + +describe("sessionsNeedingInput banner filter (isSessionNeedingInputForBanner + !isPlanningAwaitingInput)", () => { + function bannerFilter(sessions: AiSessionSummary[]): AiSessionSummary[] { + return sessions.filter((s) => isSessionNeedingInputForBanner(s) && !isPlanningAwaitingInput(s)); + } + + it("excludes a lone planning awaiting_input session from the banner feed", () => { + const sessions = [makeSession({ id: "p1", type: "planning", status: "awaiting_input" })]; + expect(bannerFilter(sessions)).toEqual([]); + }); + + it("keeps planning error sessions in the banner feed", () => { + const errorSession = makeSession({ id: "p2", type: "planning", status: "error" }); + expect(bannerFilter([errorSession])).toEqual([errorSession]); + }); + + it("keeps non-planning awaiting-input sessions in the banner feed", () => { + const cliSession = makeSession({ id: "c1", type: "cli-agent", status: "awaiting_input" }); + expect(bannerFilter([cliSession])).toEqual([cliSession]); + }); + + it("excludes planning-awaiting-input while keeping a mixed non-planning awaiting-input session", () => { + const planningAwaiting = makeSession({ id: "p3", type: "planning", status: "awaiting_input" }); + const cliAwaiting = makeSession({ id: "c2", type: "cli-agent", status: "awaiting_input" }); + expect(bannerFilter([planningAwaiting, cliAwaiting])).toEqual([cliAwaiting]); + }); + + it("excludes generating planning sessions (never in the banner or badge input)", () => { + const generating = makeSession({ id: "p4", type: "planning", status: "generating" }); + expect(bannerFilter([generating])).toEqual([]); + }); +}); describe("resolveDesktopShellRedirectTarget", () => { const remoteProfile = { diff --git a/packages/dashboard/app/utils/appLifecycle.ts b/packages/dashboard/app/utils/appLifecycle.ts index c0887f5016..94f32e9855 100644 --- a/packages/dashboard/app/utils/appLifecycle.ts +++ b/packages/dashboard/app/utils/appLifecycle.ts @@ -176,6 +176,18 @@ export function isSessionNeedingInputForBanner(session: AiSessionSummary): boole ); } +/* +FNXC:SessionBanner 2026-07-05-00:00: +Planning-Mode "awaiting input" is no longer a SessionNotificationBanner entry: the banner's Resume button did not +reliably redirect into the Planning Mode interface, so awaiting-input planning sessions are now surfaced as a yellow +`status-dot--pending` badge on the Planning nav destination instead (LeftSidebarNav + MobileNavBar), whose click +target is the already-correct `planning` view navigation. Planning sessions in `error` status are unaffected and +still render in the banner via `isSessionNeedingInputForBanner` above. +*/ +export function isPlanningAwaitingInput(session: AiSessionSummary): boolean { + return session.type === "planning" && session.status === "awaiting_input"; +} + export function getCliActionDisabledReasonForBanner(session: AiSessionSummary, action: CliActionId): string | null { if ((action === "advance" || action === "relaunch") && !session.cliSessionId) { return "CLI session id is missing."; diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index 7a99694cf5..8edc2e5be9 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -4031,6 +4031,7 @@ "moreSheetTitle": "Navigate", "noScriptsAddOne": "No scripts — add one…", "planning": "Planning", + "planningNeedsInputAriaLabel": "Planning needs your input", "primaryNavAriaLabel": "Primary navigation", "projects": "Projects", "research": "Research", diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json index a23b2ead5a..0d267e2222 100644 --- a/packages/i18n/locales/es/app.json +++ b/packages/i18n/locales/es/app.json @@ -4021,6 +4021,7 @@ "moreSheetTitle": "Navegar", "noScriptsAddOne": "Sin scripts — agregar uno…", "planning": "Planificación", + "planningNeedsInputAriaLabel": "Planificación necesita tu entrada", "primaryNavAriaLabel": "Navegación principal", "projects": "Proyectos", "research": "Investigación", diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json index 69f21a1337..fb79ee6e70 100644 --- a/packages/i18n/locales/fr/app.json +++ b/packages/i18n/locales/fr/app.json @@ -4021,6 +4021,7 @@ "moreSheetTitle": "Naviguer", "noScriptsAddOne": "Aucun script — en ajouter un…", "planning": "Planification", + "planningNeedsInputAriaLabel": "La planification nécessite votre saisie", "primaryNavAriaLabel": "Navigation principale", "projects": "Projets", "research": "Recherche", diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json index a3ad02d39c..476df6d1e0 100644 --- a/packages/i18n/locales/ko/app.json +++ b/packages/i18n/locales/ko/app.json @@ -4021,6 +4021,7 @@ "moreSheetTitle": "탐색", "noScriptsAddOne": "스크립트 없음 — 추가하세요…", "planning": "계획", + "planningNeedsInputAriaLabel": "계획에 입력이 필요합니다", "primaryNavAriaLabel": "기본 탐색", "projects": "프로젝트", "research": "연구", diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json index 6c7dee07c5..2f20fc338f 100644 --- a/packages/i18n/locales/zh-CN/app.json +++ b/packages/i18n/locales/zh-CN/app.json @@ -4021,6 +4021,7 @@ "moreSheetTitle": "导航", "noScriptsAddOne": "无脚本 — 添加一个…", "planning": "规划", + "planningNeedsInputAriaLabel": "规划需要您的输入", "primaryNavAriaLabel": "主导航", "projects": "项目", "research": "研究", diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json index 133c43ac49..3f1ed69a98 100644 --- a/packages/i18n/locales/zh-TW/app.json +++ b/packages/i18n/locales/zh-TW/app.json @@ -4021,6 +4021,7 @@ "moreSheetTitle": "導覽", "noScriptsAddOne": "無腳本 — 新增一個…", "planning": "規劃", + "planningNeedsInputAriaLabel": "規劃需要您的輸入", "primaryNavAriaLabel": "主導覽", "projects": "專案", "research": "研究", diff --git a/packages/i18n/src/resources.d.ts b/packages/i18n/src/resources.d.ts index a85f6426be..d2b638fc01 100644 --- a/packages/i18n/src/resources.d.ts +++ b/packages/i18n/src/resources.d.ts @@ -4032,6 +4032,7 @@ export default interface Resources { "moreSheetTitle": "Navigate", "noScriptsAddOne": "No scripts — add one…", "planning": "Planning", + "planningNeedsInputAriaLabel": "Planning needs your input", "primaryNavAriaLabel": "Primary navigation", "projects": "Projects", "research": "Research",