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(""); push("");
if (cardW >= 70) { 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) { 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(""); push("");
} }

View File

@@ -1,3 +1,3 @@
export declare const FEYNMAN_ASCII_LOGO: string[]; export declare const FEYNMAN_ASCII_LOGO: string[];
export declare const FEYNMAN_ASCII_LOGO_TEXT: 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 = [
"███████╗███████╗██╗ ██╗███╗ ██╗███╗ ███╗ █████╗ ███╗ ██╗", " ██████",
"██╔════╝██╔════╝╚██╗ ██╔╝████╗ ██║████╗ ████║██╔══██╗████╗ ██║", "██",
"█████ █████ ╚████╔╝ ██╔██ █████████████████████╗ ██║", "█████████ ████████ ███ ███ ███ ██████ ██ ███ ████ ███████ ███ ██████",
"██╔══╝ ██╔══╝ ██╔╝ ██║╚██╗██║██║╚██╔╝██║██╔══██║██║╚██╗██", " ███ ███ ██ ███ ███ ████ ███ ███ ██ ███ ███ ████ ███",
"██║ ███████╗ ██║ ██║ ╚████║██║ ╚═╝ ██║██║ ██║██║ ╚████║", " ███ ████████████ ███ ███ ███ ███ ███ ██ ███ █████████ ███ ███",
"╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝", " ███ ███ ██ ███ ███ ███ ███ ██ ███ ███ ███ ███ ███",
"███████ ████████ ████ ███ ███ ███ ██ ███ ██████ ███ ███ ███",
" ███",
" █████",
]; ];
export const FEYNMAN_ASCII_LOGO_TEXT = FEYNMAN_ASCII_LOGO.join("\n"); 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 { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path"; import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url"; 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 here = dirname(fileURLToPath(import.meta.url));
const appRoot = resolve(here, ".."); const appRoot = resolve(here, "..");
@@ -365,7 +365,7 @@ if (oauthPagePath && existsSync(oauthPagePath)) {
let source = readFileSync(oauthPagePath, "utf8"); 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>`;'; 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)) { 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); source = source.replace(piLogo, feynmanLogo);
writeFileSync(oauthPagePath, source, "utf8"); writeFileSync(oauthPagePath, source, "utf8");
} }
@@ -377,17 +377,17 @@ const alphaHubAuthPath = findPackageRoot("@companion-ai/alpha-hub")
if (alphaHubAuthPath && existsSync(alphaHubAuthPath)) { if (alphaHubAuthPath && existsSync(alphaHubAuthPath)) {
let source = readFileSync(alphaHubAuthPath, "utf8"); 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 oldSuccess = "'<html><body><h2>Logged in to Alpha Hub</h2><p>You can close this tab.</p></body></html>'";
const logoHtml = FEYNMAN_ASCII_LOGO_HTML.replace('color:#10b981', 'color:#34d399'); const oldError = "'<html><body><h2>Login failed</h2><p>You can close this tab.</p></body></html>'";
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 bodyAttr = `style="font-family:system-ui,sans-serif;text-align:center;padding-top:20vh;background:#050a08;color:#f0f5f2"`;
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 logo = `<h1 style="font-family:monospace;font-size:48px;color:#34d399;margin:0">feynman</h1>`;
const oldSuccess = `'<html><body><h2>Logged in to Alpha Hub</h2><p>You can close this tab.</p></body></html>'`; 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 oldError = `'<html><body><h2>Login failed</h2><p>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)) { if (source.includes(oldSuccess)) {
source = source.replace(oldSuccess, `'${successPage}'`); source = source.replace(oldSuccess, newSuccess);
} }
if (source.includes(oldError)) { if (source.includes(oldError)) {
source = source.replace(oldError, `'${errorPage}'`); source = source.replace(oldError, newError);
} }
writeFileSync(alphaHubAuthPath, source, "utf8"); writeFileSync(alphaHubAuthPath, source, "utf8");
} }

View File

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

View File

@@ -6,6 +6,7 @@ import { promptChoice, promptText } from "../setup/prompts.js";
import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js"; import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
import { import {
buildModelStatusSnapshotFromRecords, buildModelStatusSnapshotFromRecords,
chooseRecommendedModel,
getAvailableModelRecords, getAvailableModelRecords,
getSupportedModelRecords, getSupportedModelRecords,
type ModelStatusSnapshot, 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"); const provider = providerId ? resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login");
if (!provider) { if (!provider) {
if (providerId) { if (providerId) {
@@ -143,6 +144,21 @@ export async function loginModelProvider(authPath: string, providerId?: string):
}); });
printSuccess(`Model provider login complete: ${provider.id}`); 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> { 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 RESET = "\x1b[0m";
const BOLD = "\x1b[1m"; const BOLD = "\x1b[1m";
const DIM = "\x1b[2m"; const DIM = "\x1b[2m";
@@ -40,6 +42,17 @@ export function printSection(title: string): void {
console.log(paint(`${title}`, TEAL, BOLD)); 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 { export function printPanel(title: string, subtitleLines: string[] = []): void {
const inner = 53; const inner = 53;
const border = "─".repeat(inner + 2); const border = "─".repeat(inner + 2);

View File

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

View File

@@ -22,7 +22,7 @@ const { title, description = 'Research-first AI agent', active = 'home' } = Astr
<title>{title}</title> <title>{title}</title>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <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" /> <ViewTransitions fallback="none" />
<script is:inline> <script is:inline>
(function() { (function() {

View File

@@ -1,10 +1,12 @@
--- ---
import Base from '../layouts/Base.astro'; import Base from '../layouts/Base.astro';
import AsciiLogo from '../components/AsciiLogo.astro';
--- ---
<Base title="Feynman — The open source AI research agent" active="home"> <Base title="Feynman — The open source AI research agent" active="home">
<section class="text-center pt-24 pb-20 px-6"> <section class="text-center pt-24 pb-20 px-6">
<div class="max-w-2xl mx-auto"> <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> <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> <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"> <div class="inline-flex items-center gap-3 bg-surface rounded-lg px-5 py-3 mb-8 font-mono text-sm">