perf(tui): paint banner to stdout in ~2ms, before Ink loads

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 `<AlternateScreen>` 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.
This commit is contained in:
Brooklyn Nicholson
2026-04-16 15:48:41 -05:00
parent 04e36851b7
commit 9503896aa2
2 changed files with 44 additions and 4 deletions

36
ui-tui/src/bootBanner.ts Normal file
View File

@@ -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; `<AlternateScreen>` 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`
)
}

View File

@@ -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).
// `<AlternateScreen>` 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()