@@ -26,6 +26,7 @@ import { getUserStartupWarnings } from './utils/userStartupWarnings.js';
2626import { runNonInteractive } from './nonInteractiveCli.js' ;
2727import { loadExtensions , Extension } from './config/extension.js' ;
2828import { cleanupCheckpoints , registerCleanup , runExitCleanup } from './utils/cleanup.js' ;
29+ import { getIsQuitting } from './utils/quitState.js' ;
2930import { getCliVersion } from './utils/version.js' ;
3031import { checkForUpdates , executeUpdateCommand } from './ui/utils/updateCheck.js' ;
3132import {
@@ -211,6 +212,56 @@ async function askUserForUpdate(): Promise<boolean> {
211212 } ) ;
212213}
213214
215+ // -------------------------------------------------------------------------
216+ // Startup Animation Logic
217+ // -------------------------------------------------------------------------
218+ let startupTimer : NodeJS . Timeout | null = null ;
219+
220+ function startStartupAnimation ( ) {
221+ if ( ! process . stdout . isTTY || process . env . CI || process . env . DEEPV_SILENT_MODE === 'true' || process . env . NO_COLOR ) {
222+ return ;
223+ }
224+
225+ let count = 0 ;
226+ const maxChars = 20 ; // Maximum number of '=' chars
227+
228+ // Hide cursor to prevent flickering
229+ process . stdout . write ( '\x1b[?25l' ) ;
230+
231+ // Print the static message first with a newline
232+ process . stdout . write ( 'DeepV Code is starting...\n' ) ;
233+
234+ const renderFrame = ( ) => {
235+ // \x1b[2K: Clear entire line
236+ // \r: Return to start of line
237+ // Only animate the second line
238+ process . stdout . write ( `\x1b[2K\r${ '=' . repeat ( count ) } ` ) ;
239+ } ;
240+
241+ // Render first frame immediately
242+ renderFrame ( ) ;
243+
244+ startupTimer = setInterval ( ( ) => {
245+ count = ( count + 1 ) % ( maxChars + 1 ) ;
246+ renderFrame ( ) ;
247+ } , 100 ) ;
248+ }
249+
250+ function stopStartupAnimation ( ) {
251+ if ( startupTimer ) {
252+ clearInterval ( startupTimer ) ;
253+ startupTimer = null ;
254+
255+ if ( process . stdout . isTTY ) {
256+ // Clear the animation line (current line)
257+ // Then move up one line (\x1b[1A) and clear the text line
258+ // Finally restore cursor (\x1b[?25h)
259+ process . stdout . write ( '\x1b[2K\r\x1b[1A\x1b[2K\r\x1b[?25h' ) ;
260+ }
261+ }
262+ }
263+ // -------------------------------------------------------------------------
264+
214265/**
215266 * Check if checkpoint history size exceeds 2GB and prompt for cleanup
216267 */
@@ -309,6 +360,9 @@ export async function main() {
309360 // Clear screen at startup for clean interface
310361 console . clear ( ) ;
311362
363+ // Start simple loading animation
364+ startStartupAnimation ( ) ;
365+
312366 setupUnhandledRejectionHandler ( ) ;
313367
314368 // Setup Git error monitoring early to catch initialization errors
@@ -364,6 +418,7 @@ export async function main() {
364418
365419 // Handle --update flag
366420 if ( argv . update ) {
421+ stopStartupAnimation ( ) ;
367422 console . log ( t ( 'update.force.checking' ) ) ;
368423 const updateMessage = await checkForUpdates ( true , true ) ;
369424
@@ -500,6 +555,7 @@ export async function main() {
500555
501556 // Early check for list-sessions to avoid unnecessary session management
502557 if ( argv . listSessions ) {
558+ stopStartupAnimation ( ) ;
503559 const tempConfig = await loadCliConfig (
504560 settings . merged ,
505561 extensions ,
@@ -512,6 +568,7 @@ export async function main() {
512568
513569 // Handle --export-session flag
514570 if ( argv . exportSession ) {
571+ stopStartupAnimation ( ) ;
515572 try {
516573 const sessionId = argv . exportSession ;
517574 console . log ( tp ( 'export.exporting' , { sessionId } ) ) ;
@@ -526,6 +583,7 @@ export async function main() {
526583
527584 // Handle --test-audio flag
528585 if ( argv . testAudio ) {
586+ stopStartupAnimation ( ) ;
529587 console . log ( '🎵 Testing audio notifications...' ) ;
530588 console . log ( 'This will test all three notification sounds with a 1-second delay between each.' ) ;
531589 console . log ( 'Make sure your speakers/headphones are on and volume is audible.\n' ) ;
@@ -550,23 +608,43 @@ export async function main() {
550608 const sessionManager = new SessionManager ( workspaceRoot ) ;
551609
552610 // 添加进程信号处理器,确保在意外退出时也能清理空会话
611+ let isExiting = false ;
612+
553613 const handleExit = async ( ) => {
614+ // 1. 如果已经在退出中,不重复执行
615+ if ( isExiting ) return ;
616+ isExiting = true ;
617+
618+ // 🎯 macOS 优化:如果正在通过 /quit 优雅退出,快速 Ctrl+C 直接 exit 不执行 JS 清理
619+ // 这避免了多个信号处理器同时执行导致的内存积累和 OOM
620+ if ( getIsQuitting ( ) ) {
621+ process . exit ( 0 ) ;
622+ return ;
623+ }
624+
625+ // 2. 立即移除所有现有的 SIGINT/SIGTERM 监听器(关键:防止第三方库如Ink或用户继续按Ctrl+C导致的重入)
626+ process . removeAllListeners ( 'SIGINT' ) ;
627+ process . removeAllListeners ( 'SIGTERM' ) ;
628+
629+ // 3. 注册“自杀”监听器:如果用户再次按 Ctrl+C,直接强制退出,不执行任何 JS 逻辑
630+ const forceExit = ( ) => process . exit ( 1 ) ;
631+ process . on ( 'SIGINT' , forceExit ) ;
632+ process . on ( 'SIGTERM' , forceExit ) ;
633+
634+ // 4. 设置安全网:5秒后如果还没退完,强制退出(防止清理逻辑死锁或 OOM)
635+ setTimeout ( forceExit , 5000 ) . unref ( ) ;
636+
554637 try {
555638 await runExitCleanup ( ) ;
639+ process . exit ( 0 ) ;
556640 } catch ( error ) {
557- // 忽略清理错误,避免影响正常退出
641+ // 忽略清理错误,避免影响退出
642+ process . exit ( 1 ) ;
558643 }
559644 } ;
560645
561- process . on ( 'SIGINT' , async ( ) => {
562- await handleExit ( ) ;
563- process . exit ( 0 ) ;
564- } ) ;
565-
566- process . on ( 'SIGTERM' , async ( ) => {
567- await handleExit ( ) ;
568- process . exit ( 0 ) ;
569- } ) ;
646+ process . on ( 'SIGINT' , handleExit ) ;
647+ process . on ( 'SIGTERM' , handleExit ) ;
570648
571649 // Perform session cleanup based on settings (runs in background)
572650 const sessionCleanupConfig = settings . merged . sessionCleanup || {
@@ -649,6 +727,7 @@ export async function main() {
649727 }
650728
651729 if ( config . getListExtensions ( ) ) {
730+ stopStartupAnimation ( ) ;
652731 logIfNotSilent ( 'log' , 'Installed extensions:' ) ;
653732 for ( const extension of extensions ) {
654733 logIfNotSilent ( 'log' , `- ${ extension . config . name } ` ) ;
@@ -780,6 +859,7 @@ export async function main() {
780859
781860 // Check for cloud mode
782861 if ( argv . cloudMode ) {
862+ stopStartupAnimation ( ) ;
783863 const { startCloudMode } = await import ( './remote/remoteServer.js' ) ;
784864 const { maskServerUrl } = await import ( './utils/urlMask.js' ) ;
785865 const cloudServerUrl = argv . cloudServer || 'https://api-code.deepvlab.ai' ;
@@ -806,6 +886,8 @@ export async function main() {
806886
807887 // Render UI, passing necessary config values. Check that there is no command line question.
808888 if ( shouldBeInteractive ) {
889+ stopStartupAnimation ( ) ;
890+
809891 // 检查历史记录大小并提示清理
810892 await checkAndPromptHistoryCleanup ( settings ) ;
811893
@@ -818,6 +900,9 @@ export async function main() {
818900 // 🚀 启动优化:给事件循环一个喘息机会,确保之前的初始化任务(如 Git, 进程检测)不会挤占首屏渲染
819901 await new Promise ( resolve => setImmediate ( resolve ) ) ;
820902
903+ // Clear screen again before rendering Welcome UI to ensure cleanliness
904+ console . clear ( ) ;
905+
821906 const instance = render (
822907 < React . StrictMode >
823908 < AppWrapper
@@ -832,7 +917,12 @@ export async function main() {
832917 { exitOnCtrlC : false } ,
833918 ) ;
834919
835- registerCleanup ( ( ) => instance . unmount ( ) ) ;
920+ registerCleanup ( async ( ) => {
921+ instance . unmount ( ) ;
922+ // 等待 Ink/Yoga 清理资源,防止 WASM 内存访问错误
923+ // Wait for Ink/React to finish unmounting and Yoga to release resources
924+ await new Promise ( ( resolve ) => setTimeout ( resolve , 200 ) ) ;
925+ } ) ;
836926
837927 // 注册会话清理函数,在程序退出时清理空会话
838928 registerCleanup ( async ( ) => {
@@ -882,6 +972,7 @@ export async function main() {
882972 } ) ;
883973
884974 // Non-interactive mode handled by runNonInteractive
975+ stopStartupAnimation ( ) ; // Ensure stopped before non-interactive mode
885976 const nonInteractiveConfig = await loadNonInteractiveConfig (
886977 config ,
887978 extensions ,
0 commit comments