feat: feat(ui): minimal GSD welcome screen on startup (#1584)
* feat(ui): add GSD welcome screen on interactive startup Renders a two-panel boxed welcome screen to stderr before the TUI takes over, mirroring the style of the Claude Code welcome screen. Left panel — personalized greeting, GSD ASCII logo, active model + cwd Right panel — getting-started tips, recent session activity The screen is printed to stderr immediately before InteractiveMode.run(), so it appears on launch and reappears when the TUI exits (alternate-screen buffer swap). It silently skips when not a TTY or terminal < 60 cols. Files: src/welcome-screen.ts — printWelcomeScreen() implementation src/cli.ts — call site before interactiveMode.run() src/tests/welcome-screen.test.ts — 11 unit tests (all passing) * refactor(ui): minimal welcome screen — logo + metadata, no box Replace two-panel boxed layout with a minimal design: logo block with version/model/cwd alongside it, dim hint below. No box borders, no tips panel. Clean and fast. * feat(ui): show tool status line (Brave/Jina/Tavily) when keys are configured
This commit is contained in:
parent
f1a27b02b8
commit
1b6b16f2d5
3 changed files with 147 additions and 0 deletions
10
src/cli.ts
10
src/cli.ts
|
|
@ -577,6 +577,16 @@ if (enabledModelPatterns && enabledModelPatterns.length > 0) {
|
|||
}
|
||||
}
|
||||
|
||||
// Welcome screen — shown on every fresh interactive session before TUI takes over
|
||||
{
|
||||
const { printWelcomeScreen } = await import('./welcome-screen.js')
|
||||
printWelcomeScreen({
|
||||
version: process.env.GSD_VERSION || '0.0.0',
|
||||
modelName: settingsManager.getDefaultModel() || undefined,
|
||||
provider: settingsManager.getDefaultProvider() || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const interactiveMode = new InteractiveMode(session)
|
||||
markStartup('InteractiveMode')
|
||||
printStartupTimings()
|
||||
|
|
|
|||
73
src/tests/welcome-screen.test.ts
Normal file
73
src/tests/welcome-screen.test.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Welcome screen unit tests.
|
||||
*/
|
||||
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { printWelcomeScreen } from '../../dist/welcome-screen.js'
|
||||
|
||||
function capture(opts: Parameters<typeof printWelcomeScreen>[0]): string {
|
||||
const chunks: string[] = []
|
||||
const original = process.stderr.write.bind(process.stderr)
|
||||
;(process.stderr as any).write = (chunk: string) => { chunks.push(chunk); return true }
|
||||
const origIsTTY = (process.stderr as any).isTTY
|
||||
;(process.stderr as any).isTTY = true
|
||||
|
||||
try {
|
||||
printWelcomeScreen(opts)
|
||||
} finally {
|
||||
;(process.stderr as any).write = original
|
||||
;(process.stderr as any).isTTY = origIsTTY
|
||||
}
|
||||
|
||||
return chunks.join('')
|
||||
}
|
||||
|
||||
function strip(s: string): string {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
return s.replace(/\x1b\[[0-9;]*m/g, '')
|
||||
}
|
||||
|
||||
test('renders GSD logo', () => {
|
||||
const out = strip(capture({ version: '1.0.0' }))
|
||||
assert.ok(out.includes('██'), 'logo block characters missing')
|
||||
})
|
||||
|
||||
test('renders version', () => {
|
||||
const out = strip(capture({ version: '2.38.0' }))
|
||||
assert.ok(out.includes('v2.38.0'), 'version missing')
|
||||
assert.ok(out.includes('Get Shit Done'), 'brand name missing')
|
||||
})
|
||||
|
||||
test('renders model and provider', () => {
|
||||
const out = strip(capture({ version: '1.0.0', modelName: 'claude-opus-4-6', provider: 'Anthropic' }))
|
||||
assert.ok(out.includes('claude-opus-4-6'), 'model name missing')
|
||||
assert.ok(out.includes('Anthropic'), 'provider missing')
|
||||
})
|
||||
|
||||
test('renders cwd hint', () => {
|
||||
const out = strip(capture({ version: '1.0.0' }))
|
||||
assert.ok(out.includes('/gsd to begin'), 'hint line missing')
|
||||
})
|
||||
|
||||
test('skips when not a TTY', () => {
|
||||
const chunks: string[] = []
|
||||
const original = process.stderr.write.bind(process.stderr)
|
||||
;(process.stderr as any).write = (chunk: string) => { chunks.push(chunk); return true }
|
||||
const origIsTTY = (process.stderr as any).isTTY
|
||||
;(process.stderr as any).isTTY = false
|
||||
|
||||
try {
|
||||
printWelcomeScreen({ version: '1.0.0' })
|
||||
assert.equal(chunks.join(''), '', 'should produce no output when not TTY')
|
||||
} finally {
|
||||
;(process.stderr as any).write = original
|
||||
;(process.stderr as any).isTTY = origIsTTY
|
||||
}
|
||||
})
|
||||
|
||||
test('renders without model or provider', () => {
|
||||
const out = strip(capture({ version: '3.0.0' }))
|
||||
assert.ok(out.includes('v3.0.0'), 'version missing when no model provided')
|
||||
})
|
||||
64
src/welcome-screen.ts
Normal file
64
src/welcome-screen.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* GSD Welcome Screen
|
||||
*
|
||||
* Rendered to stderr before the TUI takes over.
|
||||
* No box, no panels — logo with metadata alongside, dim hint below.
|
||||
*/
|
||||
|
||||
import os from 'node:os'
|
||||
import chalk from 'chalk'
|
||||
import { GSD_LOGO } from './logo.js'
|
||||
|
||||
export interface WelcomeScreenOptions {
|
||||
version: string
|
||||
modelName?: string
|
||||
provider?: string
|
||||
}
|
||||
|
||||
function getShortCwd(): string {
|
||||
const cwd = process.cwd()
|
||||
const home = os.homedir()
|
||||
return cwd.startsWith(home) ? '~' + cwd.slice(home.length) : cwd
|
||||
}
|
||||
|
||||
export function printWelcomeScreen(opts: WelcomeScreenOptions): void {
|
||||
if (!process.stderr.isTTY) return
|
||||
|
||||
const { version, modelName, provider } = opts
|
||||
const shortCwd = getShortCwd()
|
||||
|
||||
// Info lines to sit alongside the logo (one per logo row)
|
||||
const modelLine = [modelName, provider].filter(Boolean).join(' · ')
|
||||
const INFO: (string | undefined)[] = [
|
||||
` ${chalk.bold('Get Shit Done')} ${chalk.dim('v' + version)}`,
|
||||
undefined,
|
||||
modelLine ? ` ${chalk.dim(modelLine)}` : undefined,
|
||||
` ${chalk.dim(shortCwd)}`,
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
|
||||
const lines: string[] = ['']
|
||||
for (let i = 0; i < GSD_LOGO.length; i++) {
|
||||
lines.push(chalk.cyan(GSD_LOGO[i]) + (INFO[i] ?? ''))
|
||||
}
|
||||
|
||||
// Tool status + hint — dim, aligned under the info text
|
||||
const pad = ' '.repeat(28) + ' ' // aligns with the info text column
|
||||
|
||||
const toolParts: string[] = []
|
||||
if (process.env.BRAVE_API_KEY) toolParts.push('Brave ✓')
|
||||
if (process.env.BRAVE_ANSWERS_KEY) toolParts.push('Answers ✓')
|
||||
if (process.env.JINA_API_KEY) toolParts.push('Jina ✓')
|
||||
if (process.env.TAVILY_API_KEY) toolParts.push('Tavily ✓')
|
||||
if (process.env.CONTEXT7_API_KEY) toolParts.push('Context7 ✓')
|
||||
|
||||
if (toolParts.length > 0) {
|
||||
lines.push(chalk.dim(pad + ['Web search loaded', ...toolParts].join(' · ')))
|
||||
}
|
||||
|
||||
lines.push(chalk.dim(pad + '/gsd to begin · /gsd help for all commands'))
|
||||
lines.push('')
|
||||
|
||||
process.stderr.write(lines.join('\n') + '\n')
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue