diff --git a/docs/console-page-headers.md b/docs/console-page-headers.md
new file mode 100644
index 0000000..8e6e74c
--- /dev/null
+++ b/docs/console-page-headers.md
@@ -0,0 +1,10 @@
+# Console tab headers
+
+Runs, Repositories, Issue chat, Usage & costs, Settings, User guide, Getting started
+and Account/Team use ConsolePageHeader. The component owns eyebrow/title/description
+typography and spacing, with an optional action area. Headers have no panel
+background. Repository cards keep their individual backgrounds without a second
+outer panel. Mobile actions wrap below the same header typography.
+
+Normal image preparation preserves supplied icons and social-preview.jpg. Set
+FORGELOOP_REGENERATE_BRANDING=true explicitly to regenerate them from source art.
diff --git a/docs/desktop-preview-release-notes.md b/docs/desktop-preview-release-notes.md
index fd674f9..4ec2106 100644
--- a/docs/desktop-preview-release-notes.md
+++ b/docs/desktop-preview-release-notes.md
@@ -5,6 +5,10 @@ RPM for Fedora/RHEL/openSUSE, portable `.tar.gz` archives, and native Arch
## What changed in this preview
+- **Updated branding:** The runner window, navigation mark and native Windows,
+ macOS and Linux installer/launcher icons now derive from the approved green/cyan
+ ForgeLoop logo mark used by the website. Existing identity and keys are unchanged.
+
- **Docker startup:** Start runner can start a recognized, installed local Docker
engine on Windows, macOS, or Linux, then wait for Linux-container readiness.
Startup is bounded and cancellable; no model work starts before readiness.
diff --git a/frontend/e2e/mobile-runs.spec.ts b/frontend/e2e/mobile-runs.spec.ts
index 8bb59da..f453a35 100644
--- a/frontend/e2e/mobile-runs.spec.ts
+++ b/frontend/e2e/mobile-runs.spec.ts
@@ -83,7 +83,9 @@ for (const width of [320, 390, 430, 768, 1440]) {
await expect(menuToggle).toHaveAttribute('aria-expanded', 'false');
await expect(page.locator('.console-sidebar')).toHaveCSS('visibility', 'hidden');
}
- await expect(page.getByRole('heading', { name: 'Connected repositories', exact: true })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Repositories', exact: true })).toBeVisible();
+ await expect(page.locator('.console-page-header .eyebrow')).toHaveText('Repository access');
+ expect(await page.locator('.connected-repositories').evaluate(element => getComputedStyle(element).backgroundImage)).toBe('none');
const repositoryCard = page.locator('.repository-card');
await expect(repositoryCard).toHaveCount(1);
await expect(repositoryCard.getByRole('heading', { name: 'Repository issue scan', exact: true })).toBeVisible();
diff --git a/frontend/public/favicon.png b/frontend/public/favicon.png
new file mode 100644
index 0000000..d90fbb0
Binary files /dev/null and b/frontend/public/favicon.png differ
diff --git a/frontend/public/icons/icon-180.png b/frontend/public/icons/icon-180.png
index 5219cdc..d49a216 100644
Binary files a/frontend/public/icons/icon-180.png and b/frontend/public/icons/icon-180.png differ
diff --git a/frontend/public/icons/icon-192.png b/frontend/public/icons/icon-192.png
index 226a588..dd87ab1 100644
Binary files a/frontend/public/icons/icon-192.png and b/frontend/public/icons/icon-192.png differ
diff --git a/frontend/public/icons/icon-32.png b/frontend/public/icons/icon-32.png
index 5d50984..2e3e91e 100644
Binary files a/frontend/public/icons/icon-32.png and b/frontend/public/icons/icon-32.png differ
diff --git a/frontend/public/images/social-preview.jpg b/frontend/public/images/social-preview.jpg
index f3b4f0b..ae1a300 100644
Binary files a/frontend/public/images/social-preview.jpg and b/frontend/public/images/social-preview.jpg differ
diff --git a/frontend/scripts/prepare-images.mjs b/frontend/scripts/prepare-images.mjs
index 3006220..0ea5902 100644
--- a/frontend/scripts/prepare-images.mjs
+++ b/frontend/scripts/prepare-images.mjs
@@ -1,5 +1,12 @@
import sharp from 'sharp';
-import { mkdir, copyFile, readdir } from 'node:fs/promises';
+import { mkdir, copyFile, readdir, access } from 'node:fs/promises';
+// Supplied branding is authoritative; regeneration must be an explicit choice.
+async function generateBranding(path, generate) {
+ if (process.env.FORGELOOP_REGENERATE_BRANDING !== 'true') {
+ try { await access(path); return; } catch (error) { if (error.code !== 'ENOENT') throw error; }
+ }
+ await generate();
+}
await mkdir('public/images',{recursive:true});
await mkdir('public/icons',{recursive:true});
await mkdir('public/fonts',{recursive:true});
@@ -9,13 +16,13 @@ const hero='src/assets/delivery-hero-generated.png';
// Encoding/size derivatives of the approved generated asset, not substitute artwork.
await sharp(hero).resize({width:1774,withoutEnlargement:true}).webp({quality:82}).toFile('public/images/delivery-hero.webp');
await sharp(hero).resize({width:900,withoutEnlargement:true}).webp({quality:78}).toFile('public/images/delivery-hero-small.webp');
-await sharp(hero).resize(1200,630,{fit:'cover'}).jpeg({quality:86}).toFile('public/images/social-preview.jpg');
+await generateBranding('public/images/social-preview.jpg', () => sharp(hero).resize(1200,630,{fit:'cover'}).jpeg({quality:86}).toFile('public/images/social-preview.jpg'));
// Versioned URLs avoid stale cached artwork; retain the prior source/derivatives.
const refinedHero='src/assets/delivery-hero-v2.png';
await sharp(refinedHero).webp({quality:92,effort:6}).toFile('public/images/delivery-v2-hero.webp');
await sharp(refinedHero).resize({width:1000,withoutEnlargement:true}).webp({quality:88,effort:6}).toFile('public/images/delivery-v2-hero-small.webp');
await sharp(refinedHero).resize(1200,630,{fit:'cover'}).jpeg({quality:92}).toFile('public/images/delivery-v2-social-preview.jpg');
-for(const size of [32,180,192])await sharp('src/assets/favicon.png').resize(size,size).png().toFile(`public/icons/icon-${size}.png`);
+for(const size of [32,180,192])await generateBranding(`public/icons/icon-${size}.png`, () => sharp('src/assets/favicon.png').resize(size,size).png().toFile(`public/icons/icon-${size}.png`));
for(const file of await readdir('src/assets/heroes')){
if(!file.endsWith('.png'))continue;
const key=file.slice(0,-4),source=`src/assets/heroes/${file}`;
diff --git a/frontend/src/ConsolePageHeader.tsx b/frontend/src/ConsolePageHeader.tsx
new file mode 100644
index 0000000..1d455e1
--- /dev/null
+++ b/frontend/src/ConsolePageHeader.tsx
@@ -0,0 +1,9 @@
+import type { ReactNode } from 'react';
+import './console-page-header.css';
+
+/** One typography and spacing contract for every console tab. */
+export default function ConsolePageHeader({ eyebrow, title, description, actions }: {
+ eyebrow: string; title: string; description: string; actions?: ReactNode;
+}) {
+ return
{eyebrow}
{title}
{description}
{actions &&
{actions}
};
+}
diff --git a/frontend/src/RunConsole.test.tsx b/frontend/src/RunConsole.test.tsx
index 283f264..b67b91a 100644
--- a/frontend/src/RunConsole.test.tsx
+++ b/frontend/src/RunConsole.test.tsx
@@ -29,6 +29,7 @@ it('places Account in the sidebar with a decorative icon matching the navigation
expect(account).toHaveClass('account-console-link');
expect(account.querySelector('.nav-icon')).toHaveAttribute('aria-hidden', 'true');
expect(account.querySelector('svg')).toHaveAttribute('width', '17');
+ expect(screen.getByRole('link', { name: 'ForgeLoop' }).querySelector('img')).toHaveAttribute('src', '/icons/icon-192.png?v=20261012');
expect(screen.queryByRole('link', { name: 'Support' })).not.toBeInTheDocument();
});
@@ -194,7 +195,7 @@ it('provides an in-app operator guide as the fifth navigation item', async () =>
fireEvent.click(guide);
expect(guide).toHaveAttribute('aria-expanded', 'false');
expect(screen.queryByRole('button', { name: 'Quick start' })).not.toBeInTheDocument();
- expect(screen.getByRole('heading', { name: 'Using ForgeLoop' })).toBeInTheDocument();
+ expect(screen.getByRole('heading', { name: 'User guide' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'What every setting does' })).toBeInTheDocument();
expect(screen.getByText('Auto-merge', { selector: 'dt' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'When something does not move' })).toBeInTheDocument();
diff --git a/frontend/src/RunnerPairingPage.tsx b/frontend/src/RunnerPairingPage.tsx
index fd145d0..ab7975d 100644
--- a/frontend/src/RunnerPairingPage.tsx
+++ b/frontend/src/RunnerPairingPage.tsx
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { LockKeyhole, ShieldCheck } from 'lucide-react';
-import favicon from './assets/favicon.png';
+import { brandIcon as favicon } from './brand';
import './runner-pairing.css';
import { approveRunnerPairing, loadOperator } from './api';
diff --git a/frontend/src/TeamPage.tsx b/frontend/src/TeamPage.tsx
index e0924b7..4ad1621 100644
--- a/frontend/src/TeamPage.tsx
+++ b/frontend/src/TeamPage.tsx
@@ -1,3 +1,4 @@
+import ConsolePageHeader from './ConsolePageHeader';
import { useEffect, useState, type FormEvent } from 'react';
import { inviteGithubUser, loadOrganizationMembers, revokeOrganizationMember, type OperatorSession, type OrganizationMember } from './api';
import './team.css';
@@ -29,7 +30,7 @@ export default function TeamPage({operator}: {operator: OperatorSession}) {
finally { setBusy(false); }
}
return
-
ORGANIZATION ACCESS
Team
Invite GitHub users to {operator.organizationId}. Only administrators can manage access.
+
Invite a person
Enter their GitHub username. ForgeLoop resolves it to GitHub's permanent account ID before granting access. No email is sent; share your ForgeLoop URL with them.
diff --git a/frontend/src/UserGuidePage.tsx b/frontend/src/UserGuidePage.tsx
index 8108ab8..e785dfc 100644
--- a/frontend/src/UserGuidePage.tsx
+++ b/frontend/src/UserGuidePage.tsx
@@ -1,3 +1,4 @@
+import ConsolePageHeader from './ConsolePageHeader';
import MarkdownContent from "./MarkdownContent";
import runnerSetup from "./docs/runner-setup.md?raw";
@@ -21,7 +22,7 @@ const settings = [
export default function UserGuidePage() {
return (
-
Operator handbook
Using ForgeLoop
From GitHub issue to verified pull request: setup, operating procedures, safety boundaries, and every configurable setting.
+
Start here
How delivery works
{lifecycle.map(([number,title,body])=>
{number}
{title}
{body}
)}
diff --git a/frontend/src/account/AccountPage.tsx b/frontend/src/account/AccountPage.tsx
index ea1ee6b..fe0a7d0 100644
--- a/frontend/src/account/AccountPage.tsx
+++ b/frontend/src/account/AccountPage.tsx
@@ -1,3 +1,4 @@
+import ConsolePageHeader from '../ConsolePageHeader';
import { ArrowUpRight, BarChart3, Settings2, ShieldCheck } from 'lucide-react';
import type { OrganizationMember, OperatorSession } from '../api';
import SupportApp from '../support/SupportApp';
@@ -15,7 +16,7 @@ export default function AccountPage({ operator, workspaces, onUsage, onWorkspace
const activeWorkspace = workspaces.find(item => item.organizationId === operator.organizationId);
return
-
Profile & help
Account
Review your signed-in identity, workspace access, and support requests.
Describe the change, refine a reviewable specification, then choose whether to create the issue.
-
-
+ }/>
Messages and drafts are saved in this organization workspace; do not include secrets. Each reply may incur provider charges, shown under Usage & costs.
{error &&
{error}
}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 0f40c07..6e54e42 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,3 +1,4 @@
+import ConsolePageHeader from './ConsolePageHeader';
import {
FormEvent,
useEffect,
@@ -44,7 +45,7 @@ import {
type RunAnalytics,
type PlatformConfiguration,
} from "./api";
-import favicon from "./assets/favicon.png";
+import { brandIcon as favicon } from './brand';
import MarkdownContent from "./MarkdownContent";
import UserGuidePage from "./UserGuidePage";
import RunnerSetup from "./RunnerSetup";
@@ -84,18 +85,7 @@ function RepositoryPage({ items, operator, onSettings }: { items: RepositoryConn
return (
<>
-
-
-
Connected repositories
-
- Issue intake and verification are bound to the persisted policy
- revision.
-
Hard limits applied before work enters the runner queue.
diff --git a/frontend/src/onboarding/OnboardingPage.tsx b/frontend/src/onboarding/OnboardingPage.tsx
index 5b09262..65b37dd 100644
--- a/frontend/src/onboarding/OnboardingPage.tsx
+++ b/frontend/src/onboarding/OnboardingPage.tsx
@@ -1,3 +1,4 @@
+import ConsolePageHeader from '../ConsolePageHeader';
import { useEffect, useRef, useState, type FormEvent } from 'react';
import { checkIssueIntake, loadPlatformConfiguration, loadRepositoryConnections, loadRunners, loadRuns,
type FeatureRun, type IssueIntakeCheck, type OperatorSession, type PlatformConfiguration, type RepositoryConnection, type Runner } from '../api';
@@ -40,8 +41,7 @@ export default function OnboardingPage({ operator, navigate }: Props) {
const issueUrl = repository && newIssueUrl(repository.repository, repository.issueLabel, repository.requiredAssignee, repository.requireAssignee);
const admin = operator.role === 'ADMIN';
return
-
Getting started
-
Connect a repository to your own runner, then follow one issue through verified delivery.
+
These checks do not call a model or start work. Provider keys stay on your runner. Starting a runner with eligible queued work can spend API credits.
{error &&
{error}
}
diff --git a/frontend/src/public/PublicLayout.tsx b/frontend/src/public/PublicLayout.tsx
index 67a513d..4e58686 100644
--- a/frontend/src/public/PublicLayout.tsx
+++ b/frontend/src/public/PublicLayout.tsx
@@ -1,5 +1,5 @@
import type { ReactNode } from 'react';
-const favicon='/icons/icon-192.png';
+import { brandIcon as favicon } from '../brand';
import { REPOSITORY } from './content';
import { heroStyle } from './heroes';
import { BookOpen, CircleHelp, Download, FileText, Info, Layers, LifeBuoy, Menu, Shield, X } from 'lucide-react';
diff --git a/frontend/src/usage/UsagePage.tsx b/frontend/src/usage/UsagePage.tsx
index 7b5fa5f..b37e13c 100644
--- a/frontend/src/usage/UsagePage.tsx
+++ b/frontend/src/usage/UsagePage.tsx
@@ -1,3 +1,4 @@
+import ConsolePageHeader from '../ConsolePageHeader';
import { useEffect, useState } from 'react';
import { loadProviderActivities, loadRunAnalytics, loadRuns, type FeatureRun, type ProviderActivity, type RunAnalytics } from '../api';
import { usageSummary, usd, type UsageBucket } from './usage';
@@ -28,7 +29,7 @@ export default function UsagePage({ initialAnalytics, onSettings }: { initialAna
const usage = runs && usageSummary(runs, days, repository, updated, activities);
const count = usage ? usage.total.priced + usage.total.unpriced : 0;
return
-
Organization usage
Usage & costs
Review estimated provider spend across runs and AI-assisted workspace activity.
+ Budget & execution settings}/>
Costs are estimates from recorded provider usage, not provider invoices. The report includes delivery runs, repository scans, and any metered issue-spec or chat activity. Unpriced requests are shown as N/A, never counted as free. Archived runs remain included.