diff --git a/.changeset/fn-7020-session-banner-compact.md b/.changeset/fn-7020-session-banner-compact.md new file mode 100644 index 0000000000..addcb2f53f --- /dev/null +++ b/.changeset/fn-7020-session-banner-compact.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Make the AI session needs-input banner compact and hide it on Missions or Planning. +category: fix +dev: Shrinks SessionNotificationBanner CSS and tests the DashboardBanners visibility guard. diff --git a/packages/dashboard/app/components/SessionNotificationBanner.css b/packages/dashboard/app/components/SessionNotificationBanner.css index deb484708e..48c9befc2c 100644 --- a/packages/dashboard/app/components/SessionNotificationBanner.css +++ b/packages/dashboard/app/components/SessionNotificationBanner.css @@ -1,17 +1,21 @@ /* === Session Notification Banner === */ +/* +FNXC:SessionBanner 2026-06-25-00:00: +FN-7020 requires the needs-input banner to stay visible when relevant without competing with primary dashboard content. Keep the desktop and mobile layouts compact by reducing padding, gaps, list heights, and text scale while preserving token-based touch targets. +*/ .session-notification-banner { - --session-notification-banner-max-height: min(60vh, calc(var(--space-2xl) * 13)); - --session-notification-list-max-height: min(48vh, calc(var(--space-2xl) * 9)); - --session-notification-touch-size: calc(var(--space-xl) + var(--space-md)); + --session-notification-banner-max-height: min(52vh, calc(var(--space-2xl) * 10)); + --session-notification-list-max-height: min(38vh, calc(var(--space-2xl) * 7)); + --session-notification-touch-size: calc(var(--space-xl) + var(--space-xs)); position: sticky; top: 0; z-index: 30; display: flex; flex-direction: column; - gap: var(--space-sm); - padding: var(--space-sm) var(--space-lg); + gap: var(--space-xs); + padding: var(--space-xs) var(--space-md); border-bottom: var(--btn-border-width) solid var(--border); border-left: var(--space-xs) solid var(--triage); background: var(--surface); @@ -44,6 +48,7 @@ display: inline-flex; align-items: center; gap: var(--space-xs); + font-size: 0.875rem; font-weight: 600; color: var(--text); } @@ -56,12 +61,12 @@ .session-notification-banner__dismiss-all { display: inline-flex; align-items: center; - gap: var(--space-sm); + gap: var(--space-xs); min-height: var(--session-notification-touch-size); border: var(--btn-border-width) solid transparent; background: transparent; color: var(--text-muted); - font-size: 0.75rem; + font-size: 0.6875rem; font-weight: 600; border-radius: var(--radius-sm); padding: var(--space-xs) var(--space-sm); @@ -85,7 +90,7 @@ .session-notification-banner__list { display: flex; flex-direction: column; - gap: var(--space-sm); + gap: var(--space-xs); overflow-y: auto; max-height: var(--session-notification-list-max-height); } @@ -94,8 +99,8 @@ display: flex; align-items: center; justify-content: space-between; - gap: var(--space-sm); - padding: var(--space-sm) var(--space-md); + gap: var(--space-xs); + padding: var(--space-xs) var(--space-sm); border: var(--btn-border-width) solid var(--border); border-radius: var(--radius-sm); background: color-mix(in srgb, var(--triage) 8%, var(--surface)); @@ -109,7 +114,7 @@ .session-notification-banner__item-main { display: flex; align-items: center; - gap: var(--space-sm); + gap: var(--space-xs); min-width: 0; flex: 1; } @@ -129,7 +134,7 @@ .session-notification-banner__title { margin: 0; - font-size: 0.8125rem; + font-size: 0.75rem; font-weight: 600; color: var(--text); white-space: nowrap; @@ -139,14 +144,14 @@ .session-notification-banner__meta { margin: var(--space-xs) 0 0; - font-size: 0.75rem; + font-size: 0.6875rem; color: var(--text-muted); } .session-notification-banner__actions { display: inline-flex; align-items: center; - gap: var(--space-sm); + gap: var(--space-xs); } .session-notification-banner__resume { @@ -155,9 +160,9 @@ background: transparent; color: var(--triage); border-radius: var(--radius-sm); - font-size: 0.75rem; + font-size: 0.6875rem; font-weight: 600; - padding: var(--space-xs) var(--space-md); + padding: var(--space-xs) var(--space-sm); cursor: pointer; transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast); } @@ -214,8 +219,12 @@ @media (max-width: 768px) { .session-notification-banner { - padding: var(--space-sm) var(--space-md); - gap: var(--space-sm); + --session-notification-banner-max-height: min(46vh, calc(var(--space-2xl) * 9)); + --session-notification-list-max-height: min(32vh, calc(var(--space-2xl) * 6)); + --session-notification-touch-size: calc(var(--space-xl) + var(--space-md)); + + padding: var(--space-xs) var(--space-sm); + gap: var(--space-xs); } .session-notification-banner__header { @@ -226,6 +235,8 @@ .session-notification-banner__item { flex-direction: column; align-items: flex-start; + gap: var(--space-xs); + padding: var(--space-xs); } .session-notification-banner__item-main { diff --git a/packages/dashboard/app/components/dashboard/__tests__/DashboardBanners.test.tsx b/packages/dashboard/app/components/dashboard/__tests__/DashboardBanners.test.tsx new file mode 100644 index 0000000000..f3ad6c7cde --- /dev/null +++ b/packages/dashboard/app/components/dashboard/__tests__/DashboardBanners.test.tsx @@ -0,0 +1,227 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import type { AiSessionSummary } from "../../../api"; +import type { ModalManager } from "../../../hooks/useModalManager"; +import type { DashboardBannersProps } from "../types"; + +vi.mock("../../TestModeBanner", () => ({ TestModeBanner: () => null })); +vi.mock("../../EngineUnavailableBanner", () => ({ EngineUnavailableBanner: () => null })); +vi.mock("../../EngineStatusBanner", () => ({ EngineStatusBanner: () => null })); +vi.mock("../../OAuthReloginBanner", () => ({ OAuthReloginBanner: () => null })); +vi.mock("../../CliBinaryInstallBanner", () => ({ CliBinaryInstallBanner: () => null })); +vi.mock("../../OnboardingResumeCard", () => ({ OnboardingResumeCard: () => null })); +vi.mock("../../PostOnboardingRecommendations", () => ({ PostOnboardingRecommendations: () => null })); +vi.mock("../../UpdateAvailableBanner", () => ({ UpdateAvailableBanner: () => null })); +vi.mock("../../MergeAdvanceNotice", () => ({ default: () => null })); +vi.mock("../../TaskIdIntegrityBanner", () => ({ TaskIdIntegrityBanner: () => null })); +vi.mock("../../DbCorruptionBanner", () => ({ DbCorruptionBanner: () => null })); +vi.mock("../../SetupWarningBanner", () => ({ SetupWarningBanner: () => null })); +vi.mock("../../ApprovalNotificationBanner", () => ({ ApprovalNotificationBanner: () => null })); +vi.mock("../../GitHubStarPrompt", () => ({ GitHubStarPrompt: () => null })); + +import { DashboardBanners } from "../DashboardBanners"; + +function buildSession(overrides: Partial = {}): AiSessionSummary { + return { + id: overrides.id ?? "session-1", + type: overrides.type ?? "planning", + status: overrides.status ?? "awaiting_input", + title: overrides.title ?? "Draft implementation plan", + projectId: overrides.projectId ?? "proj-1", + lockedByTab: overrides.lockedByTab ?? null, + updatedAt: overrides.updatedAt ?? "2026-06-25T00:00:00.000Z", + ...overrides, + }; +} + +function buildModalManager(overrides: Partial = {}): ModalManager { + const noop = vi.fn(); + return { + newTaskModalOpen: false, + newTaskInitialDescription: null, + isPlanningOpen: false, + planningInitialPlan: null, + planningResumeSessionId: undefined, + planningWorkflowId: undefined, + isSubtaskOpen: false, + subtaskInitialDescription: null, + subtaskResumeSessionId: undefined, + subtaskWorkflowId: undefined, + detailTask: null, + detailTaskInitialTab: "chat", + detailTaskOrigin: null, + groupModalGroupId: null, + settingsOpen: false, + settingsInitialSection: undefined, + schedulesOpen: false, + githubImportOpen: false, + usageOpen: false, + usageAnchorRect: null, + terminalOpen: false, + terminalInitialCommand: undefined, + terminalInitialCommandGeneration: 0, + filesOpen: false, + fileBrowserWorkspace: "project", + fileBrowserInitialFile: null, + activityLogOpen: false, + gitManagerOpen: false, + workflowEditorOpen: false, + workflowEditorInitialPanel: undefined, + workflowEditorInitialAction: undefined, + workflowEditorInitialWorkflowId: undefined, + agentsOpen: false, + scriptsOpen: false, + setupWizardOpen: false, + modelOnboardingOpen: false, + anyModalOpen: false, + openNewTask: noop, + openNewTaskWithDescription: noop, + closeNewTask: noop, + openPlanning: noop, + openPlanningWithInitialPlan: noop, + resumePlanning: noop, + openPlanningWithSession: noop, + closePlanning: noop, + openSubtaskBreakdown: noop, + openSubtaskWithSession: noop, + closeSubtask: noop, + openDetailTask: noop, + openDetailWithChangesTab: noop, + updateDetailTask: noop, + closeDetailTask: noop, + openGroupModal: noop, + closeGroupModal: noop, + openSettings: noop, + setSettingsSection: noop, + closeSettings: noop, + openSchedules: noop, + closeSchedules: noop, + openGitHubImport: noop, + closeGitHubImport: noop, + openUsage: noop, + closeUsage: noop, + toggleTerminal: noop, + closeTerminal: noop, + openFiles: noop, + closeFiles: noop, + setFileWorkspace: noop, + openActivityLog: noop, + closeActivityLog: noop, + openGitManager: noop, + closeGitManager: noop, + openWorkflowEditor: noop, + closeWorkflowEditor: noop, + openAgents: noop, + closeAgents: noop, + openScripts: noop, + closeScripts: noop, + runScript: vi.fn().mockResolvedValue(undefined), + openSetupWizard: noop, + closeSetupWizard: noop, + openModelOnboarding: noop, + closeModelOnboarding: noop, + onPlanningTaskCreated: noop, + onPlanningTasksCreated: noop, + onSubtaskTasksCreated: noop, + ...overrides, + }; +} + +function buildProps(overrides: Partial = {}): DashboardBannersProps { + return { + viewMode: "project", + currentProject: { id: "proj-1", name: "Project", path: "/tmp/project" } as DashboardBannersProps["currentProject"], + isTestMode: false, + dashboardHealth: null, + setDashboardHealth: vi.fn(), + taskView: "board", + modalManager: buildModalManager(), + sessionBannersHidden: false, + sessionsNeedingInput: [buildSession()], + handleOpenBackgroundSession: vi.fn(), + handleDismissNeedingInputSession: vi.fn(), + handleDismissAllNeedingInputSessions: vi.fn(), + handleCliAction: vi.fn().mockResolvedValue(undefined), + getCliActionDisabledReasonForBanner: vi.fn(() => null), + openSettingsWithNav: vi.fn(), + showOnboardingResumeCard: false, + showPostOnboardingRecommendations: false, + updateAvailable: false, + latestVersion: null, + currentVersion: null, + updateBannerDismissed: false, + dismissUpdateBanner: vi.fn(), + refreshDbCorruptionHealth: vi.fn().mockResolvedValue(undefined), + dbCorruptionRefreshing: false, + dbCorruptionRefreshError: null, + setupReadinessLoading: false, + hasWarnings: false, + setupWarningDismissed: false, + handleDismissSetupWarning: vi.fn(), + hasAiProvider: true, + hasGithub: true, + approvalBannerCandidate: null, + dismissApproval: vi.fn(), + mailboxPendingApprovalCount: 0, + handleTaskViewChange: vi.fn(), + showGitHubStarPrompt: false, + gitHubStarPromptShown: false, + markGitHubStarPromptShown: vi.fn(), + setShowGitHubStarPrompt: vi.fn(), + ...overrides, + }; +} + +function querySessionBanner(): HTMLElement | null { + return screen.queryByRole("region", { name: /AI sessions needing input or failed/i }); +} + +describe("DashboardBanners session notification visibility", () => { + /* + FNXC:SessionBanner 2026-06-25-00:00: + FN-7020 visibility surface enumeration: DashboardBanners.tsx is the only app-shell mount point for SessionNotificationBanner (grep-confirmed; other references are tests). The guard must remain viewMode === "project", currentProject present, taskView !== "missions", !modalManager.isPlanningOpen, and !sessionBannersHidden so Missions interviews and the Planning modal own their active AI-session UX while board/list surfaces still show needs-input sessions. + */ + it("does not render the session notification banner on the missions view", () => { + render(); + + expect(querySessionBanner()).not.toBeInTheDocument(); + expect(screen.queryByText("Draft implementation plan")).not.toBeInTheDocument(); + }); + + it("does not render the session notification banner while planning is open", () => { + render(); + + expect(querySessionBanner()).not.toBeInTheDocument(); + expect(screen.queryByText("Draft implementation plan")).not.toBeInTheDocument(); + }); + + it("renders the session notification banner on board views when sessions need input", () => { + render( + , + ); + + expect(querySessionBanner()).toBeInTheDocument(); + expect(screen.getByText("Awaiting input")).toBeInTheDocument(); + expect(screen.getByText("Needs attention")).toBeInTheDocument(); + }); + + it("does not render when the appearance setting hides session banners", () => { + render(); + + expect(querySessionBanner()).not.toBeInTheDocument(); + }); + + it("does not render when there are zero sessions needing input", () => { + render(); + + expect(querySessionBanner()).not.toBeInTheDocument(); + }); +});