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:
36
ui-tui/src/bootBanner.ts
Normal file
36
ui-tui/src/bootBanner.ts
Normal 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`
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user