diff --git a/apps/website/public/assets/hero.svg b/apps/website/public/assets/hero.svg index a8d053758..8ecf04a4c 100644 --- a/apps/website/public/assets/hero.svg +++ b/apps/website/public/assets/hero.svg @@ -1,31 +1,68 @@ - - + + - + Fonts are generic stacks, not the site's webfonts: this file is served as a + bare image to GitHub, where no stylesheet of ours ever loads. Georgia + stands in for Garamond and is present on every mainstream platform. + --> + + + + + - threadplane + letter-spacing="1.8" + fill="#004090" + >OPEN SOURCE · ANGULAR + + + + + Threadplane + - Threadplane — Production-ready agent UI + font-size="21" + fill="#464646" + >The open-source thread-plane for agents. + + + MIT · LANGGRAPH & AG-UI - - + diff --git a/apps/website/public/whitepaper-preview.html b/apps/website/public/whitepaper-preview.html index 3cf4aa2d2..02d79f170 100644 --- a/apps/website/public/whitepaper-preview.html +++ b/apps/website/public/whitepaper-preview.html @@ -20,8 +20,8 @@
-
Threadplane · Enterprise Angular Agent UI
-

Agent
UI
for
Angular

+
Threadplane · Open source · Angular
+

Threadplane

Production-ready chat, threads, and generative UI for AI agents

threadplane.ai · 2026
diff --git a/apps/website/public/whitepaper.pdf b/apps/website/public/whitepaper.pdf index e422fe8ce..732d06587 100644 Binary files a/apps/website/public/whitepaper.pdf and b/apps/website/public/whitepaper.pdf differ diff --git a/apps/website/scripts/generate-whitepaper.ts b/apps/website/scripts/generate-whitepaper.ts index 2d9562e00..dda7a2d7c 100644 --- a/apps/website/scripts/generate-whitepaper.ts +++ b/apps/website/scripts/generate-whitepaper.ts @@ -69,7 +69,7 @@ const WHITEPAPERS: Record = { id: 'overview', title: 'Threadplane', subtitle: 'Production-ready chat, threads, and generative UI for AI agents', - eyebrow: 'Threadplane · Enterprise Angular Agent UI', + eyebrow: 'Threadplane · Open source · Angular', coverGradient: 'linear-gradient(135deg, #fafbfc 0%, #eaf3ff 100%)', outputPdf: 'apps/website/public/whitepaper.pdf', outputHtml: 'apps/website/public/whitepaper-preview.html', diff --git a/apps/website/scripts/refresh-whitepaper-covers.ts b/apps/website/scripts/refresh-whitepaper-covers.ts index 8e6d92114..b16edc63a 100644 --- a/apps/website/scripts/refresh-whitepaper-covers.ts +++ b/apps/website/scripts/refresh-whitepaper-covers.ts @@ -13,8 +13,13 @@ * 2. Update the cover footer color (#888 → #8b8fa3 / textMuted). * 3. Update TOC row colors (rgba(0,0,0,.06) → #e6e8ee / border; * #444 → #555770 / textSecondary). - * 4. Write the updated HTML back. - * 5. Render to PDF using Puppeteer. + * 4. Replace retired positioning copy on the cover. The rendered artifacts + * outlive the generator config — the overview cover still read "Agent UI + * for Angular" under the eyebrow "Threadplane · Enterprise Angular Agent + * UI" long after both were retired, because regenerating means paying for + * LLM chapter prose nobody wanted to change. + * 5. Write the updated HTML back. + * 6. Render to PDF using Puppeteer. * * Usage: * pnpm tsx apps/website/scripts/refresh-whitepaper-covers.ts @@ -55,6 +60,25 @@ const PAPERS: Paper[] = [ }, ]; +/** + * Retired positioning, and what replaces it. + * + * Straight string swaps rather than regexes: these are exact phrases that + * shipped inside a cover, and a loose pattern here would quietly rewrite + * chapter prose further down the same file. + * + * The guide titles on the other three covers are deliberately absent. "The + * Enterprise Guide to Generative UI in Angular" is the name of a document, + * not a claim about what the product is, and renaming a book people have + * already downloaded buys nothing. + */ +const RETIRED_COPY: ReadonlyArray = [ + ['Threadplane · Enterprise Angular Agent UI', 'Threadplane · Open source · Angular'], + // The generator's config already says `title: 'Threadplane'`; only the + // rendered artifact still carries the old four-line stack. + ['Agent
UI
for
Angular', 'Threadplane'], +]; + const LEGACY_GRADIENT_RE = /background:linear-gradient\(135deg,\s*#[0-9a-fA-F]+\s+0%,\s*#[0-9a-fA-F]+\s+45%,\s*#[0-9a-fA-F]+\s+70%,\s*#[0-9a-fA-F]+\s+100%\)/; @@ -74,6 +98,10 @@ function refreshHtml(html: string, paper: Paper): string { 'border-bottom:1px solid #e6e8ee;font-size:15px;color:#555770', ); + for (const [from, to] of RETIRED_COPY) { + out = out.split(from).join(to); + } + return out; } diff --git a/apps/website/src/lib/brand-assets.spec.ts b/apps/website/src/lib/brand-assets.spec.ts new file mode 100644 index 000000000..8a6eba6d7 --- /dev/null +++ b/apps/website/src/lib/brand-assets.spec.ts @@ -0,0 +1,40 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { RETIRED_POSITIONING } from './public-copy-contract'; + +const REPO_ROOT = join(__dirname, '..', '..', '..', '..'); + +/** + * Artifacts that state the product's positioning but are not generated from + * `positioning.ts`. Each is a file a human has to remember to update, which + * is exactly why this test exists: the README banner and the whitepaper cover + * both went months asserting a tagline that had been replaced. + */ +const BRAND_ASSETS = [ + 'README.md', + 'apps/website/public/assets/hero.svg', + 'apps/website/public/whitepaper-preview.html', + 'apps/website/scripts/generate-whitepaper.ts', + 'libs/a2ui/README.md', + 'libs/ag-ui/README.md', + 'libs/chat/README.md', + 'libs/langgraph/README.md', + 'libs/middleware/README.md', + 'libs/render/README.md', + 'libs/telemetry/README.md', +]; + +describe('brand assets carry current positioning', () => { + it.each(BRAND_ASSETS)('%s states no retired positioning', (relative) => { + const text = readFileSync(join(REPO_ROOT, relative), 'utf8'); + const found = RETIRED_POSITIONING.filter((phrase) => text.includes(phrase)); + expect(found, `${relative} still says: ${found.join(', ')}`).toEqual([]); + }); + + it('names phrases to look for, so the scan cannot pass by being empty', () => { + // A guard whose list is empty passes every file and reports nothing. This + // is the mutation check the list itself cannot perform. + expect(RETIRED_POSITIONING.length).toBeGreaterThan(3); + }); +}); diff --git a/apps/website/src/lib/public-copy-contract.ts b/apps/website/src/lib/public-copy-contract.ts index 22ba68ee9..50ccea97b 100644 --- a/apps/website/src/lib/public-copy-contract.ts +++ b/apps/website/src/lib/public-copy-contract.ts @@ -40,6 +40,28 @@ export const NARRATIVE_MENTIONS: ReadonlyArray = [ ['telemetry hooks aside', /telemetry hooks/iu], ]; +/** + * Positioning we have retired, and must not still be asserting anywhere. + * + * Copy on the website is single-sourced through `positioning.ts` and guarded + * by its own spec. These are the places that are *not*: static SVG banners, + * generated PDF covers, and the package READMEs — artifacts that keep + * rendering happily long after the words on them stopped being true. The + * overview whitepaper cover carried "Agent UI for Angular" for months past + * that phrase's retirement, because regenerating it meant paying for LLM + * chapter prose nobody wanted to touch. + * + * Add a phrase here in the same change that retires it. + */ +export const RETIRED_POSITIONING: readonly string[] = [ + 'AI agent UI framework for Angular', + 'Angular AI Agent UI Framework', + 'Angular AI agent UI framework', + 'Enterprise Angular Agent UI', + 'Production-ready agent UI', + 'thread plane for enterprise agents', +]; + /** Routes retired in favour of the canonical policy. */ export const RETIRED_ROUTE_PATTERN = /\/docs\/telemetry|\/api\/markdown\/telemetry/u;