feat: add gradient startup screen and remove old OPEN box logo

Adds a new startup screen with filled-block text logo and sunset
gradient, printed to stdout before the Ink UI loads. Removes the
old OPEN box logo from the chat UI since the new screen replaces it.

Changes:
- src/components/StartupScreen.ts (NEW) — gradient OPEN CLAUDE logo
  with provider info box (Provider, Model, Endpoint). Auto-detects
  active provider from env vars (OpenAI, Gemini, DeepSeek, Ollama,
  Groq, Mistral, Azure, LM Studio, Anthropic). Skipped in CI and
  non-TTY environments.
- src/entrypoints/cli.tsx — calls printStartupScreen() at startup
  before Ink renders
- src/components/Messages.tsx — removes <LogoV2 /> from LogoHeader
  so the old OPEN box logo no longer appears in the chat UI

Addresses #55.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gnanam1990
2026-04-01 23:57:45 +05:30
parent 00744a814b
commit 9d464f3488
3 changed files with 184 additions and 1 deletions

View File

@@ -0,0 +1,179 @@
/**
* OpenClaude startup screen — filled-block text logo with sunset gradient.
* Called once at CLI startup before the Ink UI renders.
*
* Addresses: https://github.com/Gitlawb/openclaude/issues/55
*/
const ESC = '\x1b['
const RESET = `${ESC}0m`
const DIM = `${ESC}2m`
type RGB = [number, number, number]
const rgb = (r: number, g: number, b: number) => `${ESC}38;2;${r};${g};${b}m`
function lerp(a: RGB, b: RGB, t: number): RGB {
return [
Math.round(a[0] + (b[0] - a[0]) * t),
Math.round(a[1] + (b[1] - a[1]) * t),
Math.round(a[2] + (b[2] - a[2]) * t),
]
}
function gradAt(stops: RGB[], t: number): RGB {
const c = Math.max(0, Math.min(1, t))
const s = c * (stops.length - 1)
const i = Math.floor(s)
if (i >= stops.length - 1) return stops[stops.length - 1]
return lerp(stops[i], stops[i + 1], s - i)
}
function paintLine(text: string, stops: RGB[], lineT: number): string {
let out = ''
for (let i = 0; i < text.length; i++) {
const t = text.length > 1 ? lineT * 0.5 + (i / (text.length - 1)) * 0.5 : lineT
const [r, g, b] = gradAt(stops, t)
out += `${rgb(r, g, b)}${text[i]}`
}
return out + RESET
}
// ─── Colors ───────────────────────────────────────────────────────────────────
const SUNSET_GRAD: RGB[] = [
[255, 180, 100],
[240, 140, 80],
[217, 119, 87],
[193, 95, 60],
[160, 75, 55],
[130, 60, 50],
]
const ACCENT: RGB = [240, 148, 100]
const CREAM: RGB = [220, 195, 170]
const DIMCOL: RGB = [120, 100, 82]
const BORDER: RGB = [100, 80, 65]
// ─── Filled Block Text Logo ───────────────────────────────────────────────────
const LOGO_OPEN = [
` \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557`,
` \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u2550\u255d \u2588\u2588\u2588\u2557 \u2588\u2588\u2551`,
` \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551`,
` \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u2550\u255d \u2588\u2588\u2554\u2550\u2550\u2550\u255d \u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2551`,
` \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u255a\u2588\u2588\u2588\u2551`,
` \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u2550\u255d`,
]
const LOGO_CLAUDE = [
` \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557`,
` \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u2550\u255d \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u2550\u255d`,
` \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 `,
` \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u255d `,
` \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557`,
` \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d`,
]
// ─── Provider detection ───────────────────────────────────────────────────────
function detectProvider(): { name: string; model: string; baseUrl: string; isLocal: boolean } {
const useGemini = process.env.CLAUDE_CODE_USE_GEMINI === '1' || process.env.CLAUDE_CODE_USE_GEMINI === 'true'
const useOpenAI = process.env.CLAUDE_CODE_USE_OPENAI === '1' || process.env.CLAUDE_CODE_USE_OPENAI === 'true'
if (useGemini) {
const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash'
const baseUrl = process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta/openai'
return { name: 'Google Gemini', model, baseUrl, isLocal: false }
}
if (useOpenAI) {
const model = process.env.OPENAI_MODEL || 'gpt-4o'
const baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
const isLocal = /localhost|127\.0\.0\.1|0\.0\.0\.0/.test(baseUrl)
let name = 'OpenAI'
if (/deepseek/i.test(baseUrl) || /deepseek/i.test(model)) name = 'DeepSeek'
else if (/openrouter/i.test(baseUrl)) name = 'OpenRouter'
else if (/together/i.test(baseUrl)) name = 'Together AI'
else if (/groq/i.test(baseUrl)) name = 'Groq'
else if (/mistral/i.test(baseUrl) || /mistral/i.test(model)) name = 'Mistral'
else if (/azure/i.test(baseUrl)) name = 'Azure OpenAI'
else if (/localhost:11434/i.test(baseUrl)) name = 'Ollama'
else if (/localhost:1234/i.test(baseUrl)) name = 'LM Studio'
else if (/llama/i.test(model)) name = 'Meta Llama'
else if (isLocal) name = 'Local'
return { name, model, baseUrl, isLocal }
}
// Default: Anthropic
const model = process.env.ANTHROPIC_MODEL || process.env.CLAUDE_MODEL || 'claude-sonnet-4-6'
return { name: 'Anthropic', model, baseUrl: 'https://api.anthropic.com', isLocal: false }
}
// ─── Box drawing ──────────────────────────────────────────────────────────────
function boxRow(content: string, width: number, rawLen: number): string {
const pad = Math.max(0, width - 2 - rawLen)
return `${rgb(...BORDER)}\u2502${RESET}${content}${' '.repeat(pad)}${rgb(...BORDER)}\u2502${RESET}`
}
// ─── Main ─────────────────────────────────────────────────────────────────────
export function printStartupScreen(): void {
// Skip in non-interactive / CI / print mode
if (process.env.CI || !process.stdout.isTTY) return
const p = detectProvider()
const W = 62
const out: string[] = []
out.push('')
// Gradient logo
const allLogo = [...LOGO_OPEN, '', ...LOGO_CLAUDE]
const total = allLogo.length
for (let i = 0; i < total; i++) {
const t = total > 1 ? i / (total - 1) : 0
if (allLogo[i] === '') {
out.push('')
} else {
out.push(paintLine(allLogo[i], SUNSET_GRAD, t))
}
}
out.push('')
// Tagline
out.push(` ${rgb(...ACCENT)}\u2726${RESET} ${rgb(...CREAM)}Any model. Every tool. Zero limits.${RESET} ${rgb(...ACCENT)}\u2726${RESET}`)
out.push('')
// Provider info box
out.push(`${rgb(...BORDER)}\u2554${'\u2550'.repeat(W - 2)}\u2557${RESET}`)
const lbl = (k: string, v: string, c: RGB = CREAM): [string, number] => {
const padK = k.padEnd(9)
return [` ${DIM}${rgb(...DIMCOL)}${padK}${RESET} ${rgb(...c)}${v}${RESET}`, ` ${padK} ${v}`.length]
}
const provC: RGB = p.isLocal ? [130, 175, 130] : ACCENT
let [r, l] = lbl('Provider', p.name, provC)
out.push(boxRow(r, W, l))
;[r, l] = lbl('Model', p.model)
out.push(boxRow(r, W, l))
const ep = p.baseUrl.length > 38 ? p.baseUrl.slice(0, 35) + '...' : p.baseUrl
;[r, l] = lbl('Endpoint', ep)
out.push(boxRow(r, W, l))
out.push(`${rgb(...BORDER)}\u2560${'\u2550'.repeat(W - 2)}\u2563${RESET}`)
const sC: RGB = p.isLocal ? [130, 175, 130] : ACCENT
const sL = p.isLocal ? 'local' : 'cloud'
const sRow = ` ${rgb(...sC)}\u25cf${RESET} ${DIM}${rgb(...DIMCOL)}${sL}${RESET} ${DIM}${rgb(...DIMCOL)}Ready \u2014 type ${RESET}${rgb(...ACCENT)}/help${RESET}${DIM}${rgb(...DIMCOL)} to begin${RESET}`
const sLen = ` \u25cf ${sL} Ready \u2014 type /help to begin`.length
out.push(boxRow(sRow, W, sLen))
out.push(`${rgb(...BORDER)}\u255a${'\u2550'.repeat(W - 2)}\u255d${RESET}`)
out.push(` ${DIM}${rgb(...DIMCOL)}openclaude v${(globalThis as Record<string, unknown>)['MACRO_DISPLAY_VERSION'] ?? '0.1.4'}${RESET}`)
out.push('')
process.stdout.write(out.join('\n') + '\n')
}