FN-7020: compact AI session input banner
Make the dashboard AI session needs-input banner less prominent while preserving visibility rules. - Reduce the session notification banner spacing, text scale, max heights, and mobile density. - Cover banner visibility on missions, planning, hidden, empty, and board-session states. - Add a patch changeset for the published CLI package. Files changed: .changeset/fn-7020-session-banner-compact.md | 7 + .../app/components/SessionNotificationBanner.css | 47 +++-- .../dashboard/__tests__/DashboardBanners.test.tsx | 227 +++++++++++++++++++++ 3 files changed, 263 insertions(+), 18 deletions(-) Fusion-Task-Id: FN-7020 Fusion-Task-Lineage: bd48da60-29ef-4871-ae27-8a1a48f744ea
This commit is contained in:
7
.changeset/fn-7020-session-banner-compact.md
Normal file
7
.changeset/fn-7020-session-banner-compact.md
Normal file
@@ -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.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> = {}): 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> = {}): 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> = {}): 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(<DashboardBanners {...buildProps({ taskView: "missions" })} />);
|
||||
|
||||
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(<DashboardBanners {...buildProps({ modalManager: buildModalManager({ isPlanningOpen: true }) })} />);
|
||||
|
||||
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(
|
||||
<DashboardBanners
|
||||
{...buildProps({
|
||||
taskView: "board",
|
||||
sessionsNeedingInput: [
|
||||
buildSession({ id: "awaiting", title: "Awaiting input", status: "awaiting_input" }),
|
||||
buildSession({ id: "error", title: "Needs attention", status: "needs_attention", type: "cli-agent", cliVariant: "userExited" }),
|
||||
],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(<DashboardBanners {...buildProps({ sessionBannersHidden: true })} />);
|
||||
|
||||
expect(querySessionBanner()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render when there are zero sessions needing input", () => {
|
||||
render(<DashboardBanners {...buildProps({ sessionsNeedingInput: [] })} />);
|
||||
|
||||
expect(querySessionBanner()).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user