diff --git a/docs/design/performance/fire-and-forget-startup-prefetch.md b/docs/design/performance/fire-and-forget-startup-prefetch.md
new file mode 100644
index 00000000000..496352af48e
--- /dev/null
+++ b/docs/design/performance/fire-and-forget-startup-prefetch.md
@@ -0,0 +1,369 @@
+# Fire-and-Forget Startup Prefetch Optimization Design
+
+## Background and Goals
+
+The parent issue #3011 breaks down qwen-code startup optimization into multiple subtasks. The current repository has already landed several foundational capabilities:
+
+- #3219: Startup performance profiler is integrated, supporting `QWEN_CODE_PROFILE_STARTUP=1` to output startup phase JSON.
+- #3221: Tool registration has been converted to lazy factory; `Config.initialize()` no longer statically instantiates all tools.
+- #3223: API preconnect already exists, currently triggered in a fire-and-forget fashion after `loadCliConfig()`.
+- Early input capture, progressive MCP discovery, and `config.initialize()` after AppContainer render are also partially implemented.
+
+The goal of #3222 is not to redo these capabilities, but to consolidate the non-critical startup operations still scattered across the startup path into a unified fire-and-forget prefetch layer: before first paint, only await operations that truly affect correctness; after first paint, launch background tasks that do not affect the correctness of the first interaction, while preserving compatible semantics for non-interactive modes.
+
+## Current Startup Flow
+
+The key flow of the current interactive startup path is as follows:
+
+```mermaid
+flowchart TD
+ A[packages/cli/index.ts] --> B[initStartupProfiler]
+ B --> C[gemini.main]
+ C --> D[parseArguments]
+ D --> E[loadSettings]
+ E --> F[sandbox / worktree / relaunch checks]
+ F --> G[loadCliConfig]
+ G --> H[register cleanup + preconnectApi fire-and-forget]
+ H --> I[early input capture + kitty/theme probes]
+ I --> J[initializeApp awaited]
+ J --> K{interactive?}
+ K -->|yes| L[startInteractiveUI]
+ L --> M[Ink render returns / first_paint]
+ M --> N[checkForUpdates fire-and-forget]
+ M --> O[AppContainer useEffect]
+ O --> P[config.initialize awaited after render]
+ P --> Q[MCP discovery background]
+ P --> R[input_enabled]
+ K -->|no| S[config.initialize]
+ S --> T[waitForMcpReady]
+ T --> U[runNonInteractive]
+```
+
+Current state assessment:
+
+- `initializeApp()` still executes i18n, auth, theme validation, and IDE client connection serially before first paint.
+- Auth and i18n must remain before first paint; IDE connection is not a hard dependency for the first paint of a plain TUI without an initial prompt, and can be deferred on the plain TUI path. However, for paths like `qwen -i "prompt"`, `qwen -p`, stream-json, and ACP/Zed — which have no safe post-render window or whose first request needs IDE context/status — IDE connection must continue to be awaited before the first request.
+- `checkForUpdates()` is already fire-and-forget after render in `startInteractiveUI()`, but the logic is scattered within the UI startup function.
+- `preconnectApi()` is already fire-and-forget and should be kept triggering as early as possible, but brought under unified scheduling.
+- Telemetry SDK init previously occurred synchronously during `Config` construction; for plain interactive TUI it can be deferred to after render, while non-interactive paths retain the pre-first-request initialization semantics.
+- On the interactive path, `config.initialize()` already executes after React mount; MCP discovery already runs in the background inside core, with AppContainer batch-refreshing the tool list.
+- The non-interactive path still needs to await `config.waitForMcpReady()`, otherwise the first prompt might not see MCP tools, causing scripted behavior to regress.
+
+## Target Architecture
+
+Introduce a small startup prefetch scheduling layer that uniformly manages "start but don't await" tasks, split into two categories by trigger timing: early and post-render.
+
+```mermaid
+flowchart LR
+ subgraph CLI[CLI startup]
+ G[loadCliConfig] --> EP[startEarlyStartupPrefetches]
+ EP --> PC[API preconnect]
+ G --> IA[initializeAppCritical]
+ G --> HI[initializeAppWithAwaitedIde for headless / stream-json / ACP]
+ IA --> UI[startInteractiveUI]
+ end
+
+ subgraph Prefetch[StartupPrefetchController]
+ SP[startPostRenderPrefetches]
+ SP --> UP[update check]
+ SP --> IDE[IDE client connect only for ordinary TUI]
+ SP --> OTEL[telemetry SDK init for interactive TUI]
+ SP --> HK[background housekeeping import]
+ SP --> PROF[profile async task events]
+ end
+
+ subgraph UI[Interactive UI]
+ UI --> FP[Ink render / first_paint]
+ FP --> SP
+ FP --> AC[AppContainer]
+ AC --> CI[config.initialize]
+ CI --> MCP[MCP discovery background]
+ MCP --> BT[batched setTools]
+ end
+
+ subgraph Headless[Non-interactive]
+ CI2[config.initialize] --> WM[waitForMcpReady]
+ WM --> RUN[runNonInteractive]
+ end
+```
+
+The interactive startup sequence under the new design:
+
+```mermaid
+sequenceDiagram
+ participant Main as gemini.main()
+ participant Prefetch as StartupPrefetchController
+ participant UI as startInteractiveUI()
+ participant App as AppContainer
+ participant MCP as McpClientManager
+
+ Main->>Main: parseArguments + loadSettings
+ Main->>Main: loadCliConfig
+ Main->>Prefetch: startEarlyStartupPrefetches(config)
+ Prefetch-->>Prefetch: void preconnectApi()
+ Main->>Main: await initializeAppCritical(deferIdeConnection=true for ordinary TUI without initial prompt)
+ Main->>UI: startInteractiveUI(...)
+ UI->>UI: render()
+ UI->>Prefetch: startPostRenderPrefetches(config, settings, options)
+ Prefetch-->>Prefetch: void checkForUpdates()
+ Prefetch-->>Prefetch: void connectIdeClient() for ordinary TUI only
+ Prefetch-->>Prefetch: void initializeTelemetry() for interactive TUI
+ App->>App: await config.initialize()
+ App->>MCP: start background discovery
+ App->>App: input_enabled
+ MCP-->>App: mcp-client-update batches
+ App-->>MCP: geminiClient.setTools()
+```
+
+## Design Changes
+
+### 1. New Unified Startup Prefetch Scheduler
+
+Add `packages/cli/src/startup/startup-prefetch.ts`, providing two entry points:
+
+```ts
+startEarlyStartupPrefetches(config: Config): void;
+startPostRenderPrefetches(
+ config: Config,
+ settings: LoadedSettings,
+ options?: { connectIde?: boolean; initializeTelemetry?: boolean },
+): void;
+```
+
+The scheduler does exactly three things:
+
+- Launches prefetch tasks by name.
+- Uses `void task().catch(...)` to explicitly not await and not throw.
+- Records debug logs and profiler async events to verify whether tasks are launched before or after render.
+
+The scheduler must guarantee idempotency per phase, preventing React StrictMode, repeated test calls, or anomalous re-entries from launching the same task multiple times.
+
+### 2. Early Prefetch: Maximize Head Start
+
+`startEarlyStartupPrefetches(config)` is called immediately after `loadCliConfig()` succeeds.
+
+The first phase includes only API preconnect:
+
+- Reads the current auth type and resolved base URL from `config.getModelsConfig()`.
+- Reads proxy from `config.getProxy()`.
+- Calls the existing `preconnectApi(authType, { resolvedBaseUrl, proxy })`.
+- Preserves existing environment gates: `QWEN_CODE_DISABLE_PRECONNECT`, sandbox, custom CA, non-Node runtime, no proxy, etc.
+
+This adds no new configuration options. Preconnect failures only write debug logs and do not affect startup.
+
+### 3. Post-Render Prefetch: Launch After First Paint
+
+`startPostRenderPrefetches(config, settings)` is called in `startInteractiveUI()` after Ink `render()` returns and `first_paint` is recorded.
+
+First batch includes:
+
+- Update check: migrate the existing `checkForUpdates().then(handleAutoUpdate)` logic, preserving the `settings.merged.general?.enableAutoUpdate !== false` gate.
+- IDE client connection: moved to post-render prefetch only on the plain interactive TUI path without an initial prompt. Callers must explicitly pass `connectIde: true`, and the scheduler internally still checks `config.getIdeMode()`. `qwen -i "prompt"`, non-interactive, stream-json, and ACP/Zed do not defer IDE connection through this entry point.
+- Telemetry SDK init: moved to post-render prefetch only on the interactive TUI path. `Config` still retains telemetry settings, but skips the construction-time SDK side effect via `deferTelemetryInitialization`; post-render prefetch launches the SDK via `initializeTelemetry(config)`. Non-interactive, stream-json, and ACP/Zed do not defer.
+- Background housekeeping: can be migrated from `gemini.tsx` to post-render prefetch, giving all background startup tasks a unified entry point; still limited to interactive, still uses dynamic import and error swallowing.
+
+None of these tasks may affect the return value of `startInteractiveUI()`, nor may they write user-visible errors to the TUI stderr. Failures only go to debug logs.
+
+### 4. Split `initializeApp()` Critical Path, Preserve Non-TUI Awaited IDE Connection
+
+Add a shared helper to avoid duplicating IDE connection logic between the TUI deferred path and the non-TUI awaited path:
+
+```ts
+export async function connectIdeForStartup(config: Config): Promise {
+ if (!config.getIdeMode()) return;
+
+ const ideClient = await IdeClient.getInstance();
+ await ideClient.connect();
+ logIdeConnection(config, new IdeConnectionEvent(IdeConnectionType.START));
+}
+```
+
+`initializeApp()` remains as pre-first-paint critical initialization, but gains an explicit option:
+
+```ts
+interface InitializeAppOptions {
+ deferIdeConnection?: boolean;
+}
+```
+
+The default must remain backward-compatible: `deferIdeConnection` defaults to `false`. That is, when no option is passed, IDE connection is still awaited within `initializeApp()`.
+
+The awaited content of `initializeApp()` becomes:
+
+- `initializeI18n(...)`
+- `performInitialAuth(...)`
+- `validateTheme(settings)`
+- When `deferIdeConnection !== true`, `await connectIdeForStartup(config)`
+- Compute `shouldOpenAuthDialog`
+- Read `config.getGeminiMdFileCount()`
+
+The call site in `gemini.tsx` is responsible for selecting based on the run mode:
+
+```ts
+const deferIdeConnection =
+ config.isInteractive() && !config.getExperimentalZedIntegration() && !input;
+
+const initializationResult = await initializeApp(config, settings, {
+ deferIdeConnection,
+});
+```
+
+Subsequently, only when `deferIdeConnection === true`, `startInteractiveUI()` fires-and-forgets the IDE connection via `startPostRenderPrefetches(..., { connectIde: true })`; prompt-interactive, which auto-submits the first question, continues to await IDE before render and passes `connectIde: false` to avoid post-render duplicate connection.
+
+This split addresses the compatibility risk flagged in review:
+
+- Plain interactive TUI: IDE socket/IPC connection no longer blocks first paint.
+- `qwen -i "prompt"`: continues to await IDE connection before the first auto-submitted request, and post-render does not reconnect.
+- `qwen -p` / piped stdin: continues to await IDE connection before the first model request.
+- stream-json: continues to complete IDE connection before session/control request handling.
+- ACP/Zed: continues to retain awaited IDE startup, avoiding missing IDE context/status on the first request.
+
+### 5. MCP and Non-Interactive Semantics Remain Unchanged
+
+This design does not change the core MCP state machine.
+
+Interactive:
+
+- Continues to call `config.initialize()` in the mount effect of `AppContainer`.
+- `Config.initialize()` continues to launch background MCP discovery.
+- AppContainer continues to listen for `mcp-client-update` and batch-call `geminiClient.setTools()` at ~16ms intervals.
+- First paint and input availability do not wait for MCP to fully settle.
+
+Non-interactive / stream-json / ACP:
+
+- Continues to await IDE connection before the first model request.
+- Continues to await `config.waitForMcpReady()` before the first model request.
+- Preserves the tool visibility semantics of the old synchronous path.
+- Preserves the existing behavior of stderr warnings on MCP failure.
+
+## Estimated Performance Gains
+
+Gains fall into two categories.
+
+The first is shortened critical path before first paint:
+
+- IDE client connection for plain interactive TUI no longer blocks first paint; gains depend on IDE socket/IPC connection time, expected to be tens to hundreds of milliseconds.
+- Telemetry SDK init for plain interactive TUI no longer blocks first paint; gains depend on OTel SDK/exporter construction cost, typically a small to moderate synchronous startup overhead.
+- Update check, housekeeping, preconnect, and similar tasks have a unified fire-and-forget entry point, preventing future maintenance from accidentally placing them back on the awaited path.
+
+The second is first API request gains:
+
+- Continues to preserve the #3223 API preconnect design.
+- When proxy/shared dispatcher is reusable, the first API request can avoid TCP+TLS handshake costs, expected 100-200ms.
+
+Note: #3219's historical baseline showed module loading once accounted for ~94% of total startup time; #3221's lazy tool registration has already addressed the largest bottleneck. The core benefit of #3222 is more about perceived TTI and first-paint responsiveness, rather than eliminating all module loading costs.
+
+## Risks and Scope of Impact
+
+### Risks
+
+- IDE capabilities on plain TUI may shift from "connected before first paint" to "connected very shortly after first paint". Mitigation: only defer on the plain interactive TUI path; non-interactive, stream-json, and ACP/Zed maintain awaited connection before the first request.
+- Pre-render telemetry events may be no-op dropped when the SDK is not yet initialized. Mitigation: only defer for interactive TUI; non-interactive pre-first-request telemetry retains its original semantics, no new buffering queue added.
+- Deferred task failures may not be prominent. Mitigation: unified wrapper records debug logs and profiler async events.
+- Migrating update/preconnect may inadvertently change existing gates. Mitigation: verbatim preservation of existing settings/env conditions.
+- Over-deferring may leave capabilities unready when the first user input depends on them. Mitigation: auth, config construction, permissions, hooks, memory, tool registry, and non-interactive MCP ready all remain awaited.
+
+### Scope of Impact
+
+Expected to only involve the CLI startup layer:
+
+- `packages/cli/src/startup/startup-prefetch.ts`
+- `packages/cli/src/core/initializer.ts`
+- `packages/cli/src/gemini.tsx`
+- `packages/cli/src/ui/startInteractiveUI.tsx`
+- Corresponding unit tests
+
+No changes to:
+
+- CLI arguments and configuration schema
+- Core tool registry protocol
+- MCP discovery state machine
+- Model request protocol
+- User-visible command behavior
+
+## Unit Test Plan
+
+### `packages/cli/src/startup/startup-prefetch.test.ts`
+
+Coverage:
+
+- `startEarlyStartupPrefetches()` calls `preconnectApi()` with auth type, resolved base URL, and proxy.
+- Early prefetch does not await task completion.
+- Repeated calls are idempotent, not launching the same early task again.
+- `startPostRenderPrefetches()` launches update check when `enableAutoUpdate !== false`.
+- Does not launch update check when `enableAutoUpdate === false`.
+- Launches IDE connect and calls `logIdeConnection()` when `options.connectIde === true` and `config.getIdeMode() === true`.
+- Does not trigger IDE connect when `options.connectIde !== true`.
+- Does not trigger IDE connect when `config.getIdeMode() === false` even if `options.connectIde === true`.
+- Launches telemetry SDK init when `options.initializeTelemetry === true`.
+- Does not trigger telemetry SDK init when `options.initializeTelemetry !== true`.
+- Deferred task rejections do not cause the public API to throw, only write debug logs.
+
+### `packages/cli/src/core/initializer.test.ts`
+
+Adjustments and additions:
+
+- `initializeApp()` by default awaits `connectIdeForStartup()`, preserving non-TUI path compatibility.
+- `initializeApp(..., { deferIdeConnection: true })` does not call `IdeClient.getInstance()` or `connect()`.
+- `initializeApp(..., { deferIdeConnection: false })` calls and awaits IDE connect when `config.getIdeMode() === true`.
+- Still awaits `initializeI18n()`.
+- Still awaits `performInitialAuth()`.
+- On auth failure, retains `authError` and `shouldOpenAuthDialog === true`.
+- On theme validation failure, retains `themeError`.
+- When auth type is explicitly provided and auth succeeds, `shouldOpenAuthDialog === false`.
+
+### `packages/cli/src/ui/startInteractiveUI.test.tsx`
+
+Coverage:
+
+- After Ink `render()` returns and `first_paint` is recorded, calls `startPostRenderPrefetches(config, settings)`.
+- Plain TUI path passes `{ connectIde: true, initializeTelemetry: true }`.
+- When prompt-interactive has already awaited IDE before render, passes `{ connectIde: false, initializeTelemetry: true }` to avoid duplicate IDE connect.
+- Non-TUI paths do not trigger IDE/telemetry post-render prefetch through `startInteractiveUI()`.
+- Post-render prefetch rejections do not cause `startInteractiveUI()` to reject.
+- After update check is moved out of `startInteractiveUI()` inline logic, it is no longer directly called.
+
+### `packages/cli/src/gemini.test.tsx`
+
+Adjustments and additions:
+
+- Plain interactive TUI calls `initializeApp(config, settings, { deferIdeConnection: true })`, and connects IDE in post-render prefetch.
+- Prompt-interactive calls `initializeApp(config, settings, { deferIdeConnection: false })`, and post-render prefetch does not reconnect IDE.
+- `qwen -p` / piped stdin / stream-json calls `initializeApp(config, settings, { deferIdeConnection: false })` or uses defaults, ensuring IDE is connected before the first request.
+- ACP/Zed path does not enable IDE deferred prefetch, continues through awaited IDE startup.
+
+### `packages/core/src/config/config.test.ts`
+
+Coverage:
+
+- When telemetry is enabled and `deferTelemetryInitialization` is not passed, `Config` construction still calls `initializeTelemetry(config)`.
+- When telemetry is enabled and `deferTelemetryInitialization === true`, `Config` construction does not call `initializeTelemetry(config)`, but `config.getTelemetryEnabled()` still returns true.
+
+### Regression Tests
+
+Recommended execution:
+
+```bash
+cd packages/cli && npx vitest run src/core/initializer.test.ts src/startup/startup-prefetch.test.ts
+cd packages/cli && npx vitest run src/gemini.test.tsx
+cd packages/core && npx vitest run src/config/config.test.ts -t "telemetry"
+```
+
+## Acceptance Criteria
+
+- Interactive REPL first paint does not wait for IDE connection, telemetry init, update check, or housekeeping.
+- Non-interactive, stream-json, and ACP/Zed still await IDE connection before the first request.
+- Non-interactive, stream-json, and ACP/Zed do not defer telemetry SDK init.
+- API preconnect still fires-and-forgets as early as possible after `loadCliConfig()`.
+- Auth, config, permissions, hooks, memory, and other correctness-critical initializations remain awaited where needed.
+- Non-interactive first prompt still waits for MCP ready.
+- All deferred task failures do not affect REPL rendering.
+- Profiler shows deferred tasks launching as expected around first_paint.
+- Unit tests cover critical paths, idempotency, error swallowing, and non-interactive compatibility constraints.
+
+## Default Assumptions
+
+- #3221 is actually an issue on GitHub, not a PR; the current repository already contains the lazy tool registry implementation.
+- This design adds no new configuration options, avoiding turning startup optimization into user-configurable complexity.
+- "REPL renders before deferred operations complete" means Ink first-paint return and input availability, not requiring all background capabilities to finish before the user sees the UI.
+- Non-interactive mode prioritizes compatibility, not pursuing first-paint optimization as aggressively as interactive mode.
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts
index 226fa89e8fc..3933796be2d 100644
--- a/packages/cli/src/config/config.test.ts
+++ b/packages/cli/src/config/config.test.ts
@@ -31,6 +31,7 @@ const mockSessionServiceInstance = vi.hoisted(() => ({
const mockSessionServiceCtor = vi.hoisted(() =>
vi.fn(() => mockSessionServiceInstance),
);
+const mockConfigConstructorParams = vi.hoisted(() => vi.fn());
vi.mock('../utils/stdioHelpers.js', () => ({
writeStderrLine: mockWriteStderrLine,
@@ -160,8 +161,15 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
SkillManagerMock.prototype.listSkills = vi.fn().mockResolvedValue([]);
SkillManagerMock.prototype.addChangeListener = vi.fn();
SkillManagerMock.prototype.removeChangeListener = vi.fn();
+ class ConfigWithParamCapture extends actualServer.Config {
+ constructor(...args: ConstructorParameters) {
+ mockConfigConstructorParams(args[0]);
+ super(...args);
+ }
+ }
return {
...actualServer,
+ Config: ConfigWithParamCapture,
NativeLspService: vi
.fn()
.mockImplementation(() => createNativeLspServiceInstance()),
@@ -1541,6 +1549,7 @@ describe('loadCliConfig', () => {
describe('loadCliConfig telemetry', () => {
const originalArgv = process.argv;
+ const originalIsTTY = process.stdin.isTTY;
beforeEach(() => {
vi.resetAllMocks();
@@ -1550,6 +1559,7 @@ describe('loadCliConfig telemetry', () => {
afterEach(() => {
process.argv = originalArgv;
+ process.stdin.isTTY = originalIsTTY;
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
@@ -1570,6 +1580,41 @@ describe('loadCliConfig telemetry', () => {
expect(config.getTelemetryEnabled()).toBe(true);
});
+ it('should initialize telemetry before prompt-interactive startup', async () => {
+ process.argv = [
+ 'node',
+ 'script.js',
+ '-i',
+ 'hello from prompt-interactive',
+ '--telemetry',
+ ];
+ const argv = await parseArguments();
+
+ await loadCliConfig({}, argv);
+
+ expect(mockConfigConstructorParams).toHaveBeenCalledWith(
+ expect.objectContaining({
+ question: 'hello from prompt-interactive',
+ deferTelemetryInitialization: false,
+ }),
+ );
+ });
+
+ it('should defer telemetry for ordinary interactive startup', async () => {
+ process.argv = ['node', 'script.js', '--telemetry'];
+ process.stdin.isTTY = true;
+ const argv = await parseArguments();
+
+ await loadCliConfig({}, argv);
+
+ expect(mockConfigConstructorParams).toHaveBeenCalledWith(
+ expect.objectContaining({
+ question: '',
+ deferTelemetryInitialization: true,
+ }),
+ );
+ });
+
it('should set telemetry to false when --no-telemetry flag is present', async () => {
process.argv = ['node', 'script.js', '--no-telemetry'];
const argv = await parseArguments();
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index 826c0dfc724..5a6b61c5fb9 100755
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -2050,6 +2050,9 @@ export async function loadCliConfig(
showResponseTokensPerSecond:
settings.ui?.showResponseTokensPerSecond === true,
telemetry: telemetrySettings,
+ // Prompt-interactive (`qwen -i "prompt"`) auto-submits its first prompt,
+ // so telemetry must be ready before that request is sent.
+ deferTelemetryInitialization: interactive && !isAcpMode && !question,
outboundCorrelation: settings.outboundCorrelation,
usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled ?? true,
clearContextOnIdle: settings.context?.clearContextOnIdle,
diff --git a/packages/cli/src/core/initializer.test.ts b/packages/cli/src/core/initializer.test.ts
index acfa0240b62..0ac9e5c5110 100644
--- a/packages/cli/src/core/initializer.test.ts
+++ b/packages/cli/src/core/initializer.test.ts
@@ -5,7 +5,7 @@
*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { initializeApp } from './initializer.js';
+import { connectIdeForStartup, initializeApp } from './initializer.js';
const mockPerformInitialAuth = vi.fn();
const mockValidateTheme = vi.fn();
@@ -164,7 +164,7 @@ describe('initializeApp', () => {
expect(result.shouldOpenAuthDialog).toBe(false);
});
- it('should connect to IDE when in IDE mode', async () => {
+ it('should connect to IDE by default when in IDE mode', async () => {
mockConfig.getIdeMode.mockReturnValue(true);
await initializeApp(mockConfig as never, mockSettings as never);
@@ -174,6 +174,28 @@ describe('initializeApp', () => {
expect(mockLogIdeConnection).toHaveBeenCalled();
});
+ it('should not connect to IDE when deferred', async () => {
+ mockConfig.getIdeMode.mockReturnValue(true);
+
+ await initializeApp(mockConfig as never, mockSettings as never, {
+ deferIdeConnection: true,
+ });
+
+ expect(mockGetInstance).not.toHaveBeenCalled();
+ expect(mockConnect).not.toHaveBeenCalled();
+ expect(mockLogIdeConnection).not.toHaveBeenCalled();
+ });
+
+ it('should connect to IDE through startup helper', async () => {
+ mockConfig.getIdeMode.mockReturnValue(true);
+
+ await connectIdeForStartup(mockConfig as never);
+
+ expect(mockGetInstance).toHaveBeenCalled();
+ expect(mockConnect).toHaveBeenCalled();
+ expect(mockLogIdeConnection).toHaveBeenCalled();
+ });
+
it('should not connect to IDE when not in IDE mode', async () => {
mockConfig.getIdeMode.mockReturnValue(false);
diff --git a/packages/cli/src/core/initializer.ts b/packages/cli/src/core/initializer.ts
index 3b976f33794..0e256b2aa79 100644
--- a/packages/cli/src/core/initializer.ts
+++ b/packages/cli/src/core/initializer.ts
@@ -14,10 +14,7 @@ import {
import { type LoadedSettings } from '../config/settings.js';
import { performInitialAuth } from './auth.js';
import { validateTheme } from './theme.js';
-import {
- initializeI18n,
- resolveLanguageSetting,
-} from '../i18n/index.js';
+import { initializeI18n, resolveLanguageSetting } from '../i18n/index.js';
export interface InitializationResult {
authError: string | null;
@@ -26,6 +23,30 @@ export interface InitializationResult {
geminiMdFileCount: number;
}
+export interface InitializeAppOptions {
+ /**
+ * When true, skip the awaited IDE connection inside initializeApp().
+ * Ordinary interactive TUI startup uses this so IDE IPC can run after first
+ * paint; non-TUI paths leave it false so the first request keeps IDE context.
+ */
+ deferIdeConnection?: boolean;
+}
+
+/**
+ * Establishes the startup IDE connection and records the connection telemetry.
+ *
+ * Callers choose whether to await this on the startup critical path. Headless,
+ * stream-json, and ACP/Zed await it before their first request; ordinary TUI
+ * startup schedules it post-render through startup prefetch.
+ */
+export async function connectIdeForStartup(config: Config): Promise {
+ if (!config.getIdeMode()) return;
+
+ const ideClient = await IdeClient.getInstance();
+ await ideClient.connect();
+ logIdeConnection(config, new IdeConnectionEvent(IdeConnectionType.START));
+}
+
/**
* Orchestrates the application's startup initialization.
* This runs BEFORE the React UI is rendered.
@@ -36,6 +57,7 @@ export interface InitializationResult {
export async function initializeApp(
config: Config,
settings: LoadedSettings,
+ options: InitializeAppOptions = {},
): Promise {
// Initialize i18n system
await initializeI18n(
@@ -52,10 +74,8 @@ export async function initializeApp(
const shouldOpenAuthDialog =
!config.getModelsConfig().wasAuthTypeExplicitlyProvided() || !!authError;
- if (config.getIdeMode()) {
- const ideClient = await IdeClient.getInstance();
- await ideClient.connect();
- logIdeConnection(config, new IdeConnectionEvent(IdeConnectionType.START));
+ if (!options.deferIdeConnection) {
+ await connectIdeForStartup(config);
}
return {
diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx
index b91dc34b55d..c778bafec65 100644
--- a/packages/cli/src/gemini.test.tsx
+++ b/packages/cli/src/gemini.test.tsx
@@ -30,6 +30,9 @@ import { ApprovalMode, OutputFormat } from '@qwen-code/qwen-code-core';
const mockWriteStderrLine = vi.hoisted(() => vi.fn());
const mockHandleListExtensions = vi.hoisted(() => vi.fn());
+const mockStartEarlyStartupPrefetches = vi.hoisted(() => vi.fn());
+const mockStartPostRenderPrefetches = vi.hoisted(() => vi.fn());
+const mockRunAcpAgent = vi.hoisted(() => vi.fn());
const lspConfigWatcherMock = vi.hoisted(() => ({
instances: [] as Array<{
listener?: (event: unknown) => void | Promise;
@@ -149,6 +152,17 @@ vi.mock('./core/initializer.js', () => ({
}),
}));
+vi.mock('./startup/startup-prefetch.js', () => ({
+ startEarlyStartupPrefetches: (...args: unknown[]) =>
+ mockStartEarlyStartupPrefetches(...args),
+ startPostRenderPrefetches: (...args: unknown[]) =>
+ mockStartPostRenderPrefetches(...args),
+}));
+
+vi.mock('./acp-integration/acpAgent.js', () => ({
+ runAcpAgent: (...args: unknown[]) => mockRunAcpAgent(...args),
+}));
+
vi.mock('./commands/extensions/list.js', () => ({
handleList: mockHandleListExtensions,
}));
@@ -755,6 +769,11 @@ describe('gemini.tsx main function', () => {
}
expect(mockWriteStderrLine).toHaveBeenCalledWith('late memory warning');
+ expect(initializerModule.initializeApp).toHaveBeenCalledWith(
+ configStub,
+ expect.any(Object),
+ { deferIdeConnection: false },
+ );
});
it('creates non-interactive prompt ids that preserve session correlation', () => {
@@ -1079,6 +1098,11 @@ describe('gemini.tsx main function', () => {
configStub,
expect.any(Object),
);
+ expect(initializerModule.initializeApp).toHaveBeenCalledWith(
+ configStub,
+ expect.any(Object),
+ { deferIdeConnection: false },
+ );
expect(runExitCleanupMock).toHaveBeenCalledTimes(1);
});
});
@@ -1148,6 +1172,15 @@ describe('gemini.tsx main function kitty protocol', () => {
'./config/config.js'
);
const { loadSettings } = await import('./config/settings.js');
+ const initializerModule = await import('./core/initializer.js');
+ const initializeAppSpy = vi
+ .spyOn(initializerModule, 'initializeApp')
+ .mockResolvedValue({
+ authError: null,
+ themeError: null,
+ shouldOpenAuthDialog: false,
+ geminiMdFileCount: 0,
+ });
vi.mocked(loadCliConfig).mockResolvedValue({
isInteractive: () => true,
getQuestion: () => '',
@@ -1167,6 +1200,7 @@ describe('gemini.tsx main function kitty protocol', () => {
getModelsConfig: () => ({ getCurrentAuthType: () => null }),
getUsageStatisticsEnabled: () => true,
getSessionId: () => 'test-session-id',
+ isTelemetryInitializationDeferred: () => true,
} as unknown as Config);
vi.mocked(loadSettings).mockReturnValue({
errors: [],
@@ -1238,6 +1272,399 @@ describe('gemini.tsx main function kitty protocol', () => {
expect(setRawModeSpy).toHaveBeenCalledWith(true);
expect(detectAndEnableKittyProtocol).toHaveBeenCalledTimes(1);
+ expect(initializeAppSpy).toHaveBeenCalledWith(
+ expect.any(Object),
+ expect.any(Object),
+ {
+ deferIdeConnection: true,
+ },
+ );
+ expect(mockStartPostRenderPrefetches).toHaveBeenCalledWith(
+ expect.any(Object),
+ expect.any(Object),
+ {
+ connectIde: true,
+ initializeTelemetry: true,
+ },
+ );
+ expect(mockStartEarlyStartupPrefetches).toHaveBeenCalledWith(
+ expect.any(Object),
+ );
+ });
+
+ it('should await IDE connection when interactive mode has an initial prompt', async () => {
+ const { loadCliConfig, parseArguments } = await import(
+ './config/config.js'
+ );
+ const { loadSettings } = await import('./config/settings.js');
+ const initializerModule = await import('./core/initializer.js');
+ const initializeAppSpy = vi
+ .spyOn(initializerModule, 'initializeApp')
+ .mockResolvedValue({
+ authError: null,
+ themeError: null,
+ shouldOpenAuthDialog: false,
+ geminiMdFileCount: 0,
+ });
+ vi.mocked(loadCliConfig).mockResolvedValue({
+ isInteractive: () => true,
+ getQuestion: () => 'hello from prompt-interactive',
+ getSandbox: () => false,
+ getDebugMode: () => false,
+ getListExtensions: () => false,
+ getMcpServers: () => ({}),
+ getTopTierMcpServers: () => undefined,
+ initialize: vi.fn(),
+ waitForMcpReady: vi.fn().mockResolvedValue(undefined),
+ getIdeMode: () => false,
+ getExperimentalZedIntegration: () => false,
+ getScreenReader: () => false,
+ getGeminiMdFileCount: () => 0,
+ getWarnings: () => [],
+ isSafeMode: () => false,
+ getModelsConfig: () => ({ getCurrentAuthType: () => null }),
+ getUsageStatisticsEnabled: () => true,
+ getSessionId: () => 'test-session-id',
+ isTelemetryInitializationDeferred: () => false,
+ } as unknown as Config);
+ vi.mocked(loadSettings).mockReturnValue({
+ errors: [],
+ merged: {
+ advanced: {},
+ security: { auth: {} },
+ ui: {},
+ },
+ setValue: vi.fn(),
+ forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
+ migrationWarnings: [],
+ getUserHooks: () => undefined,
+ getProjectHooks: () => undefined,
+ } as never);
+ vi.mocked(parseArguments).mockResolvedValue({
+ model: undefined,
+ sandbox: undefined,
+ sandboxImage: undefined,
+ debug: undefined,
+ prompt: undefined,
+ promptInteractive: undefined,
+ systemPrompt: undefined,
+ appendSystemPrompt: undefined,
+ query: undefined,
+ yolo: undefined,
+ bare: undefined,
+ approvalMode: undefined,
+ telemetry: undefined,
+ telemetryTarget: undefined,
+ telemetryOtlpEndpoint: undefined,
+ telemetryOtlpProtocol: undefined,
+ telemetryLogPrompts: undefined,
+ telemetryOutfile: undefined,
+ allowedMcpServerNames: undefined,
+ mcpConfig: undefined,
+ allowedTools: undefined,
+ acp: undefined,
+ experimentalAcp: undefined,
+ extensions: undefined,
+ listExtensions: undefined,
+ openaiLogging: undefined,
+ openaiApiKey: undefined,
+ openaiBaseUrl: undefined,
+ openaiLoggingDir: undefined,
+ proxy: undefined,
+ includeDirectories: undefined,
+ screenReader: undefined,
+ inputFormat: undefined,
+ outputFormat: undefined,
+ includePartialMessages: undefined,
+ continue: undefined,
+ resume: undefined,
+ coreTools: undefined,
+ excludeTools: undefined,
+ disabledSlashCommands: undefined,
+ authType: undefined,
+ maxSessionTurns: undefined,
+ maxWallTime: undefined,
+ maxToolCalls: undefined,
+ maxSubagentDepth: undefined,
+ experimentalLsp: undefined,
+ channel: undefined,
+ chatRecording: undefined,
+ sessionId: undefined,
+ fallbackModel: undefined,
+ });
+
+ await main();
+
+ expect(initializeAppSpy).toHaveBeenCalledWith(
+ expect.any(Object),
+ expect.any(Object),
+ {
+ deferIdeConnection: false,
+ },
+ );
+ expect(mockStartPostRenderPrefetches).toHaveBeenCalledWith(
+ expect.any(Object),
+ expect.any(Object),
+ {
+ connectIde: false,
+ initializeTelemetry: false,
+ },
+ );
+ expect(mockStartEarlyStartupPrefetches).toHaveBeenCalledWith(
+ expect.any(Object),
+ );
+ });
+
+ it('should await IDE connection when interactive mode has an input file', async () => {
+ const { loadCliConfig, parseArguments } = await import(
+ './config/config.js'
+ );
+ const { loadSettings } = await import('./config/settings.js');
+ const initializerModule = await import('./core/initializer.js');
+ const initializeAppSpy = vi
+ .spyOn(initializerModule, 'initializeApp')
+ .mockResolvedValue({
+ authError: null,
+ themeError: null,
+ shouldOpenAuthDialog: false,
+ geminiMdFileCount: 0,
+ });
+ vi.mocked(loadCliConfig).mockResolvedValue({
+ isInteractive: () => true,
+ getQuestion: () => '',
+ getInputFile: () => '/tmp/qwen-input.jsonl',
+ getSandbox: () => false,
+ getDebugMode: () => false,
+ getListExtensions: () => false,
+ getMcpServers: () => ({}),
+ getTopTierMcpServers: () => undefined,
+ initialize: vi.fn(),
+ waitForMcpReady: vi.fn().mockResolvedValue(undefined),
+ getIdeMode: () => false,
+ getExperimentalZedIntegration: () => false,
+ getScreenReader: () => false,
+ getGeminiMdFileCount: () => 0,
+ getWarnings: () => [],
+ isSafeMode: () => false,
+ getModelsConfig: () => ({ getCurrentAuthType: () => null }),
+ getUsageStatisticsEnabled: () => true,
+ getSessionId: () => 'test-session-id',
+ isTelemetryInitializationDeferred: () => true,
+ } as unknown as Config);
+ vi.mocked(loadSettings).mockReturnValue({
+ errors: [],
+ merged: {
+ advanced: {},
+ security: { auth: {} },
+ ui: {},
+ },
+ setValue: vi.fn(),
+ forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
+ migrationWarnings: [],
+ getUserHooks: () => undefined,
+ getProjectHooks: () => undefined,
+ } as never);
+ vi.mocked(parseArguments).mockResolvedValue({
+ model: undefined,
+ sandbox: undefined,
+ sandboxImage: undefined,
+ debug: undefined,
+ prompt: undefined,
+ promptInteractive: undefined,
+ systemPrompt: undefined,
+ appendSystemPrompt: undefined,
+ query: undefined,
+ yolo: undefined,
+ bare: undefined,
+ approvalMode: undefined,
+ telemetry: undefined,
+ telemetryTarget: undefined,
+ telemetryOtlpEndpoint: undefined,
+ telemetryOtlpProtocol: undefined,
+ telemetryLogPrompts: undefined,
+ telemetryOutfile: undefined,
+ allowedMcpServerNames: undefined,
+ mcpConfig: undefined,
+ allowedTools: undefined,
+ acp: undefined,
+ experimentalAcp: undefined,
+ extensions: undefined,
+ listExtensions: undefined,
+ openaiLogging: undefined,
+ openaiApiKey: undefined,
+ openaiBaseUrl: undefined,
+ openaiLoggingDir: undefined,
+ proxy: undefined,
+ includeDirectories: undefined,
+ screenReader: undefined,
+ inputFormat: undefined,
+ outputFormat: undefined,
+ includePartialMessages: undefined,
+ continue: undefined,
+ resume: undefined,
+ coreTools: undefined,
+ excludeTools: undefined,
+ disabledSlashCommands: undefined,
+ authType: undefined,
+ maxSessionTurns: undefined,
+ maxWallTime: undefined,
+ maxToolCalls: undefined,
+ maxSubagentDepth: undefined,
+ experimentalLsp: undefined,
+ channel: undefined,
+ chatRecording: undefined,
+ sessionId: undefined,
+ fallbackModel: undefined,
+ });
+
+ await main();
+
+ expect(initializeAppSpy).toHaveBeenCalledWith(
+ expect.any(Object),
+ expect.any(Object),
+ {
+ deferIdeConnection: false,
+ },
+ );
+ expect(mockStartPostRenderPrefetches).toHaveBeenCalledWith(
+ expect.any(Object),
+ expect.any(Object),
+ {
+ connectIde: false,
+ initializeTelemetry: true,
+ },
+ );
+ });
+
+ it('should not defer IDE connection when Zed integration is enabled', async () => {
+ const { loadCliConfig, parseArguments } = await import(
+ './config/config.js'
+ );
+ const { loadSettings } = await import('./config/settings.js');
+ const initializerModule = await import('./core/initializer.js');
+ const initializeAppSpy = vi
+ .spyOn(initializerModule, 'initializeApp')
+ .mockResolvedValue({
+ authError: null,
+ themeError: null,
+ shouldOpenAuthDialog: false,
+ geminiMdFileCount: 0,
+ });
+ vi.mocked(loadCliConfig).mockResolvedValue({
+ isInteractive: () => true,
+ getQuestion: () => '',
+ getSandbox: () => false,
+ getDebugMode: () => false,
+ getListExtensions: () => false,
+ getMcpServers: () => ({}),
+ getTopTierMcpServers: () => undefined,
+ initialize: vi.fn(),
+ waitForMcpReady: vi.fn().mockResolvedValue(undefined),
+ getIdeMode: () => false,
+ getExperimentalZedIntegration: () => true,
+ getScreenReader: () => false,
+ getGeminiMdFileCount: () => 0,
+ getWarnings: () => [],
+ isSafeMode: () => false,
+ getModelsConfig: () => ({ getCurrentAuthType: () => null }),
+ getUsageStatisticsEnabled: () => true,
+ getSessionId: () => 'test-session-id',
+ } as unknown as Config);
+ vi.mocked(loadSettings).mockReturnValue({
+ errors: [],
+ merged: {
+ advanced: {},
+ security: { auth: {} },
+ ui: {},
+ },
+ setValue: vi.fn(),
+ forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
+ migrationWarnings: [],
+ getUserHooks: () => undefined,
+ getProjectHooks: () => undefined,
+ } as never);
+ vi.mocked(parseArguments).mockResolvedValue({
+ model: undefined,
+ sandbox: undefined,
+ sandboxImage: undefined,
+ debug: undefined,
+ prompt: undefined,
+ promptInteractive: undefined,
+ systemPrompt: undefined,
+ appendSystemPrompt: undefined,
+ query: undefined,
+ yolo: undefined,
+ bare: undefined,
+ approvalMode: undefined,
+ telemetry: undefined,
+ telemetryTarget: undefined,
+ telemetryOtlpEndpoint: undefined,
+ telemetryOtlpProtocol: undefined,
+ telemetryLogPrompts: undefined,
+ telemetryOutfile: undefined,
+ allowedMcpServerNames: undefined,
+ mcpConfig: undefined,
+ allowedTools: undefined,
+ acp: undefined,
+ experimentalAcp: undefined,
+ extensions: undefined,
+ listExtensions: undefined,
+ openaiLogging: undefined,
+ openaiApiKey: undefined,
+ openaiBaseUrl: undefined,
+ openaiLoggingDir: undefined,
+ proxy: undefined,
+ includeDirectories: undefined,
+ screenReader: undefined,
+ inputFormat: undefined,
+ outputFormat: undefined,
+ includePartialMessages: undefined,
+ continue: undefined,
+ resume: undefined,
+ coreTools: undefined,
+ excludeTools: undefined,
+ disabledSlashCommands: undefined,
+ authType: undefined,
+ maxSessionTurns: undefined,
+ maxWallTime: undefined,
+ maxToolCalls: undefined,
+ maxSubagentDepth: undefined,
+ experimentalLsp: undefined,
+ channel: undefined,
+ chatRecording: undefined,
+ sessionId: undefined,
+ fallbackModel: undefined,
+ });
+
+ // Mock process.exit to throw instead of terminating the process
+ const originalExit = process.exit;
+ process.exit = ((code?: string | number | null | undefined) => {
+ throw new MockProcessExitError(code);
+ }) as unknown as typeof process.exit;
+
+ try {
+ await main();
+ } catch (e) {
+ if (!(e instanceof MockProcessExitError)) throw e;
+ } finally {
+ process.exit = originalExit;
+ }
+
+ expect(initializeAppSpy).toHaveBeenCalledWith(
+ expect.any(Object),
+ expect.any(Object),
+ {
+ deferIdeConnection: false,
+ },
+ );
+ expect(mockRunAcpAgent).toHaveBeenCalledWith(
+ expect.any(Object),
+ expect.any(Object),
+ expect.any(Object),
+ );
+ expect(mockStartEarlyStartupPrefetches).toHaveBeenCalledWith(
+ expect.any(Object),
+ );
});
it('should run cleanup before exiting on interactive SIGINT', async () => {
@@ -1285,6 +1712,7 @@ describe('gemini.tsx main function kitty protocol', () => {
getProxy: () => undefined,
getUsageStatisticsEnabled: () => true,
getSessionId: () => 'test-session-id',
+ isTelemetryInitializationDeferred: () => true,
} as unknown as Config);
vi.mocked(loadSettings).mockReturnValue({
errors: [],
@@ -1448,7 +1876,8 @@ describe('startInteractiveUI', () => {
const mockConfig = {
getProjectRoot: () => '/root',
getScreenReader: () => false,
- } as Config;
+ isTelemetryInitializationDeferred: () => true,
+ } as unknown as Config;
const mockSettings = {
merged: {
ui: {
@@ -1470,10 +1899,6 @@ describe('startInteractiveUI', () => {
disableKittyProtocol: vi.fn(),
}));
- vi.mock('./ui/utils/updateCheck.js', () => ({
- checkForUpdates: vi.fn(() => Promise.resolve(null)),
- }));
-
vi.mock('./utils/cleanup.js', () => ({
cleanupCheckpoints: vi.fn(() => Promise.resolve()),
registerCleanup: vi.fn(),
@@ -1524,7 +1949,6 @@ describe('startInteractiveUI', () => {
it('should perform all startup tasks in correct order', async () => {
const { getCliVersion } = await import('./utils/version.js');
- const { checkForUpdates } = await import('./ui/utils/updateCheck.js');
const { registerCleanup } = await import('./utils/cleanup.js');
const mockInitializationResult = {
@@ -1550,15 +1974,42 @@ describe('startInteractiveUI', () => {
const cleanupFn = vi.mocked(registerCleanup).mock.calls[0][0];
expect(typeof cleanupFn).toBe('function');
- // checkForUpdates should be called asynchronously (not waited for)
- // We need a small delay to let it execute
- await new Promise((resolve) => setTimeout(resolve, 0));
- expect(checkForUpdates).toHaveBeenCalledTimes(1);
+ expect(mockStartPostRenderPrefetches).toHaveBeenCalledWith(
+ mockConfig,
+ mockSettings,
+ { connectIde: false, initializeTelemetry: true },
+ );
});
- it('should not call checkForUpdates when enableAutoUpdate is false', async () => {
- const { checkForUpdates } = await import('./ui/utils/updateCheck.js');
+ it('can skip post-render IDE connection after prompt-interactive awaited it', async () => {
+ const promptInteractiveConfig = {
+ ...mockConfig,
+ isTelemetryInitializationDeferred: () => false,
+ } as unknown as Config;
+ const mockInitializationResult = {
+ authError: null,
+ themeError: null,
+ shouldOpenAuthDialog: false,
+ geminiMdFileCount: 0,
+ };
+ await startInteractiveUI(
+ promptInteractiveConfig,
+ mockSettings,
+ mockStartupWarnings,
+ mockWorkspaceRoot,
+ mockInitializationResult,
+ { postRenderConnectIde: false },
+ );
+
+ expect(mockStartPostRenderPrefetches).toHaveBeenCalledWith(
+ promptInteractiveConfig,
+ mockSettings,
+ { connectIde: false, initializeTelemetry: false },
+ );
+ });
+
+ it('delegates auto-update gating to post-render prefetch', async () => {
const settingsWithAutoUpdateDisabled = {
merged: {
general: {
@@ -1585,9 +2036,10 @@ describe('startInteractiveUI', () => {
mockInitializationResult,
);
- await new Promise((resolve) => setTimeout(resolve, 0));
-
- // checkForUpdates should NOT be called when enableAutoUpdate is false
- expect(checkForUpdates).not.toHaveBeenCalled();
+ expect(mockStartPostRenderPrefetches).toHaveBeenCalledWith(
+ mockConfig,
+ settingsWithAutoUpdateDisabled,
+ { connectIde: false, initializeTelemetry: true },
+ );
});
});
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index 18e2d1df0fc..7025d33d0aa 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -49,6 +49,7 @@ import {
buildStartupWorktreeNotice,
type StartupWorktreeContext,
} from './startup/worktreeStartup.js';
+import { startEarlyStartupPrefetches } from './startup/startup-prefetch.js';
import {
cleanupCheckpoints,
registerCleanup,
@@ -73,7 +74,6 @@ import { getUserStartupWarnings } from './utils/userStartupWarnings.js';
import { initializeWarningHandler } from './utils/warningHandler.js';
import { writeStderrLine } from './utils/stdioHelpers.js';
import { getHeadlessYoloSafetyWarning } from './utils/headlessSafetyWarnings.js';
-import { preconnectApi } from './utils/apiPreconnect.js';
import { initializeLlmOutputLanguage } from './utils/languageUtils.js';
const debugLogger = createDebugLogger('STARTUP');
@@ -687,20 +687,7 @@ export async function main() {
// This ensures MCP server subprocesses are properly terminated on exit
registerCleanup(() => config.shutdown());
- // Startup optimization: preconnect API to warm TCP+TLS connection
- // Fires early; cost is one HEAD request even for local-only commands
- try {
- const modelsConfig = config.getModelsConfig();
- const authType = modelsConfig.getCurrentAuthType();
- const resolvedBaseUrl = modelsConfig.getGenerationConfig().baseUrl;
- const proxy = config.getProxy();
- preconnectApi(authType, { resolvedBaseUrl, proxy });
- } catch (error) {
- // If we can't get authType, skip preconnect - it's optional optimization
- debugLogger.debug(
- `Preconnect skipped due to error getting authType: ${error}`,
- );
- }
+ startEarlyStartupPrefetches(config);
const wasRaw = process.stdin.isRaw;
let kittyProtocolDetectionComplete: Promise | undefined;
@@ -764,7 +751,16 @@ export async function main() {
// For stream-json mode, defer config.initialize() until after the initialize control request
// For other modes, initialize normally
const { initializeApp } = await import('./core/initializer.js');
- const initializationResult = await initializeApp(config, settings);
+ let input = config.getQuestion();
+ const hasRemoteInput = Boolean(config.getInputFile?.());
+ const deferIdeConnection =
+ config.isInteractive() &&
+ !config.getExperimentalZedIntegration() &&
+ !input &&
+ !hasRemoteInput;
+ const initializationResult = await initializeApp(config, settings, {
+ deferIdeConnection,
+ });
profileCheckpoint('after_initialize_app');
if (config.getExperimentalZedIntegration()) {
@@ -775,26 +771,6 @@ export async function main() {
process.exit(0);
}
- // Background housekeeping: file-history cleanup and (future) other
- // periodic disk maintenance. Interactive-only — serve/SDK/ACP modes
- // don't create the file-history dirs this cleans, so they skip.
- // Dynamic import keeps --help / one-shot --prompt paths from loading
- // this code at all. Timers inside are .unref()'d so they never block
- // process exit.
- if (config.isInteractive()) {
- // .catch() is intentional: a dynamic-import or module-init failure
- // (theoretically near-impossible — the module has no top-level side
- // effects — but defense in depth matches the runPass try/catch in
- // scheduler.ts) becomes a swallowed log instead of an unhandled
- // promise rejection that crashes the REPL.
- void import('./utils/housekeeping/scheduler.js')
- .then((m) => m.startBackgroundHousekeeping(config, settings))
- .catch((err) => {
- debugLogger.warn('failed to start background housekeeping:', err);
- });
- }
-
- let input = config.getQuestion();
const startupWarnings = [
...new Set([
...(config.isSafeMode()
@@ -872,6 +848,9 @@ export async function main() {
startupWarnings,
process.cwd(),
initializationResult!,
+ {
+ postRenderConnectIde: deferIdeConnection,
+ },
);
// Clean up corruption env vars so subsequent relaunch children
// and subprocesses don't inherit stale state.
diff --git a/packages/cli/src/startup/startup-prefetch.test.ts b/packages/cli/src/startup/startup-prefetch.test.ts
new file mode 100644
index 00000000000..9a3abe9d9bf
--- /dev/null
+++ b/packages/cli/src/startup/startup-prefetch.test.ts
@@ -0,0 +1,287 @@
+/**
+ * @license
+ * Copyright 2026 Qwen Code
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Config } from '@qwen-code/qwen-code-core';
+import type { LoadedSettings } from '../config/settings.js';
+import {
+ startEarlyStartupPrefetches,
+ startPostRenderPrefetches,
+} from './startup-prefetch.js';
+
+const mockDebug = vi.hoisted(() => vi.fn());
+const mockWarn = vi.hoisted(() => vi.fn());
+const mockPreconnectApi = vi.hoisted(() => vi.fn());
+const mockRecordStartupEvent = vi.hoisted(() => vi.fn());
+const mockCheckForUpdates = vi.hoisted(() => vi.fn());
+const mockHandleAutoUpdate = vi.hoisted(() => vi.fn());
+const mockConnectIdeForStartup = vi.hoisted(() => vi.fn());
+const mockInitializeTelemetry = vi.hoisted(() => vi.fn());
+const mockStartBackgroundHousekeeping = vi.hoisted(() => vi.fn());
+
+vi.mock('@qwen-code/qwen-code-core', () => ({
+ createDebugLogger: () => ({
+ debug: mockDebug,
+ warn: mockWarn,
+ }),
+ initializeTelemetry: (...args: unknown[]) => mockInitializeTelemetry(...args),
+}));
+
+vi.mock('../utils/apiPreconnect.js', () => ({
+ preconnectApi: (...args: unknown[]) => mockPreconnectApi(...args),
+}));
+
+vi.mock('../utils/startupProfiler.js', () => ({
+ recordStartupEvent: (...args: unknown[]) => mockRecordStartupEvent(...args),
+}));
+
+vi.mock('../ui/utils/updateCheck.js', () => ({
+ checkForUpdates: (...args: unknown[]) => mockCheckForUpdates(...args),
+}));
+
+vi.mock('../utils/handleAutoUpdate.js', () => ({
+ handleAutoUpdate: (...args: unknown[]) => mockHandleAutoUpdate(...args),
+}));
+
+vi.mock('../core/initializer.js', () => ({
+ connectIdeForStartup: (...args: unknown[]) =>
+ mockConnectIdeForStartup(...args),
+}));
+
+vi.mock('../utils/housekeeping/scheduler.js', () => ({
+ startBackgroundHousekeeping: (...args: unknown[]) =>
+ mockStartBackgroundHousekeeping(...args),
+}));
+
+function makeConfig(overrides: Partial = {}): Config {
+ return {
+ getModelsConfig: () => ({
+ getCurrentAuthType: () => 'openai',
+ getGenerationConfig: () => ({ baseUrl: 'https://api.openai.com/v1' }),
+ }),
+ getProxy: () => 'http://proxy.example',
+ getProjectRoot: () => '/repo',
+ getIdeMode: () => true,
+ isInteractive: () => true,
+ ...overrides,
+ } as unknown as Config;
+}
+
+function makeSettings(
+ enableAutoUpdate: boolean | undefined = undefined,
+): LoadedSettings {
+ return {
+ merged: {
+ general: enableAutoUpdate === undefined ? {} : { enableAutoUpdate },
+ },
+ } as LoadedSettings;
+}
+
+describe('startupPrefetch', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.useRealTimers();
+ mockCheckForUpdates.mockResolvedValue(null);
+ mockConnectIdeForStartup.mockResolvedValue(undefined);
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('starts API preconnect with resolved auth config', () => {
+ const config = makeConfig();
+
+ startEarlyStartupPrefetches(config);
+
+ expect(mockPreconnectApi).toHaveBeenCalledWith('openai', {
+ resolvedBaseUrl: 'https://api.openai.com/v1',
+ proxy: 'http://proxy.example',
+ });
+ });
+
+ it('does not record an unbalanced lifecycle event for API preconnect', () => {
+ const config = makeConfig();
+
+ startEarlyStartupPrefetches(config);
+
+ expect(mockRecordStartupEvent).not.toHaveBeenCalledWith(
+ 'startup_prefetch_started',
+ { name: 'api_preconnect' },
+ );
+ });
+
+ it('starts early prefetch only once per config', () => {
+ const config = makeConfig();
+
+ startEarlyStartupPrefetches(config);
+ startEarlyStartupPrefetches(config);
+
+ expect(mockPreconnectApi).toHaveBeenCalledTimes(1);
+ });
+
+ it('skips preconnect errors without throwing', () => {
+ const config = makeConfig({
+ getModelsConfig: () => {
+ throw new Error('bad config');
+ },
+ } as Partial);
+
+ expect(() => startEarlyStartupPrefetches(config)).not.toThrow();
+ expect(mockPreconnectApi).not.toHaveBeenCalled();
+ expect(mockDebug).toHaveBeenCalled();
+ });
+
+ it('starts post-render tasks without awaiting completion', async () => {
+ const config = makeConfig();
+ const updatePromise = new Promise(() => {});
+ mockCheckForUpdates.mockReturnValue(updatePromise);
+
+ startPostRenderPrefetches(config, makeSettings(), { connectIde: true });
+
+ await vi.dynamicImportSettled();
+
+ expect(mockCheckForUpdates).toHaveBeenCalledTimes(1);
+ expect(mockConnectIdeForStartup).toHaveBeenCalledWith(config);
+ expect(mockStartBackgroundHousekeeping).toHaveBeenCalledWith(
+ config,
+ expect.any(Object),
+ );
+ });
+
+ it('does not run update check when auto-update is disabled', async () => {
+ const config = makeConfig();
+
+ startPostRenderPrefetches(config, makeSettings(false), {
+ connectIde: true,
+ });
+
+ await vi.dynamicImportSettled();
+
+ expect(mockCheckForUpdates).not.toHaveBeenCalled();
+ expect(mockConnectIdeForStartup).toHaveBeenCalledWith(config);
+ });
+
+ it('requires connectIde option before connecting IDE', async () => {
+ const config = makeConfig();
+
+ startPostRenderPrefetches(config, makeSettings());
+
+ await vi.dynamicImportSettled();
+
+ expect(mockConnectIdeForStartup).not.toHaveBeenCalled();
+ });
+
+ it('does not connect IDE when IDE mode is disabled', async () => {
+ const config = makeConfig({ getIdeMode: () => false } as Partial);
+
+ startPostRenderPrefetches(config, makeSettings(), { connectIde: true });
+
+ await vi.dynamicImportSettled();
+
+ expect(mockConnectIdeForStartup).not.toHaveBeenCalled();
+ });
+
+ it('fails deferred IDE connection when the startup connect hangs', async () => {
+ vi.useFakeTimers();
+ const config = makeConfig();
+ const hangingConnect = new Promise(() => {});
+ mockConnectIdeForStartup.mockReturnValue(hangingConnect);
+
+ startPostRenderPrefetches(config, makeSettings(), { connectIde: true });
+
+ await vi.dynamicImportSettled();
+ await vi.advanceTimersByTimeAsync(10_000);
+
+ expect(mockRecordStartupEvent).toHaveBeenCalledWith(
+ 'startup_prefetch_failed',
+ { name: 'ide_connect' },
+ );
+ expect(mockWarn).toHaveBeenCalledWith(
+ 'ide_connect failed:',
+ expect.objectContaining({
+ message: 'ide_connect timed out after 10000ms',
+ }),
+ );
+ });
+
+ it('initializes telemetry when requested', async () => {
+ const config = makeConfig();
+
+ startPostRenderPrefetches(config, makeSettings(), {
+ initializeTelemetry: true,
+ });
+
+ await vi.dynamicImportSettled();
+
+ expect(mockInitializeTelemetry).toHaveBeenCalledWith(config);
+ });
+
+ it('does not initialize telemetry unless requested', async () => {
+ const config = makeConfig();
+
+ startPostRenderPrefetches(config, makeSettings());
+
+ await vi.dynamicImportSettled();
+
+ expect(mockInitializeTelemetry).not.toHaveBeenCalled();
+ });
+
+ it('swallows telemetry initialization failures', async () => {
+ const config = makeConfig();
+ const error = new Error('otel unavailable');
+ mockInitializeTelemetry.mockImplementation(() => {
+ throw error;
+ });
+
+ expect(() =>
+ startPostRenderPrefetches(config, makeSettings(), {
+ initializeTelemetry: true,
+ }),
+ ).not.toThrow();
+
+ await vi.dynamicImportSettled();
+
+ expect(mockWarn).toHaveBeenCalledWith('telemetry_init failed:', error);
+ });
+
+ it('swallows deferred task failures', async () => {
+ const config = makeConfig();
+ const error = new Error('network down');
+ mockCheckForUpdates.mockRejectedValue(error);
+
+ expect(() =>
+ startPostRenderPrefetches(config, makeSettings()),
+ ).not.toThrow();
+
+ await vi.dynamicImportSettled();
+
+ expect(mockWarn).toHaveBeenCalledWith('update_check failed:', error);
+ });
+
+ it('does not start housekeeping for non-interactive configs', async () => {
+ const config = makeConfig({
+ isInteractive: () => false,
+ } as Partial);
+
+ startPostRenderPrefetches(config, makeSettings());
+
+ await vi.dynamicImportSettled();
+
+ expect(mockStartBackgroundHousekeeping).not.toHaveBeenCalled();
+ });
+
+ it('starts post-render prefetch only once per config', async () => {
+ const config = makeConfig();
+
+ startPostRenderPrefetches(config, makeSettings());
+ startPostRenderPrefetches(config, makeSettings());
+
+ await vi.dynamicImportSettled();
+
+ expect(mockCheckForUpdates).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/cli/src/startup/startup-prefetch.ts b/packages/cli/src/startup/startup-prefetch.ts
new file mode 100644
index 00000000000..61bf7c3adbb
--- /dev/null
+++ b/packages/cli/src/startup/startup-prefetch.ts
@@ -0,0 +1,137 @@
+/**
+ * @license
+ * Copyright 2026 Qwen Code
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ createDebugLogger,
+ initializeTelemetry,
+ type Config,
+} from '@qwen-code/qwen-code-core';
+import type { LoadedSettings } from '../config/settings.js';
+import { preconnectApi } from '../utils/apiPreconnect.js';
+import { recordStartupEvent } from '../utils/startupProfiler.js';
+
+const debugLogger = createDebugLogger('STARTUP_PREFETCH');
+
+const DEFERRED_IDE_CONNECT_TIMEOUT_MS = 10_000;
+
+const earlyStarted = new WeakSet();
+const postRenderStarted = new WeakSet();
+
+function withTimeout(
+ promise: Promise,
+ name: string,
+ timeoutMs: number,
+): Promise {
+ let timeoutId: ReturnType | undefined;
+ const timeout = new Promise((_, reject) => {
+ timeoutId = setTimeout(() => {
+ reject(new Error(`${name} timed out after ${timeoutMs}ms`));
+ }, timeoutMs);
+ timeoutId.unref?.();
+ });
+
+ return Promise.race([promise, timeout]).finally(() => {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ });
+}
+
+/**
+ * Starts a best-effort startup task without adding it to the caller's
+ * critical path. Failures are recorded for diagnostics and otherwise
+ * swallowed so optional prefetch work cannot break REPL startup.
+ */
+function runDeferredTask(name: string, task: () => Promise | void): void {
+ recordStartupEvent('startup_prefetch_started', { name });
+ void Promise.resolve()
+ .then(task)
+ .then(() => {
+ recordStartupEvent('startup_prefetch_completed', { name });
+ })
+ .catch((err) => {
+ recordStartupEvent('startup_prefetch_failed', { name });
+ debugLogger.warn(`${name} failed:`, err);
+ });
+}
+
+/**
+ * Starts pre-render startup prefetches that benefit from maximum lead time.
+ *
+ * This runs after `loadCliConfig()` has produced a Config, but before
+ * `initializeApp()` and UI rendering. Keep this phase limited to work that is
+ * cheap to start, independent of Ink/React, and safe to ignore on failure.
+ */
+export function startEarlyStartupPrefetches(config: Config): void {
+ if (earlyStarted.has(config)) return;
+ earlyStarted.add(config);
+
+ try {
+ const modelsConfig = config.getModelsConfig();
+ const authType = modelsConfig.getCurrentAuthType();
+ const resolvedBaseUrl = modelsConfig.getGenerationConfig().baseUrl;
+ const proxy = config.getProxy();
+ preconnectApi(authType, { resolvedBaseUrl, proxy });
+ } catch (error) {
+ debugLogger.debug(
+ `Preconnect skipped due to error getting authType: ${error}`,
+ );
+ }
+}
+
+/**
+ * Starts post-render startup prefetches for ordinary interactive TUI sessions.
+ *
+ * This runs immediately after Ink's `render()` returns (`first_paint`). Tasks
+ * here may load heavier modules or perform network/IPC work, but they must not
+ * affect `startInteractiveUI()` success. `connectIde` is opt-in so headless,
+ * stream-json, and ACP/Zed paths can keep their awaited IDE startup semantics.
+ */
+export function startPostRenderPrefetches(
+ config: Config,
+ settings: LoadedSettings,
+ options: { connectIde?: boolean; initializeTelemetry?: boolean } = {},
+): void {
+ if (postRenderStarted.has(config)) return;
+ postRenderStarted.add(config);
+
+ if (settings.merged.general?.enableAutoUpdate !== false) {
+ runDeferredTask('update_check', async () => {
+ const [{ checkForUpdates }, { handleAutoUpdate }] = await Promise.all([
+ import('../ui/utils/updateCheck.js'),
+ import('../utils/handleAutoUpdate.js'),
+ ]);
+ const info = await checkForUpdates();
+ handleAutoUpdate(info, settings, config.getProjectRoot());
+ });
+ }
+
+ if (options.connectIde && config.getIdeMode()) {
+ runDeferredTask('ide_connect', async () => {
+ const { connectIdeForStartup } = await import('../core/initializer.js');
+ await withTimeout(
+ connectIdeForStartup(config),
+ 'ide_connect',
+ DEFERRED_IDE_CONNECT_TIMEOUT_MS,
+ );
+ });
+ }
+
+ if (options.initializeTelemetry) {
+ runDeferredTask('telemetry_init', () => {
+ initializeTelemetry(config);
+ });
+ }
+
+ if (config.isInteractive()) {
+ runDeferredTask('background_housekeeping', async () => {
+ const { startBackgroundHousekeeping } = await import(
+ '../utils/housekeeping/scheduler.js'
+ );
+ startBackgroundHousekeeping(config, settings);
+ });
+ }
+}
diff --git a/packages/cli/src/ui/startInteractiveUI.tsx b/packages/cli/src/ui/startInteractiveUI.tsx
index 71662dcae18..9c8a1021a77 100644
--- a/packages/cli/src/ui/startInteractiveUI.tsx
+++ b/packages/cli/src/ui/startInteractiveUI.tsx
@@ -26,15 +26,14 @@ import { VimModeProvider } from './contexts/VimModeContext.js';
import { AgentViewProvider } from './contexts/AgentViewContext.js';
import { BackgroundTaskViewProvider } from './contexts/BackgroundTaskViewContext.js';
import { useKittyKeyboardProtocol } from './hooks/useKittyKeyboardProtocol.js';
-import { checkForUpdates } from './utils/updateCheck.js';
import { disableKittyProtocol } from './utils/kittyProtocolDetector.js';
import { installTerminalRedrawOptimizer } from './utils/terminalRedrawOptimizer.js';
import { installSynchronizedOutput } from './utils/synchronizedOutput.js';
-import { handleAutoUpdate } from '../utils/handleAutoUpdate.js';
import { registerCleanup } from '../utils/cleanup.js';
import { stopAndGetCapturedInput } from '../utils/earlyInputCapture.js';
import { profileCheckpoint } from '../utils/startupProfiler.js';
import { writeStderrLine } from '../utils/stdioHelpers.js';
+import { startPostRenderPrefetches } from '../startup/startup-prefetch.js';
import {
computeWindowTitle,
writeTerminalTitle,
@@ -43,12 +42,18 @@ import { getCliVersion } from '../utils/version.js';
const debugLogger = createDebugLogger('STARTUP');
+export interface StartInteractiveUIOptions {
+ postRenderConnectIde?: boolean;
+ postRenderInitializeTelemetry?: boolean;
+}
+
export async function startInteractiveUI(
config: Config,
settings: LoadedSettings,
startupWarnings: string[],
workspaceRoot: string = process.cwd(),
initializationResult: InitializationResult,
+ options: StartInteractiveUIOptions = {},
) {
const version = await getCliVersion();
setWindowTitle(settings, basename(workspaceRoot));
@@ -195,19 +200,12 @@ export async function startInteractiveUI(
// after this — it carries the `config_initialize_*` and
// `input_enabled` checkpoints that complete the first-screen picture.
profileCheckpoint('first_paint');
-
- // Check for updates only if enableAutoUpdate is not explicitly disabled.
- // Using !== false ensures updates are enabled by default when undefined.
- if (settings.merged.general?.enableAutoUpdate !== false) {
- checkForUpdates()
- .then((info) => {
- handleAutoUpdate(info, settings, config.getProjectRoot());
- })
- .catch((err) => {
- // Silently ignore update check errors.
- debugLogger.warn(`Update check failed: ${err}`);
- });
- }
+ startPostRenderPrefetches(config, settings, {
+ connectIde: options.postRenderConnectIde ?? false,
+ initializeTelemetry:
+ options.postRenderInitializeTelemetry ??
+ config.isTelemetryInitializationDeferred(),
+ });
registerCleanup(async () => {
remoteInputWatcher?.shutdown();
diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts
index 0931977fcbf..50577b590f9 100644
--- a/packages/core/src/config/config.test.ts
+++ b/packages/core/src/config/config.test.ts
@@ -29,6 +29,7 @@ import {
DEFAULT_OTLP_ENDPOINT,
SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH_LIMIT,
QwenLogger,
+ initializeTelemetry,
isTelemetrySdkInitialized,
shutdownTelemetry,
refreshSessionContext,
@@ -4055,6 +4056,21 @@ describe('Server Config (config.ts)', () => {
};
const config = new Config(paramsWithTelemetry);
expect(config.getTelemetryEnabled()).toBe(true);
+ expect(config.isTelemetryInitializationDeferred()).toBe(false);
+ expect(initializeTelemetry).toHaveBeenCalledWith(config);
+ });
+
+ it('Config constructor should defer telemetry initialization when requested', () => {
+ const paramsWithTelemetry: ConfigParameters = {
+ ...baseParams,
+ telemetry: { enabled: true },
+ deferTelemetryInitialization: true,
+ };
+ const config = new Config(paramsWithTelemetry);
+
+ expect(config.getTelemetryEnabled()).toBe(true);
+ expect(config.isTelemetryInitializationDeferred()).toBe(true);
+ expect(initializeTelemetry).not.toHaveBeenCalled();
});
it('Config shutdown should flush telemetry when SDK is initialized', async () => {
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index f0c4e19b8bd..3d29289511c 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -908,6 +908,11 @@ export interface ConfigParameters {
accessibility?: AccessibilitySettings;
showResponseTokensPerSecond?: boolean;
telemetry?: TelemetrySettings;
+ /**
+ * Delay SDK startup for interactive render paths. Telemetry settings still
+ * remain readable from Config; only the global SDK side effect is deferred.
+ */
+ deferTelemetryInitialization?: boolean;
outboundCorrelation?: OutboundCorrelationSettings;
gitCoAuthor?: GitCoAuthorParam;
usageStatisticsEnabled?: boolean;
@@ -1561,6 +1566,7 @@ export class Config {
private readonly accessibility: AccessibilitySettings;
private readonly showResponseTokensPerSecond: boolean;
private readonly telemetrySettings: ResolvedTelemetrySettings;
+ private readonly telemetryInitializationDeferred: boolean;
private readonly outboundCorrelationSettings: OutboundCorrelationSettings;
private readonly gitCoAuthor: GitCoAuthorSettings;
private readonly usageStatisticsEnabled: boolean;
@@ -1796,6 +1802,8 @@ export class Config {
metrics: params.telemetry?.metrics,
resourceAttributeWarnings: params.telemetry?.resourceAttributeWarnings,
};
+ this.telemetryInitializationDeferred =
+ params.deferTelemetryInitialization ?? false;
this.outboundCorrelationSettings = {
propagateTraceContext:
params.outboundCorrelation?.propagateTraceContext ?? false,
@@ -1952,7 +1960,10 @@ export class Config {
onModelChange: this.handleModelChange.bind(this),
});
- if (this.telemetrySettings.enabled) {
+ if (
+ this.telemetrySettings.enabled &&
+ !this.telemetryInitializationDeferred
+ ) {
initializeTelemetry(this);
}
@@ -4956,6 +4967,10 @@ export class Config {
return this.telemetrySettings.enabled ?? false;
}
+ isTelemetryInitializationDeferred(): boolean {
+ return this.telemetryInitializationDeferred;
+ }
+
getTelemetryLogPromptsEnabled(): boolean {
return this.telemetrySettings.logPrompts ?? true;
}