From 156d6baf677e18aac7da9c2bbf867d5099471f83 Mon Sep 17 00:00:00 2001 From: ChangedRuby Date: Wed, 22 Jul 2026 09:03:55 -0300 Subject: [PATCH 1/2] add Verboo logo at startup --- src/components/StartupScreen.ts | 72 ++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/src/components/StartupScreen.ts b/src/components/StartupScreen.ts index f3cb88b019..7095b39135 100644 --- a/src/components/StartupScreen.ts +++ b/src/components/StartupScreen.ts @@ -77,6 +77,30 @@ 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`, +] + // ─── Provider detection ─────────────────────────────────────────────────────── function resolveVerbooStartupModel(modelOverride?: string): string { @@ -215,7 +239,7 @@ 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. +// 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 @@ -231,18 +255,54 @@ export function printStartupScreen(modelOverride?: string): void { 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 // padding base entre logo e texto 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}`) + // ── Info lines (lado direito) ── + const rightCol = [ + ``, + `${bold}${SOFT}Verboo Code${RESET} ${DIMP}v${version}${RESET}`, + `${DIMP}Tokens ilimitados \u00b7 Privacidade \u00b7 Velocidade${RESET}`, + `${DIMP}${p.name} \u00b7 ${p.model}${RESET}`, + `${DIMP}${ep}${RESET}`, + `${DIMP}${displayCwd}${RESET}`, + ``, + ] + // ── Pinta o logo e calcula largura visual ── + const paintedLogo: { text: string; visualWidth: number }[] = [] + let maxLogoWidth = 0 + 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 m = mask[j] + const isBlock = ch === '\u2580' || ch === '\u2584' + if (isBlock && m === '1') { + painted += `${PURPLE_FILL}${ch}${RESET}` + } else if (isBlock) { + painted += `${PURPLE}${ch}${RESET}` + } else { + painted += ch + } + } + paintedLogo.push({ text: painted, visualWidth: line.length }) + if (line.length > maxLogoWidth) maxLogoWidth = line.length + } + + // ── Saída lado a lado ── + for (let i = 0; i < paintedLogo.length; i++) { + const { text, visualWidth } = paintedLogo[i] + const gap = ' '.repeat(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('') From 3ee26f39f2c1a707fe4b8420b4e1d890963b4a61 Mon Sep 17 00:00:00 2001 From: Pedro Ivo Date: Thu, 30 Jul 2026 14:04:53 -0300 Subject: [PATCH 2/2] fix(startup): support compact logo layout --- src/components/StartupScreen.test.ts | 41 +++++++- src/components/StartupScreen.ts | 143 +++++++++++++++++---------- 2 files changed, 132 insertions(+), 52 deletions(-) diff --git a/src/components/StartupScreen.test.ts b/src/components/StartupScreen.test.ts index 880d7e54f6..10ca96b1d1 100644 --- a/src/components/StartupScreen.test.ts +++ b/src/components/StartupScreen.test.ts @@ -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, @@ -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) + }) +}) diff --git a/src/components/StartupScreen.ts b/src/components/StartupScreen.ts index 7095b39135..6a84d16158 100644 --- a/src/components/StartupScreen.ts +++ b/src/components/StartupScreen.ts @@ -101,6 +101,8 @@ const VERBOO_LOGO_MASK = [ `110 01110 011`, ] +const STARTUP_LOGO_MIN_COLUMNS = 80 + // ─── Provider detection ─────────────────────────────────────────────────────── function resolveVerbooStartupModel(modelOverride?: string): string { @@ -238,21 +240,20 @@ 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 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 +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, + 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` @@ -260,52 +261,92 @@ export function printStartupScreen(modelOverride?: string): void { 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 // padding base entre logo e texto + const LOGO_TEXT_PADDING = 2 out.push('') - // ── Info lines (lado direito) ── - const rightCol = [ - ``, - `${bold}${SOFT}Verboo Code${RESET} ${DIMP}v${version}${RESET}`, - `${DIMP}Tokens ilimitados \u00b7 Privacidade \u00b7 Velocidade${RESET}`, - `${DIMP}${p.name} \u00b7 ${p.model}${RESET}`, - `${DIMP}${ep}${RESET}`, - `${DIMP}${displayCwd}${RESET}`, - ``, - ] - // ── Pinta o logo e calcula largura visual ── - const paintedLogo: { text: string; visualWidth: number }[] = [] - let maxLogoWidth = 0 - 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 m = mask[j] - const isBlock = ch === '\u2580' || ch === '\u2584' - if (isBlock && m === '1') { - painted += `${PURPLE_FILL}${ch}${RESET}` - } else if (isBlock) { - painted += `${PURPLE}${ch}${RESET}` - } else { - painted += ch + + 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 }) } - paintedLogo.push({ text: painted, visualWidth: line.length }) - if (line.length > maxLogoWidth) maxLogoWidth = line.length - } - // ── Saída lado a lado ── - for (let i = 0; i < paintedLogo.length; i++) { - const { text, visualWidth } = paintedLogo[i] - const gap = ' '.repeat(maxLogoWidth - visualWidth + LOGO_TEXT_PADDING) - out.push(` ${text}${gap}${rightCol[i]}`) + 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)) }