Merge pull request #70 from gnanam1990/feat/gradient-startup-screen
feat: gradient startup screen with provider info
This commit is contained in:
@@ -59,7 +59,7 @@ const LogoHeader = React.memo(function LogoHeader(t0) {
|
|||||||
} = t0;
|
} = t0;
|
||||||
let t1;
|
let t1;
|
||||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||||
t1 = <LogoV2 />;
|
t1 = null;
|
||||||
$[0] = t1;
|
$[0] = t1;
|
||||||
} else {
|
} else {
|
||||||
t1 = $[0];
|
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')
|
||||||
|
}
|
||||||
@@ -100,6 +100,10 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
validateProviderEnvOrExit()
|
validateProviderEnvOrExit()
|
||||||
|
|
||||||
|
// Print the gradient startup screen before the Ink UI loads
|
||||||
|
const { printStartupScreen } = await import('../components/StartupScreen.js')
|
||||||
|
printStartupScreen()
|
||||||
|
|
||||||
// For all other paths, load the startup profiler
|
// For all other paths, load the startup profiler
|
||||||
const {
|
const {
|
||||||
profileCheckpoint
|
profileCheckpoint
|
||||||
|
|||||||
Reference in New Issue
Block a user