@@ -17,6 +17,7 @@ import {getUpdateNotice, onUpdateNotice} from '../../utils/ui/updateNotice.js';
1717import { useTheme } from '../contexts/ThemeContext.js' ;
1818import UpdateNotice from '../components/common/UpdateNotice.js' ;
1919import { useTerminalTitle } from '../../hooks/ui/useTerminalTitle.js' ;
20+ import { runUpdateAndExit } from '../../utils/core/runUpdate.js' ;
2021
2122// Lazy load all configuration screens for better startup performance
2223const ConfigScreen = React . lazy ( ( ) => import ( './ConfigScreen.js' ) ) ;
@@ -46,6 +47,16 @@ const ThemeSettingsScreen = React.lazy(
4647const HooksConfigScreen = React . lazy ( ( ) => import ( './HooksConfigScreen.js' ) ) ;
4748const MCPConfigScreen = React . lazy ( ( ) => import ( './MCPConfigScreen.js' ) ) ;
4849
50+ // 模块级标志:保证 SNOW CLI LOGO 的逐字符出现动画在整个进程生命周期内只播放一次。
51+ // 任何后续的重渲染(菜单切换返回、终端 resize 触发的 remount 等)都直接显示完整 LOGO,
52+ // 不会再次触发动画。
53+ let hasPlayedLogoRevealAnimation = false ;
54+ // LOGO 完整版可见字符总数(3 行 × 21 字符 = 63),用作 reveal 的上限。
55+ // 中等版(36)小于该值,所以同一个 totalChars 也能让中等版提前完成动画。
56+ const LOGO_REVEAL_MAX_CHARS = 63 ;
57+ // 每个字符出现的间隔时间(毫秒),决定动画的整体速度。
58+ const LOGO_REVEAL_INTERVAL_MS = 10 ;
59+
4960type Props = {
5061 version ?: string ;
5162 onMenuSelect ?: ( value : string ) => void ;
@@ -85,6 +96,32 @@ export default function WelcomeScreen({
8596 const { columns : terminalWidth } = useTerminalSize ( ) ;
8697 const { stdout} = useStdout ( ) ;
8798 const isInitialMount = useRef ( true ) ;
99+
100+ // LOGO 逐字符出现动画:
101+ // - revealChars === undefined 表示动画已结束(或本次进程之前已播放过),完整显示。
102+ // - 数字值表示当前可见的字符数,会从 0 递增到 LOGO_REVEAL_MAX_CHARS。
103+ // 使用模块级 hasPlayedLogoRevealAnimation 保证只在首次进入时播放一次。
104+ const [ logoRevealChars , setLogoRevealChars ] = useState < number | undefined > (
105+ ( ) => ( hasPlayedLogoRevealAnimation ? undefined : 0 ) ,
106+ ) ;
107+ useEffect ( ( ) => {
108+ if ( hasPlayedLogoRevealAnimation ) return ;
109+ const interval = setInterval ( ( ) => {
110+ setLogoRevealChars ( prev => {
111+ if ( prev === undefined ) return undefined ;
112+ const next = prev + 1 ;
113+ if ( next >= LOGO_REVEAL_MAX_CHARS ) {
114+ clearInterval ( interval ) ;
115+ hasPlayedLogoRevealAnimation = true ;
116+ // 切换为 undefined 让 ChatHeaderLogo 直接渲染完整字符串,
117+ // 后续重渲染不再走遮罩逻辑。
118+ return undefined ;
119+ }
120+ return next ;
121+ } ) ;
122+ } , LOGO_REVEAL_INTERVAL_MS ) ;
123+ return ( ) => clearInterval ( interval ) ;
124+ } , [ ] ) ;
88125 // 当终端宽度变化触发清屏时,先渲染为 null 一帧,把 ink/log-update 内部
89126 // "上一帧"缓存重置为空字符串;下一帧再切回 false 恢复完整内容,
90127 // 使新内容必然作为差异被完整写出,避免清屏后画面丢失。
@@ -113,6 +150,8 @@ export default function WelcomeScreen({
113150 return unsubscribe ;
114151 } , [ ] ) ;
115152
153+ const hasUpdate = ! ! updateNotice ;
154+
116155 const menuOptions = useMemo (
117156 ( ) => [
118157 {
@@ -182,14 +221,27 @@ export default function WelcomeScreen({
182221 value : 'theme' ,
183222 infoText : t . welcome . themeSettingsInfo ,
184223 } ,
224+ ...( hasUpdate
225+ ? [
226+ {
227+ label : `${ t . welcome . updateNow } ${
228+ updateNotice ? ` (v${ updateNotice . latestVersion } )` : ''
229+ } `,
230+ value : 'update-now' ,
231+ color : '#FFD700' ,
232+ infoText : t . welcome . updateNowInfo ,
233+ clearTerminal : true ,
234+ } ,
235+ ]
236+ : [ ] ) ,
185237 {
186238 label : t . welcome . exit ,
187239 value : 'exit' ,
188240 color : 'rgb(232, 131, 136)' ,
189241 infoText : t . welcome . exitInfo ,
190242 } ,
191243 ] ,
192- [ t ] ,
244+ [ t , hasUpdate , updateNotice ] ,
193245 ) ;
194246
195247 const [ remountKey , setRemountKey ] = useState ( 0 ) ;
@@ -250,6 +302,11 @@ export default function WelcomeScreen({
250302 setInlineView ( 'language-settings' ) ;
251303 } else if ( value === 'theme' ) {
252304 setInlineView ( 'theme-settings' ) ;
305+ } else if ( value === 'update-now' ) {
306+ // Hand the terminal over to npm: unmount Ink and exec the update.
307+ // runUpdateAndExit() does not return — the process exits when
308+ // the npm child finishes.
309+ runUpdateAndExit ( ) ;
253310 } else {
254311 // Pass through to parent for other actions (chat, exit, etc.)
255312 onMenuSelect ?.( value ) ;
@@ -335,6 +392,11 @@ export default function WelcomeScreen({
335392 // 否则 ChatHeaderLogo 在 hideCompact 模式下会返回 null,留下一个空的右半区——
336393 // 此时直接把整个圆角框让给 Menu 占满,不再做左右拆分。
337394 const showLogoPane = logoColumnWidth >= 20 ;
395+ // 当右侧 LOGO 走"完整最大版"分支(terminalWidth >= 30,对应这里 logoColumnWidth >= 30)
396+ // 且存在更新提示时:把更新提示从顶部移到右侧 LOGO 下方,LOGO 区改为顶端对齐让 LOGO 上移,
397+ // 这样在宽终端下能更紧凑地利用右半区的垂直空间。
398+ const isFullLogoPane = showLogoPane && logoColumnWidth >= 30 ;
399+ const showUpdateNoticeInLogoPane = isFullLogoPane && ! ! updateNotice ;
338400
339401 // 调整终端宽度后清屏的中间帧:渲染为 null,强制 log-update 把上一帧缓存
340402 // 重置为空字符串,下一帧的真实内容才能作为完整新内容被写出。
@@ -344,7 +406,7 @@ export default function WelcomeScreen({
344406
345407 return (
346408 < Box flexDirection = "column" width = { terminalWidth } key = { remountKey } >
347- { inlineView === 'menu' && updateNotice && (
409+ { inlineView === 'menu' && updateNotice && ! showUpdateNoticeInLogoPane && (
348410 < UpdateNotice
349411 currentVersion = { updateNotice . currentVersion }
350412 latestVersion = { updateNotice . latestVersion }
@@ -389,21 +451,34 @@ export default function WelcomeScreen({
389451 </ Box >
390452 < Box
391453 flexDirection = "column"
392- justifyContent = "center"
454+ justifyContent = {
455+ showUpdateNoticeInLogoPane ? 'flex-start' : 'center'
456+ }
393457 alignItems = "center"
394458 paddingX = { 2 }
459+ paddingY = { showUpdateNoticeInLogoPane ? 1 : 0 }
395460 flexGrow = { 1 }
396461 >
397462 < ChatHeaderLogo
398463 terminalWidth = { logoColumnWidth }
399464 logoGradient = { theme . colors . logoGradient }
400465 hideCompact
466+ revealChars = { logoRevealChars }
401467 />
402468 < Box marginTop = { 1 } >
403469 < Text color = "gray" dimColor >
404470 v{ version } • { t . welcome . subtitle }
405471 </ Text >
406472 </ Box >
473+ { showUpdateNoticeInLogoPane && updateNotice && (
474+ < Box marginTop = { 1 } >
475+ < UpdateNotice
476+ currentVersion = { updateNotice . currentVersion }
477+ latestVersion = { updateNotice . latestVersion }
478+ terminalWidth = { logoColumnWidth }
479+ />
480+ </ Box >
481+ ) }
407482 </ Box >
408483 </ >
409484 ) : (
0 commit comments