diff --git a/packages/dashboard/app/components/AgentsView.css b/packages/dashboard/app/components/AgentsView.css
index eb0cea5a86..129ecc2b6a 100644
--- a/packages/dashboard/app/components/AgentsView.css
+++ b/packages/dashboard/app/components/AgentsView.css
@@ -278,11 +278,12 @@ The base grid keeps a token-sized handle column as the no-JS fallback, while the
background: var(--surface);
}
+/* FNXC:AgentsView 2026-06-22-01:00: ViewHeader supplies the top padding, so the scrollable body drops its top inset to avoid doubling the gap under the header (keeps horizontal + bottom padding). */
.agents-view-content {
flex: 1;
min-height: 0;
overflow-y: auto;
- padding: calc(var(--space-lg) + var(--space-xs));
+ padding: 0 calc(var(--space-lg) + var(--space-xs)) calc(var(--space-lg) + var(--space-xs));
}
diff --git a/packages/dashboard/app/components/DevServerView.css b/packages/dashboard/app/components/DevServerView.css
index 2bdca7348a..c6bfe4d6b7 100644
--- a/packages/dashboard/app/components/DevServerView.css
+++ b/packages/dashboard/app/components/DevServerView.css
@@ -1,10 +1,14 @@
/* === DevServerView === */
+/*
+FNXC:DevServer 2026-06-22-01:00:
+Header migrated to the shared ViewHeader (.view-header), which supplies the --space-lg top/side and --space-md bottom padding. The view no longer adds its own top padding (was causing a doubled gap under the header); only the side and bottom padding remain.
+*/
.dev-server-view {
display: flex;
flex-direction: column;
gap: var(--space-md);
- padding: var(--space-lg);
+ padding: 0 var(--space-lg) var(--space-lg);
min-height: 0;
height: 100%;
overflow-y: auto;
@@ -13,29 +17,15 @@
}
/*
-FNXC:DevServer 2026-06-22-00:00:
-The Dev Server view header must read like other embedded views (Command Center cc-header): a plain title row with actions, not a bordered card. Drop the card border/background/padding so the heading sits flush with the view padding; the title font already matches the shared 1.125rem embedded-title size.
+FNXC:DevServer 2026-06-22-01:00:
+.dev-server-header-title now wraps just the status badge inside ViewHeader's actions slot; the mobile flex-wrap rule keeps it from overflowing on narrow widths.
*/
-.dev-server-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: var(--space-md);
-}
-
.dev-server-header-title {
display: flex;
align-items: center;
gap: var(--space-sm);
}
-.dev-server-header-title h2 {
- margin: 0;
- font-size: 1.125rem;
- font-weight: 600;
- color: var(--text);
-}
-
.dev-server-header-actions {
display: flex;
align-items: center;
@@ -542,7 +532,7 @@ The Dev Server view header must read like other embedded views (Command Center c
grid-template-rows: auto auto 1fr;
}
- .dev-server-header {
+ .dev-server-view > .view-header {
grid-column: 1 / -1;
}
@@ -572,12 +562,7 @@ The Dev Server view header must read like other embedded views (Command Center c
.dev-server-view {
display: flex;
flex-direction: column;
- padding: var(--space-md);
- }
-
- .dev-server-header {
- flex-direction: column;
- align-items: flex-start;
+ padding: 0 var(--space-md) var(--space-md);
}
.dev-server-header-title {
diff --git a/packages/dashboard/app/components/DevServerView.tsx b/packages/dashboard/app/components/DevServerView.tsx
index 8c88886db3..ecca3d4d06 100644
--- a/packages/dashboard/app/components/DevServerView.tsx
+++ b/packages/dashboard/app/components/DevServerView.tsx
@@ -11,6 +11,7 @@ import type { ToastType } from "../hooks/useToast";
import { DevServerLogViewer } from "./DevServerLogViewer";
import { PreviewIframe } from "./PreviewIframe";
import { recordResumeEvent } from "../utils/resumeInstrumentation";
+import { ViewHeader } from "./ViewHeader";
interface DevServerViewProps {
addToast: (msg: string, type?: ToastType) => void;
@@ -365,50 +366,58 @@ export function DevServerView({ addToast, projectId }: DevServerViewProps) {
return (
-
-
-
-
{t("devserver.title", "Dev Server")}
-
- {statusBadge.label}
-
-
-
-
-
-
-
-
+ {/*
+ FNXC:DevServer 2026-06-22-01:00:
+ Migrated to the shared ViewHeader for cross-view consistency. The status badge sits next to the title inside the actions slot (wrapped in .dev-server-header-title so the existing mobile flex-wrap rule still applies), and the Start/Stop/Restart controls follow in .dev-server-header-actions. ViewHeader supplies the standard view padding; the view body must not repeat the top padding.
+ */}
+
+
+
+ {statusBadge.label}
+
+
+
+
+
+
+
+ >
+ )}
+ />
diff --git a/packages/dashboard/app/components/DocumentsView.css b/packages/dashboard/app/components/DocumentsView.css
index 2a42034ff4..36979534a5 100644
--- a/packages/dashboard/app/components/DocumentsView.css
+++ b/packages/dashboard/app/components/DocumentsView.css
@@ -149,11 +149,12 @@ The header row now comes from the shared .view-header (which supplies the --spac
box-shadow: var(--focus-ring-strong);
}
+/* FNXC:DocumentsView 2026-06-22-01:00: ViewHeader supplies the top padding, so the scrollable content body drops its top inset to avoid doubling the gap under the header (keeps horizontal + bottom padding). */
.documents-view-content {
flex: 1;
min-height: 0;
overflow: auto;
- padding: var(--space-lg);
+ padding: 0 var(--space-lg) var(--space-lg);
}
.documents-view-loading,
diff --git a/packages/dashboard/app/components/InsightsView.css b/packages/dashboard/app/components/InsightsView.css
index 5b2d4d8014..cb52777d28 100644
--- a/packages/dashboard/app/components/InsightsView.css
+++ b/packages/dashboard/app/components/InsightsView.css
@@ -8,48 +8,15 @@
}
/*
-FNXC:Insights 2026-06-22-00:00:
-The header title and action buttons must never overlap: allow the row to wrap so the actions drop to a new line when there is not enough horizontal room. The gap keeps spacing between the wrapped rows.
+FNXC:Insights 2026-06-22-01:00:
+Header migrated to the shared ViewHeader component (.view-header). The old .insights-view-header / .insights-view-title / .insights-view-actions rules were removed; the count badge and action toggles still render inside ViewHeader's actions slot, so their styling rules are retained below.
*/
-.insights-view-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-wrap: wrap;
- gap: var(--space-sm);
- padding: var(--space-lg);
- border-bottom: 1px solid var(--border);
- background: var(--surface);
- flex-shrink: 0;
-}
-
-.insights-view-title {
- display: flex;
- align-items: center;
- gap: var(--space-sm);
-}
-
-.insights-view-title h2 {
- margin: 0;
- font-size: 1.125rem;
- font-weight: 600;
- display: flex;
- align-items: center;
- gap: var(--space-sm);
-}
-
.insights-view-count {
font-size: 0.8125rem;
color: var(--text-muted);
font-weight: normal;
}
-.insights-view-actions {
- display: flex;
- align-items: center;
- gap: var(--space-sm);
-}
-
/* Model configuration row — collapsible, below the action bar */
.insights-model-config {
display: flex;
@@ -493,31 +460,6 @@ The header title and action buttons must never overlap: allow the row to wrap so
/* Mobile responsive: stack panes vertically; sidebar becomes a horizontal scroller */
@media (max-width: 768px) {
- .insights-view-header {
- flex-wrap: nowrap;
- gap: var(--space-sm);
- padding: var(--space-md);
- }
-
- .insights-view-title {
- min-width: 0;
- flex: 1 1 auto;
- }
-
- .insights-view-title h2 {
- font-size: 1rem;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- .insights-view-actions {
- flex-shrink: 0;
- justify-content: flex-end;
- flex-wrap: nowrap;
- gap: var(--space-xs);
- }
-
.insights-body {
flex-direction: column;
}
@@ -591,6 +533,7 @@ The header title and action buttons must never overlap: allow the row to wrap so
height: calc(var(--space-2xl) + var(--space-sm));
}
+ .view-header__actions .insights-view-actions-btn,
.insights-view-actions .btn {
min-height: calc(var(--space-2xl) + var(--space-xs));
}
@@ -633,19 +576,6 @@ Insights tablets at 769px–1024px were inheriting the desktop fixed category si
overflow: hidden;
}
- .insights-view-header {
- min-inline-size: 0;
- }
-
- .insights-view-title {
- flex: 1 1 auto;
- min-inline-size: 0;
- }
-
- .insights-view-actions {
- flex-shrink: 0;
- }
-
.insights-body {
flex-direction: column;
inline-size: 100%;
diff --git a/packages/dashboard/app/components/InsightsView.tsx b/packages/dashboard/app/components/InsightsView.tsx
index b50b27a904..cd5fd86bdf 100644
--- a/packages/dashboard/app/components/InsightsView.tsx
+++ b/packages/dashboard/app/components/InsightsView.tsx
@@ -27,6 +27,7 @@ import {
Activity,
} from "lucide-react";
import { CustomModelDropdown } from "./CustomModelDropdown";
+import { ViewHeader } from "./ViewHeader";
import { fetchModels, updateGlobalSettings, type ModelInfo } from "../api";
import { useInsights, type InsightSection } from "../hooks/useInsights";
import { BACKLOG_HEALTH_TITLE_PREFIXES, isBacklogHealthInsight } from "./backlog-health-filter";
@@ -480,16 +481,16 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask, model
return (
-
-
-
-
- {t("insights.title", "Insights")}
-
- {totalCount} {t("common.total", "total")}
-
-
-
+ {/*
+ FNXC:Insights 2026-06-22-01:00:
+ Migrated to the shared ViewHeader for consistency with other main-content views. The insight count and action buttons live in the actions slot; ViewHeader already provides the --space-lg side/top padding and --space-md bottom gap, so the view body must not repeat the top padding.
+ */}
+
+ {totalCount} {t("common.total", "total")}
{backlogHealthCount > 0 && (
)}
-
-
+ >
+ )}
+ />
{showModelConfig && (
diff --git a/packages/dashboard/app/components/MailboxModal.css b/packages/dashboard/app/components/MailboxModal.css
index 02b220a7e1..63406b973f 100644
--- a/packages/dashboard/app/components/MailboxModal.css
+++ b/packages/dashboard/app/components/MailboxModal.css
@@ -652,12 +652,13 @@
padding-top: var(--space-sm);
}
+/* FNXC:MailboxView 2026-06-22-01:00: ViewHeader supplies the top padding, so the standalone-view scroll body drops its top inset to avoid doubling the gap under the header (keeps horizontal + bottom padding). */
.mailbox-view .mailbox-content {
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
- padding: var(--space-xl);
+ padding: 0 var(--space-xl) var(--space-xl);
max-height: none;
}
diff --git a/packages/dashboard/app/components/MissionManager.css b/packages/dashboard/app/components/MissionManager.css
index 586a8b111b..80d256f266 100644
--- a/packages/dashboard/app/components/MissionManager.css
+++ b/packages/dashboard/app/components/MissionManager.css
@@ -139,13 +139,14 @@ Title metric matches the shared ViewHeader (1.125rem) so the Missions header rea
}
/* ── Body ── */
+/* FNXC:MissionManager 2026-06-22-01:00: The aligned .mission-manager__header supplies the top padding, so the scroll body drops its top inset to avoid doubling the gap under the header (keeps horizontal + bottom padding). */
.mission-manager__body {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
- padding: var(--space-lg);
+ padding: 0 var(--space-lg) var(--space-lg);
-webkit-overflow-scrolling: touch;
}
diff --git a/packages/dashboard/app/components/PullRequestView.css b/packages/dashboard/app/components/PullRequestView.css
index ef23d55c19..6fd02f252c 100644
--- a/packages/dashboard/app/components/PullRequestView.css
+++ b/packages/dashboard/app/components/PullRequestView.css
@@ -1,3 +1,7 @@
+/*
+FNXC:PullRequests 2026-06-22-01:00:
+The view now renders the shared ViewHeader at the top, which supplies the --space-lg top/side padding. The view drops its own top padding so the gap under the header is just ViewHeader's --space-md bottom; side and bottom padding remain.
+*/
.pr-view {
display: flex;
flex-direction: column;
@@ -6,7 +10,7 @@
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
- padding: var(--space-lg);
+ padding: 0 var(--space-lg) var(--space-lg);
color: var(--text);
}
diff --git a/packages/dashboard/app/components/PullRequestView.tsx b/packages/dashboard/app/components/PullRequestView.tsx
index 4306c04434..6fe624b1d5 100644
--- a/packages/dashboard/app/components/PullRequestView.tsx
+++ b/packages/dashboard/app/components/PullRequestView.tsx
@@ -13,6 +13,7 @@ import {
MessageSquare,
} from "lucide-react";
import { api } from "../api";
+import { ViewHeader } from "./ViewHeader";
import "./PullRequestView.css";
// Mirrors the route's serialized entity (register-pull-requests-routes.ts).
@@ -175,10 +176,17 @@ export function PullRequestView(props: PullRequestViewProps) {
const { state, summary } = detail;
+ /*
+ FNXC:PullRequests 2026-06-22-01:00:
+ Added the shared ViewHeader (GitPullRequest icon, matching the left-sidebar nav) at the top of every populated PR state so the view reads consistently with other main-content views. The PR-specific identity row (repo/number/branch/state) stays below it. ViewHeader supplies the standard --space-lg top/side padding; the view body must not repeat the top padding.
+ */
+ const viewHeader = ;
+
// ── creating ───────────────────────────────────────────────────────────────
if (state === "creating") {
return (
+ {viewHeader}
{t("pr.view.creating", "Creating PR…")}
@@ -191,6 +199,7 @@ export function PullRequestView(props: PullRequestViewProps) {
if (state === "failed") {
return (
+ {viewHeader}
@@ -216,6 +225,7 @@ export function PullRequestView(props: PullRequestViewProps) {
if (detail.unverified) {
return (
+ {viewHeader}
{t("pr.view.verifyingGithub", "Verifying with GitHub…")}
@@ -240,6 +250,7 @@ export function PullRequestView(props: PullRequestViewProps) {
return (
+ {viewHeader}
{/* responding banner */}
diff --git a/packages/dashboard/app/components/SettingsModal.css b/packages/dashboard/app/components/SettingsModal.css
index a8af66dbca..a5d8bd86cd 100644
--- a/packages/dashboard/app/components/SettingsModal.css
+++ b/packages/dashboard/app/components/SettingsModal.css
@@ -145,6 +145,9 @@ The embedded title reads like other embedded-view titles (Planning modal-header-
}
.settings-modal--embedded .modal-header--embedded .settings-modal-heading h3 {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
font-size: 1.125rem;
letter-spacing: normal;
}
diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx
index bbdc5bb45a..c2aac913f5 100644
--- a/packages/dashboard/app/components/SettingsModal.tsx
+++ b/packages/dashboard/app/components/SettingsModal.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useRef, type CSSProperties, type MouseEvent } from "react";
-import { Globe, Folder, RefreshCw, Star, HelpCircle } from "lucide-react";
+import { Globe, Folder, RefreshCw, Star, HelpCircle, Settings as SettingsIcon } from "lucide-react";
import {
getErrorMessage,
normalizeMergeIntegrationWorktreeMode,
@@ -2920,8 +2920,12 @@ export function SettingsModal({
style={isEmbedded ? undefined : keyboardStyle}
>
+ {/* FNXC:Settings 2026-06-22-01:00: Embedded title gains a Settings icon (size 20, matching the sidebar nav and shared ViewHeader) so the embedded settings panel reads consistently with other main-content destinations; title is already 1.125rem. */}