From 8d30860fd711d221ba0ec7ecac542d4b3b6d8c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=8B=E7=AB=9F?= <1048927295@qq.com> Date: Wed, 22 Apr 2026 11:07:58 +0800 Subject: [PATCH 1/5] fix(cli): remove residual blank lines after MCP init completes (#3095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConfigInitDisplay rendered plus a content line, so the live area grew by 2 rows during startup. When initialization finished and the component unmounted, Ink shrank the live area but the rows it had already committed to the terminal scrollback cannot be reclaimed, leaving a visible gap above the input. Move the MCP init status into the Footer's left-bottom status slot (always mounted, fixed height) so the live area height stays constant across the init → ready transition. The status participates in the existing priority chain: ctrlC / ctrlD / escape / vim / shell / autoAccept / configInit / hint. --- packages/cli/src/ui/components/Composer.tsx | 3 -- .../cli/src/ui/components/Footer.test.tsx | 1 + packages/cli/src/ui/components/Footer.tsx | 7 +++ .../useConfigInitMessage.ts} | 43 +++++++++++-------- 4 files changed, 34 insertions(+), 20 deletions(-) rename packages/cli/src/ui/{components/ConfigInitDisplay.tsx => hooks/useConfigInitMessage.ts} (52%) diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 263988686c3..c5bda6bea92 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -16,7 +16,6 @@ import { useUIActions } from '../contexts/UIActionsContext.js'; import { useVimMode } from '../contexts/VimModeContext.js'; import { useConfig } from '../contexts/ConfigContext.js'; import { StreamingState, type HistoryItemToolGroup } from '../types.js'; -import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js'; import { FeedbackDialog } from '../FeedbackDialog.js'; import { t } from '../../i18n/index.js'; @@ -104,8 +103,6 @@ export const Composer = () => { /> )} - {!uiState.isConfigInitialized && } - {uiState.isFeedbackDialogOpen && } diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx index 585f47ecec2..f635af0a5b2 100644 --- a/packages/cli/src/ui/components/Footer.test.tsx +++ b/packages/cli/src/ui/components/Footer.test.tsx @@ -78,6 +78,7 @@ const createMockUIState = (overrides: Partial = {}): UIState => contextFileNames: [], showToolDescriptions: false, ideContextState: undefined, + isConfigInitialized: true, ...overrides, }) as UIState; diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index abe7e456949..9a22070f50d 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -15,10 +15,12 @@ import { ShellModeIndicator } from './ShellModeIndicator.js'; import { isNarrowWidth } from '../utils/isNarrowWidth.js'; import { useStatusLine } from '../hooks/useStatusLine.js'; +import { useConfigInitMessage } from '../hooks/useConfigInitMessage.js'; import { useUIState } from '../contexts/UIStateContext.js'; import { useConfig } from '../contexts/ConfigContext.js'; import { useVimMode } from '../contexts/VimModeContext.js'; import { ApprovalMode } from '@qwen-code/qwen-code-core'; +import { GeminiSpinner } from './GeminiRespondingSpinner.js'; import { t } from '../../i18n/index.js'; /** @@ -52,6 +54,7 @@ export const Footer: React.FC = () => { const config = useConfig(); const { vimEnabled, vimMode } = useVimMode(); const { lines: statusLineLines } = useStatusLine(); + const configInitMessage = useConfigInitMessage(uiState.isConfigInitialized); const dreamRunning = useDreamRunning(config.getProjectRoot()); const { promptTokenCount, showAutoAcceptIndicator } = { @@ -96,6 +99,10 @@ export const Footer: React.FC = () => { ) : showAutoAcceptIndicator !== undefined && showAutoAcceptIndicator !== ApprovalMode.DEFAULT ? ( + ) : configInitMessage ? ( + + {configInitMessage} + ) : suppressHint ? null : ( {t('? for shortcuts')} ); diff --git a/packages/cli/src/ui/components/ConfigInitDisplay.tsx b/packages/cli/src/ui/hooks/useConfigInitMessage.ts similarity index 52% rename from packages/cli/src/ui/components/ConfigInitDisplay.tsx rename to packages/cli/src/ui/hooks/useConfigInitMessage.ts index 264eeeafa47..8dc102869ed 100644 --- a/packages/cli/src/ui/components/ConfigInitDisplay.tsx +++ b/packages/cli/src/ui/hooks/useConfigInitMessage.ts @@ -5,19 +5,34 @@ */ import { useEffect, useState } from 'react'; -import { appEvents } from './../../utils/events.js'; -import { Box, Text } from 'ink'; -import { useConfig } from '../contexts/ConfigContext.js'; +import { appEvents } from '../../utils/events.js'; import { type McpClient, MCPServerStatus } from '@qwen-code/qwen-code-core'; -import { GeminiSpinner } from './GeminiRespondingSpinner.js'; -import { theme } from '../semantic-colors.js'; import { t } from '../../i18n/index.js'; -export const ConfigInitDisplay = () => { - const config = useConfig(); - const [message, setMessage] = useState(t('Initializing...')); +/** + * Returns a human-readable initialization status message while config is + * being initialized (MCP servers connecting, etc.). Returns `null` once + * initialization is complete so the caller can fall through to its + * default content. + * + * Rendered inline (e.g. in the Footer's left-bottom status slot) instead + * of as a standalone component, so the live area's height stays constant + * across the init → ready transition and no residual blank rows remain + * in the terminal scrollback. + */ +export function useConfigInitMessage( + isConfigInitialized: boolean, +): string | null { + const [message, setMessage] = useState( + isConfigInitialized ? null : t('Initializing...'), + ); useEffect(() => { + if (isConfigInitialized) { + setMessage(null); + return; + } + const onChange = (clients?: Map) => { if (!clients || clients.size === 0) { setMessage(t('Initializing...')); @@ -41,13 +56,7 @@ export const ConfigInitDisplay = () => { return () => { appEvents.off('mcp-client-update', onChange); }; - }, [config]); + }, [isConfigInitialized]); - return ( - - - {message} - - - ); -}; + return message; +} From ba6cdfabf6ef1e62a8ce96eb7376a6572b06a5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=8B=E7=AB=9F?= <1048927295@qq.com> Date: Wed, 22 Apr 2026 11:25:49 +0800 Subject: [PATCH 2/5] fix(cli): suppress MCP init message when custom status line is active MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit follow-up. Previously the configInit branch preceded the suppressHint branch in the footer's left-bottom priority chain. With a custom status line configured, {null} collapses to zero rows in Ink, so the footer's bottom row went from 1 row during init to 0 rows after — a 1-row height oscillation that reintroduces the same scrollback-residue symptom the original fix eliminated in the default case. Swap the order so suppressHint short-circuits to null first: the init message now shares the hint's suppression rule, keeping the footer's height constant in every configuration. Also: - Gate the hook's return on isConfigInitialized directly instead of letting the effect clear state, avoiding a one-frame flash where the stale "Initializing..." message leaks through on the first render after init completes. - Cover the new behavior with three Footer tests, including a regression test for the custom-status-line case. --- .../cli/src/ui/components/Footer.test.tsx | 36 +++++++++++++++++++ packages/cli/src/ui/components/Footer.tsx | 8 +++-- .../cli/src/ui/hooks/useConfigInitMessage.ts | 24 +++++-------- 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx index f635af0a5b2..dfa28302a3c 100644 --- a/packages/cli/src/ui/components/Footer.test.tsx +++ b/packages/cli/src/ui/components/Footer.test.tsx @@ -150,6 +150,42 @@ describe('