From 9503896aa2923a510b24c9eca919b94813a7889d Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 16 Apr 2026 15:48:41 -0500 Subject: [PATCH] perf(tui): paint banner to stdout in ~2ms, before Ink loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dynamic-importing @hermes/ink + App costs ~170ms on cold start — during that window the terminal was blank. Now `entry.tsx` writes a raw-ANSI banner to stdout immediately after the TTY check, using hardcoded DEFAULT_THEME colors. Ink's `` wipes the normal-screen buffer when it mounts, so the boot banner is replaced seamlessly by the real React render a moment later — no double-banner, no flash. T=2ms banner visible (vs. ~170ms before) T=~170ms React + Ink mounts T=~200ms alt screen takes over, Banner component repaints Palette drift between `bootBanner.ts` and the live theme is harmless — the live render overrides after ~200ms. Narrow terminals (cols < 98) fall back to the one-line "⚕ NOUS HERMES" marker. --- ui-tui/src/bootBanner.ts | 36 ++++++++++++++++++++++++++++++++++++ ui-tui/src/entry.tsx | 12 ++++++++---- 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 ui-tui/src/bootBanner.ts diff --git a/ui-tui/src/bootBanner.ts b/ui-tui/src/bootBanner.ts new file mode 100644 index 000000000..2aac254a2 --- /dev/null +++ b/ui-tui/src/bootBanner.ts @@ -0,0 +1,36 @@ +// Prints the Hermes banner as raw ANSI to stdout before React/Ink load. +// Gives the user instant visual feedback during the ~170ms dynamic-import +// window; `` wipes the normal-screen buffer when Ink +// mounts, so there is no double-banner. +// +// Palette is hardcoded to match DEFAULT_THEME — drifting the theme's +// banner colors here is fine, Ink's real render takes over in ~200ms. + +const GOLD = '\x1b[38;2;255;215;0m' +const AMBER = '\x1b[38;2;255;191;0m' +const BRONZE = '\x1b[38;2;205;127;50m' +const DIM = '\x1b[38;2;184;134;11m' +const RESET = '\x1b[0m' + +const LOGO = [ + '██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗', + '██║ ██║██╔════╝██╔══██╗████╗ ████║██╔════╝██╔════╝ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝', + '███████║█████╗ ██████╔╝██╔████╔██║█████╗ ███████╗█████╗███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ', + '██╔══██║██╔══╝ ██╔══██╗██║╚██╔╝██║██╔══╝ ╚════██║╚════╝██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ', + '██║ ██║███████╗██║ ██║██║ ╚═╝ ██║███████╗███████║ ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ', + '╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ' +] + +const GRADIENT = [GOLD, GOLD, AMBER, AMBER, BRONZE, BRONZE] +const LOGO_WIDTH = 98 + +export function bootBanner(cols: number = process.stdout.columns || 80): string { + const lines = + cols >= LOGO_WIDTH + ? LOGO.map((text, i) => `${GRADIENT[i]}${text}${RESET}`) + : [`\x1b[1m${GOLD}⚕ NOUS HERMES${RESET}`] + + return ( + '\n' + lines.join('\n') + '\n' + `${DIM}⚕ Nous Research · Messenger of the Digital Gods${RESET}\n\n` + ) +} diff --git a/ui-tui/src/entry.tsx b/ui-tui/src/entry.tsx index abbcf7a4c..3c21de93e 100644 --- a/ui-tui/src/entry.tsx +++ b/ui-tui/src/entry.tsx @@ -1,8 +1,10 @@ #!/usr/bin/env node -// Import order matters for cold start: `GatewayClient` has only node-builtin -// deps (<20ms), so spawning the python gateway before loading @hermes/ink -// + App (~200ms combined) gives python ~200ms of free parallel time to run -// its own module imports instead of starting those after node is done. +// Import order matters for cold start: `GatewayClient` + `bootBanner` have +// only node-builtin deps (<20ms), so we can paint the banner and spawn the +// python gateway before loading @hermes/ink + App (~170ms combined). +// `` wipes the normal-screen buffer on Ink mount, so the +// boot banner is replaced seamlessly by the real React render. +import { bootBanner } from './bootBanner.js' import { GatewayClient } from './gatewayClient.js' if (!process.stdin.isTTY) { @@ -10,6 +12,8 @@ if (!process.stdin.isTTY) { process.exit(0) } +process.stdout.write(bootBanner()) + const gw = new GatewayClient() gw.start()