Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion src/components/StartupScreen.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
import stripAnsi from 'strip-ansi'
import { VERBOO_ROUTER_URL } from '../constants/oauth.js'
import { detectProvider } from './StartupScreen.js'
import { detectProvider, renderStartupScreen } from './StartupScreen.js'
import { saveGlobalConfig } from '../utils/config.js'
import {
resetSettingsCache,
Expand Down Expand Up @@ -185,3 +186,41 @@ describe('detectProvider — Verboo isolation', () => {
)
})
})

describe('renderStartupScreen', () => {
const provider = {
name: 'Verboo',
model: 'early-adopters/qwen3.6-27b',
baseUrl: 'https://code.verboo.ai/router/v1',
isLocal: false,
}

test('renders the mascot layout safely in a wide terminal', () => {
const output = renderStartupScreen(provider, '0.14.5', '~/project', 120)
const plainOutput = stripAnsi(output)

expect(plainOutput).toContain('▄▀▀▀▀▀▀▀▄')
expect(plainOutput).toContain('Verboo Code')
expect(output).toContain('\x1b[0m')
expect(output).not.toContain('undefined')
expect(plainOutput.split('\n').every(line => line.length <= 120)).toBe(true)
})

test('uses the compact header in a narrow terminal', () => {
const output = stripAnsi(
renderStartupScreen(
{
...provider,
model: 'a-provider-model-name-that-is-long-enough-to-wrap',
},
'0.14.5',
'~/a/very/long/project/path/that/would/wrap',
70,
),
)

expect(output).toContain('👻')
expect(output).not.toContain('▄▀▀▀▀▀▀▀▄')
expect(output.split('\n').every(line => line.length <= 70)).toBe(true)
})
})
141 changes: 121 additions & 20 deletions src/components/StartupScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,32 @@ const LOGO_VERBOO_CODE = [
` \u2570\u2500\u2500\u2500\u256f \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u256f\u2570\u2500\u256f \u2570\u2500\u256f\u2570\u2500\u2500\u2500\u2500\u2500\u256f \u2570\u2500\u2500\u2500\u2500\u2500\u256f \u2570\u2500\u2500\u2500\u2500\u2500\u256f \u2570\u2500\u2500\u2500\u2500\u2500\u256f \u2570\u2500\u2500\u2500\u2500\u2500\u256f \u2570\u2500\u2500\u2500\u2500\u2500\u256f \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u256f`,
]

// ─── Verboo ASCII Logo ────────────────────────────────────────────────────────

const VERBOO_LOGO = [
` ▄▀▀▀▀▀▀▀▄ `,
`▄▀▀▀▀▀▀▀▀▀▀▀▄`,
`▀▀▀ ▀▀▀▀▀ ▀▀▀`,
`▀▀▀▀▀▀▀▀▀▀▀▀▀`,
`▀▀▀▀▀▄▄▄▀▀▀▀▀`,
` ▀▀▀▀▀▀▀▀▀▀▀ `,
`▄▀▀ ▀▀▀▀▀ ▀▀▄`,
]

// Máscara 1:1 com VERBOO_LOGO — 1 = bg roxo (bloco cheio), 0 = fg só (meio-bloco)
// Edite os 1/0 para escolher quais caracteres ficam totalmente preenchidos
const VERBOO_LOGO_MASK = [
` 011111110 `,
`0111111111110`,
`1110111110111`,
`1111011101111`,
`1111100011111`,
` 11111111111 `,
`110 01110 011`,
]

const STARTUP_LOGO_MIN_COLUMNS = 80

// ─── Provider detection ───────────────────────────────────────────────────────

function resolveVerbooStartupModel(modelOverride?: string): string {
Expand Down Expand Up @@ -214,38 +240,113 @@ function boxRow(content: string, width: number, rawLen: number): string {

// ─── Main ─────────────────────────────────────────────────────────────────────

// VERBOO-BRAND: compact rounded header (replaces giant ASCII splash).
// Layout inspired by the V2 logo style \u2014 fantasma + name + meta on the right.
export function printStartupScreen(modelOverride?: string): void {
// Skip in non-interactive / CI / print mode
if (process.env.CI || !process.stdout.isTTY) return
function truncateStartupText(text: string, maxLength: number): string {
if (maxLength <= 0) return ''
if (text.length <= maxLength) return text
if (maxLength === 1) return '\u2026'
return `${text.slice(0, maxLength - 1)}\u2026`
}

const p = detectProvider(modelOverride)
export function renderStartupScreen(
p: ReturnType<typeof detectProvider>,
version: string,
displayCwd: string,
columns: number,
): string {
const out: string[] = []

// Resolve cwd to a tilde-shortened display path
const home = process.env.HOME || process.env.USERPROFILE || ''
const cwd = process.cwd()
const displayCwd = home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd

const version = MACRO.DISPLAY_VERSION ?? MACRO.VERSION
const bold = `${ESC}1m`
const PURPLE = rgb(...ACCENT)
const PURPLE_FILL = `${rgb(...ACCENT)}${ESC}48;2;${ACCENT[0]};${ACCENT[1]};${ACCENT[2]}m`
const SOFT = rgb(...CREAM)
const DIMP = `${DIM}${rgb(...DIMCOL)}`
const STATUS_C = p.isLocal ? rgb(130, 200, 140) : PURPLE
const statusLabel = p.isLocal ? 'local' : 'cloud'
const ep = p.baseUrl.length > 48 ? p.baseUrl.slice(0, 45) + '...' : p.baseUrl
const LOGO_TEXT_PADDING = 2

out.push('')
out.push(` ${PURPLE}\ud83d\udc7b${RESET} ${bold}${SOFT}Verboo Code${RESET} ${DIMP}v${version}${RESET}`)
out.push(` ${DIMP}Tokens ilimitados \u00b7 Privacidade \u00b7 Velocidade${RESET}`)
out.push(` ${DIMP}${p.name} \u00b7 ${p.model}${RESET}`)
out.push(` ${DIMP}${ep}${RESET}`)
out.push(` ${DIMP}${displayCwd}${RESET}`)

if (columns < STARTUP_LOGO_MIN_COLUMNS) {
const compactTextWidth = Math.max(1, columns - 6)
const provider = truncateStartupText(`${p.name} \u00b7 ${p.model}`, compactTextWidth)
const endpoint = truncateStartupText(p.baseUrl, compactTextWidth)
const cwd = truncateStartupText(displayCwd, compactTextWidth)

out.push(` ${PURPLE}\ud83d\udc7b${RESET} ${bold}${SOFT}Verboo Code${RESET} ${DIMP}v${version}${RESET}`)
out.push(` ${DIMP}Tokens ilimitados \u00b7 Privacidade \u00b7 Velocidade${RESET}`)
out.push(` ${DIMP}${provider}${RESET}`)
out.push(` ${DIMP}${endpoint}${RESET}`)
out.push(` ${DIMP}${cwd}${RESET}`)
} else {
const maxLogoWidth = Math.max(...VERBOO_LOGO.map(line => line.trimEnd().length))
const rightTextWidth = Math.max(
1,
columns - 2 - maxLogoWidth - LOGO_TEXT_PADDING,
)
const provider = truncateStartupText(`${p.name} \u00b7 ${p.model}`, rightTextWidth)
const endpoint = truncateStartupText(p.baseUrl, rightTextWidth)
const cwd = truncateStartupText(displayCwd, rightTextWidth)
const rightCol = [
``,
`${bold}${SOFT}Verboo Code${RESET} ${DIMP}v${version}${RESET}`,
`${DIMP}Tokens ilimitados \u00b7 Privacidade \u00b7 Velocidade${RESET}`,
`${DIMP}${provider}${RESET}`,
`${DIMP}${endpoint}${RESET}`,
`${DIMP}${cwd}${RESET}`,
``,
]

const paintedLogo: { text: string; visualWidth: number }[] = []
for (let i = 0; i < VERBOO_LOGO.length; i++) {
const line = (VERBOO_LOGO[i] ?? '').trimEnd()
const mask = VERBOO_LOGO_MASK[i] ?? ''
let painted = ''
for (let j = 0; j < line.length; j++) {
const ch = line[j] ?? ''
const isBlock = ch === '\u2580' || ch === '\u2584'
if (isBlock && mask[j] === '1') {
painted += `${PURPLE_FILL}${ch}${RESET}`
} else if (isBlock) {
painted += `${PURPLE}${ch}${RESET}`
} else {
painted += ch
}
}
paintedLogo.push({ text: painted, visualWidth: line.length })
}

for (let i = 0; i < paintedLogo.length; i++) {
const { text, visualWidth } = paintedLogo[i] ?? {
text: '',
visualWidth: 0,
}
const gap = ' '.repeat(
Math.max(0, maxLogoWidth - visualWidth + LOGO_TEXT_PADDING),
)
out.push(` ${text}${gap}${rightCol[i] ?? ''}`)
}
}

out.push('')
out.push(` ${STATUS_C}\u25cf${RESET} ${DIMP}${statusLabel}${RESET} ${DIMP}Ready \u2014 type ${RESET}${PURPLE}/help${RESET}${DIMP} to begin${RESET}`)
out.push('')

process.stdout.write(out.join('\n') + '\n')
return `${out.join('\n')}\n`
}

// VERBOO-BRAND: compact rounded header (replaces giant ASCII splash).
// Layout inspired by the V2 logo style \u2014 logo + name + meta on the right.
export function printStartupScreen(modelOverride?: string): void {
// Skip in non-interactive / CI / print mode
if (process.env.CI || !process.stdout.isTTY) return

const p = detectProvider(modelOverride)

// Resolve cwd to a tilde-shortened display path
const home = process.env.HOME || process.env.USERPROFILE || ''
const cwd = process.cwd()
const displayCwd = home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd

const version = MACRO.DISPLAY_VERSION ?? MACRO.VERSION
const columns = process.stdout.columns ?? STARTUP_LOGO_MIN_COLUMNS
process.stdout.write(renderStartupScreen(p, version, displayCwd, columns))
}