Unify branding: VT323 logotype across website, TUI, and OAuth pages

- Add VT323 ASCII art logo to logo.mjs as single source of truth
- Website nav and hero use VT323 font via AsciiLogo.astro component
- TUI header and CLI help render the ASCII logo with block-centered alignment
- OAuth callback pages (Pi and alphaXiv) show branded feynman logotype
- Auto-set recommended model after provider login

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Advait Paliwal
2026-03-23 23:28:54 -07:00
parent cd0e5d953a
commit 779dd2441c
10 changed files with 63 additions and 27 deletions

View File

@@ -230,8 +230,10 @@ export function installFeynmanHeader(
push("");
if (cardW >= 70) {
const maxLogoW = Math.max(...FEYNMAN_AGENT_LOGO.map((l) => l.length));
const logoOffset = " ".repeat(Math.max(0, Math.floor((cardW - maxLogoW) / 2)));
for (const logoLine of FEYNMAN_AGENT_LOGO) {
push(theme.fg("accent", theme.bold(centerText(truncateVisible(logoLine, cardW), cardW))));
push(theme.fg("accent", theme.bold(`${logoOffset}${truncateVisible(logoLine, cardW)}`)));
}
push("");
}

View File

@@ -1,3 +1,3 @@
export declare const FEYNMAN_ASCII_LOGO: string[];
export declare const FEYNMAN_ASCII_LOGO_TEXT: string;
export declare const FEYNMAN_ASCII_LOGO_HTML: string;
export declare const FEYNMAN_LOGO_HTML: string;

View File

@@ -1,12 +1,15 @@
export const FEYNMAN_ASCII_LOGO = [
"███████╗███████╗██╗ ██╗███╗ ██╗███╗ ███╗ █████╗ ███╗ ██╗",
"██╔════╝██╔════╝╚██╗ ██╔╝████╗ ██║████╗ ████║██╔══██╗████╗ ██║",
"█████ █████ ╚████╔╝ ██╔██ █████████████████████╗ ██║",
"██╔══╝ ██╔══╝ ██╔╝ ██║╚██╗██║██║╚██╔╝██║██╔══██║██║╚██╗██",
"██║ ███████╗ ██║ ██║ ╚████║██║ ╚═╝ ██║██║ ██║██║ ╚████║",
"╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝",
" ██████",
"██",
"█████████ ████████ ███ ███ ███ ██████ ██ ███ ████ ███████ ███ ██████",
" ███ ███ ██ ███ ███ ████ ███ ███ ██ ███ ███ ████ ███",
" ███ ████████████ ███ ███ ███ ███ ███ ██ ███ █████████ ███ ███",
" ███ ███ ██ ███ ███ ███ ███ ██ ███ ███ ███ ███ ███",
"███████ ████████ ████ ███ ███ ███ ██ ███ ██████ ███ ███ ███",
" ███",
" █████",
];
export const FEYNMAN_ASCII_LOGO_TEXT = FEYNMAN_ASCII_LOGO.join("\n");
export const FEYNMAN_LOGO_HTML = `<link href="https://fonts.googleapis.com/css2?family=Silkscreen:wght@700&display=swap" rel="stylesheet"><span style="font-family:'Silkscreen',cursive;font-size:48px;font-weight:700;color:#10b981">feynman</span>`;
export const FEYNMAN_LOGO_HTML = `<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet"><span style="font-family:'VT323',monospace;font-size:64px;letter-spacing:-0.05em;color:#10b981">feynman</span>`;

View File

@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { FEYNMAN_ASCII_LOGO_HTML } from "../logo.mjs";
import { FEYNMAN_LOGO_HTML } from "../logo.mjs";
const here = dirname(fileURLToPath(import.meta.url));
const appRoot = resolve(here, "..");
@@ -365,7 +365,7 @@ if (oauthPagePath && existsSync(oauthPagePath)) {
let source = readFileSync(oauthPagePath, "utf8");
const piLogo = 'const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" aria-hidden="true"><path fill="#fff" fill-rule="evenodd" d="M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z M282.65 282.65 V400 H400 V282.65 Z"/><path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/></svg>`;';
if (source.includes(piLogo)) {
const feynmanLogo = `const LOGO_SVG = \`${FEYNMAN_ASCII_LOGO_HTML}\`;`;
const feynmanLogo = `const LOGO_SVG = \`${FEYNMAN_LOGO_HTML}\`;`;
source = source.replace(piLogo, feynmanLogo);
writeFileSync(oauthPagePath, source, "utf8");
}
@@ -377,17 +377,17 @@ const alphaHubAuthPath = findPackageRoot("@companion-ai/alpha-hub")
if (alphaHubAuthPath && existsSync(alphaHubAuthPath)) {
let source = readFileSync(alphaHubAuthPath, "utf8");
const callbackStyle = `style="font-family:system-ui,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:80vh;background:#050a08;color:#f0f5f2"`;
const logoHtml = FEYNMAN_ASCII_LOGO_HTML.replace('color:#10b981', 'color:#34d399');
const successPage = `<html><body ${callbackStyle}>${logoHtml}<h2 style="color:#34d399;margin-top:24px">Logged in</h2><p style="color:#8aaa9a">You can close this tab.</p></body></html>`;
const errorPage = `<html><body ${callbackStyle}>${logoHtml}<h2 style="color:#ef4444;margin-top:24px">Login failed</h2><p style="color:#8aaa9a">You can close this tab.</p></body></html>`;
const oldSuccess = `'<html><body><h2>Logged in to Alpha Hub</h2><p>You can close this tab.</p></body></html>'`;
const oldError = `'<html><body><h2>Login failed</h2><p>You can close this tab.</p></body></html>'`;
const oldSuccess = "'<html><body><h2>Logged in to Alpha Hub</h2><p>You can close this tab.</p></body></html>'";
const oldError = "'<html><body><h2>Login failed</h2><p>You can close this tab.</p></body></html>'";
const bodyAttr = `style="font-family:system-ui,sans-serif;text-align:center;padding-top:20vh;background:#050a08;color:#f0f5f2"`;
const logo = `<h1 style="font-family:monospace;font-size:48px;color:#34d399;margin:0">feynman</h1>`;
const newSuccess = `'<html><body ${bodyAttr}>${logo}<h2 style="color:#34d399;margin-top:16px">Logged in</h2><p style="color:#8aaa9a">You can close this tab.</p></body></html>'`;
const newError = `'<html><body ${bodyAttr}>${logo}<h2 style="color:#ef4444;margin-top:16px">Login failed</h2><p style="color:#8aaa9a">You can close this tab.</p></body></html>'`;
if (source.includes(oldSuccess)) {
source = source.replace(oldSuccess, `'${successPage}'`);
source = source.replace(oldSuccess, newSuccess);
}
if (source.includes(oldError)) {
source = source.replace(oldError, `'${errorPage}'`);
source = source.replace(oldError, newError);
}
writeFileSync(alphaHubAuthPath, source, "utf8");
}

View File

@@ -28,7 +28,7 @@ import { printSearchStatus } from "./search/commands.js";
import { runDoctor, runStatus } from "./setup/doctor.js";
import { setupPreviewDependencies } from "./setup/preview.js";
import { runSetup } from "./setup/setup.js";
import { printInfo, printPanel, printSection } from "./ui/terminal.js";
import { printAsciiHeader, printInfo, printPanel, printSection } from "./ui/terminal.js";
import {
cliCommandSections,
formatCliWorkflowUsage,
@@ -50,7 +50,7 @@ function printHelp(appRoot: string): void {
(command) => command.section === "Research Workflows" && command.topLevelCli,
);
printPanel("Feynman", [
printAsciiHeader([
"Research-first agent shell built on Pi.",
"Use `feynman setup` first if this is a new machine.",
]);
@@ -123,7 +123,7 @@ async function handleModelCommand(subcommand: string | undefined, args: string[]
}
if (subcommand === "login") {
await loginModelProvider(feynmanAuthPath, args[0]);
await loginModelProvider(feynmanAuthPath, args[0], feynmanSettingsPath);
return;
}

View File

@@ -6,6 +6,7 @@ import { promptChoice, promptText } from "../setup/prompts.js";
import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
import {
buildModelStatusSnapshotFromRecords,
chooseRecommendedModel,
getAvailableModelRecords,
getSupportedModelRecords,
type ModelStatusSnapshot,
@@ -109,7 +110,7 @@ export function printModelList(settingsPath: string, authPath: string): void {
}
}
export async function loginModelProvider(authPath: string, providerId?: string): Promise<void> {
export async function loginModelProvider(authPath: string, providerId?: string, settingsPath?: string): Promise<void> {
const provider = providerId ? resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login");
if (!provider) {
if (providerId) {
@@ -143,6 +144,21 @@ export async function loginModelProvider(authPath: string, providerId?: string):
});
printSuccess(`Model provider login complete: ${provider.id}`);
if (settingsPath) {
const currentSpec = getCurrentModelSpec(settingsPath);
const available = getAvailableModelRecords(authPath);
const currentValid = currentSpec
? available.some((m) => `${m.provider}/${m.id}` === currentSpec)
: false;
if ((!currentSpec || !currentValid) && available.length > 0) {
const recommended = chooseRecommendedModel(authPath);
if (recommended) {
setDefaultModelSpec(settingsPath, authPath, recommended.spec);
}
}
}
}
export async function logoutModelProvider(authPath: string, providerId?: string): Promise<void> {

View File

@@ -1,3 +1,5 @@
import { FEYNMAN_ASCII_LOGO } from "../../logo.mjs";
const RESET = "\x1b[0m";
const BOLD = "\x1b[1m";
const DIM = "\x1b[2m";
@@ -40,6 +42,17 @@ export function printSection(title: string): void {
console.log(paint(`${title}`, TEAL, BOLD));
}
export function printAsciiHeader(subtitleLines: string[] = []): void {
console.log("");
for (const line of FEYNMAN_ASCII_LOGO) {
console.log(paint(` ${line}`, TEAL, BOLD));
}
for (const line of subtitleLines) {
console.log(paint(` ${line}`, ASH));
}
console.log("");
}
export function printPanel(title: string, subtitleLines: string[] = []): void {
const inner = 53;
const border = "─".repeat(inner + 2);

View File

@@ -7,13 +7,13 @@ interface Props {
const { class: className = '', size = 'hero' } = Astro.props;
const sizeClasses = size === 'nav'
? 'text-lg'
: 'text-4xl sm:text-5xl md:text-6xl';
? 'text-2xl'
: 'text-6xl sm:text-7xl md:text-8xl';
---
<span
class:list={[
"font-['Silkscreen'] text-accent font-bold tracking-tight inline-block",
"font-['VT323'] text-accent inline-block tracking-tighter",
sizeClasses,
className,
]}

View File

@@ -22,7 +22,7 @@ const { title, description = 'Research-first AI agent', active = 'home' } = Astr
<title>{title}</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Silkscreen:wght@400;700&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet" />
<ViewTransitions fallback="none" />
<script is:inline>
(function() {

View File

@@ -1,10 +1,12 @@
---
import Base from '../layouts/Base.astro';
import AsciiLogo from '../components/AsciiLogo.astro';
---
<Base title="Feynman — The open source AI research agent" active="home">
<section class="text-center pt-24 pb-20 px-6">
<div class="max-w-2xl mx-auto">
<AsciiLogo size="hero" class="mb-4" />
<h1 class="text-5xl sm:text-6xl font-bold tracking-tight mb-6" style="text-wrap: balance">The open source AI research agent</h1>
<p class="text-lg text-text-muted mb-10 leading-relaxed" style="text-wrap: pretty">Investigate topics, write papers, run experiments, review research, audit codebases &mdash; every output cited and source-grounded</p>
<div class="inline-flex items-center gap-3 bg-surface rounded-lg px-5 py-3 mb-8 font-mono text-sm">