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:
@@ -59,7 +59,7 @@ const LogoHeader = React.memo(function LogoHeader(t0) {
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <LogoV2 />;
|
||||
t1 = null;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
|
||||
179
src/components/StartupScreen.ts
Normal file
179
src/components/StartupScreen.ts
Normal 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')
|
||||
}
|
||||
Reference in New Issue
Block a user