CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
gemini.tsx1139 linesDownload Raw Back to src
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import {8  AuthType,9  type Config,10  InputFormat,11  isDebugLoggingDegraded,12  isBareMode,13  logUserPrompt,14  QWEN_CODE_SIMPLE_ENV_VAR,15  Storage,16  SessionService,17  setStartupEventSink,18  createDebugLogger,19  persistSessionUsage,20  uiTelemetryService,21} from '@qwen-code/qwen-code-core';22import dns from 'node:dns';23import os from 'node:os';24import path from 'node:path';25import v8 from 'node:v8';26import { validateAuthMethod } from './config/auth.js';27import * as cliConfig from './config/config.js';28import {29  buildDisabledSkillNamesProvider,30  loadCliConfig,31  parseArguments,32} from './config/config.js';33import type { DnsResolutionOrder } from './config/settings.js';34import {35  ENV_CORRUPTED_PATH,36  ENV_WAS_RECOVERED,37  createMinimalSettings,38  getSettingsWarnings,39  loadSettings,40  preResolveHomeEnvOverrides,41} from './config/settings.js';42import { SettingsWatcher } from './config/settingsWatcher.js';43import { registerMcpHotReload } from './config/hot-reload.js';44import { LspConfigWatcher } from './config/lsp-config-watcher.js';45import { initializeI18n, resolveLanguageSetting } from './i18n/index.js';46import {47  setupStartupWorktree,48  persistStartupWorktreeSidecar,49  buildStartupWorktreeNotice,50  type StartupWorktreeContext,51} from './startup/worktreeStartup.js';52import { startEarlyStartupPrefetches } from './startup/startup-prefetch.js';53import {54  cleanupCheckpoints,55  registerCleanup,56  runExitCleanup,57} from './utils/cleanup.js';58import { AppEvent, appEvents } from './utils/events.js';59import { readStdin } from './utils/readStdin.js';60import {61  profileCheckpoint,62  recordStartupEvent,63  setInteractiveMode,64  finalizeStartupProfile,65  isStartupProfilerEnabled,66} from './utils/startupProfiler.js';67import {68  relaunchAppInChildProcess,69  relaunchOnExitCode,70} from './utils/relaunch.js';71import { start_sandbox } from './utils/sandbox.js';72import { getStartupWarnings } from './utils/startupWarnings.js';73import { getUserStartupWarnings } from './utils/userStartupWarnings.js';74import { initializeWarningHandler } from './utils/warningHandler.js';75import { writeStderrLine } from './utils/stdioHelpers.js';76import { getHeadlessYoloSafetyWarning } from './utils/headlessSafetyWarnings.js';77import { initializeLlmOutputLanguage } from './utils/languageUtils.js';78 79const debugLogger = createDebugLogger('STARTUP');80 81function clearCorruptionEnvVars(): void {82  delete process.env[ENV_CORRUPTED_PATH];83  delete process.env[ENV_WAS_RECOVERED];84}85 86export function validateDnsResolutionOrder(87  order: string | undefined,88): DnsResolutionOrder {89  const defaultValue: DnsResolutionOrder = 'ipv4first';90  if (order === undefined) {91    return defaultValue;92  }93  if (order === 'ipv4first' || order === 'verbatim') {94    return order;95  }96  // We don't want to throw here, just warn and use the default.97  writeStderrLine(98    `Invalid value for dnsResolutionOrder in settings: "${order}". Using default "${defaultValue}".`,99  );100  return defaultValue;101}102 103function getNodeMemoryArgs(isDebugMode: boolean): string[] {104  const totalMemoryMB = os.totalmem() / (1024 * 1024);105  const heapStats = v8.getHeapStatistics();106  const currentMaxOldSpaceSizeMb = Math.floor(107    heapStats.heap_size_limit / 1024 / 1024,108  );109 110  // Set target to 50% of total memory111  const targetMaxOldSpaceSizeInMB = Math.floor(totalMemoryMB * 0.5);112  if (isDebugMode) {113    writeStderrLine(114      `Current heap size ${currentMaxOldSpaceSizeMb.toFixed(2)} MB`,115    );116  }117 118  if (process.env['QWEN_CODE_NO_RELAUNCH']) {119    return [];120  }121 122  if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) {123    if (isDebugMode) {124      writeStderrLine(125        `Need to relaunch with more memory: ${targetMaxOldSpaceSizeInMB.toFixed(2)} MB`,126      );127    }128    return [`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`];129  }130 131  return [];132}133 134import { loadSandboxConfig } from './config/sandboxConfig.js';135 136export function setupUnhandledRejectionHandler() {137  let unhandledRejectionOccurred = false;138  process.on('unhandledRejection', (reason, _promise) => {139    const errorMessage = `=========================================140This is an unexpected error. Please file a bug report using the /bug tool.141CRITICAL: Unhandled Promise Rejection!142=========================================143Reason: ${reason}${144      reason instanceof Error && reason.stack145        ? `146Stack trace:147${reason.stack}`148        : ''149    }`;150    appEvents.emit(AppEvent.LogError, errorMessage);151    if (!unhandledRejectionOccurred) {152      unhandledRejectionOccurred = true;153      appEvents.emit(AppEvent.OpenDebugConsole);154    }155  });156}157 158function getSignalExitCode(signal: NodeJS.Signals): number {159  return signal === 'SIGINT' ? 130 : 143;160}161 162function installInteractiveSignalHandlers(wasRaw: boolean): () => void {163  let cleanupStarted = false;164 165  const handleSignal = (signal: NodeJS.Signals) => {166    if (process.stdin.isTTY) {167      process.stdin.setRawMode(wasRaw);168    }169 170    if (cleanupStarted) {171      return;172    }173    cleanupStarted = true;174 175    void runExitCleanup()176      .catch((error) => {177        debugLogger.error(`Error during ${signal} cleanup:`, error);178      })179      .finally(() => {180        process.exit(getSignalExitCode(signal));181      });182  };183 184  const handleSigterm = () => {185    handleSignal('SIGTERM');186  };187  const handleSigint = () => {188    handleSignal('SIGINT');189  };190 191  process.once('SIGTERM', handleSigterm);192  process.once('SIGINT', handleSigint);193 194  return () => {195    process.removeListener('SIGTERM', handleSigterm);196    process.removeListener('SIGINT', handleSigint);197  };198}199 200export async function main() {201  profileCheckpoint('main_entry');202  // Bridge core-package startup events (Config.initialize, MCP discovery,203  // GeminiClient.setTools) into the cli's startup profiler. Gated on204  // `isStartupProfilerEnabled()` so that when QWEN_CODE_PROFILE_STARTUP is205  // unset (the common case) every core-side `recordStartupEvent()` call206  // sees a null sink and short-circuits at the first comparison, instead207  // of going through this arrow wrapper and the profiler's own enabled208  // check.209  if (isStartupProfilerEnabled()) {210    setStartupEventSink((name, attrs) => recordStartupEvent(name, attrs));211  }212  setupUnhandledRejectionHandler();213  initializeWarningHandler();214 215  if (process.argv.includes('--bare')) {216    process.env[QWEN_CODE_SIMPLE_ENV_VAR] = '1';217  }218 219  // Run before yargs parses subcommands — handlers like `channel status`/`stop`220  // call `process.exit` before `loadSettings()` would otherwise bootstrap.221  preResolveHomeEnvOverrides();222 223  let argv = await parseArguments();224  profileCheckpoint('after_parse_arguments');225 226  if (isBareMode(argv.bare)) {227    process.env[QWEN_CODE_SIMPLE_ENV_VAR] = '1';228  }229 230  // Load user settings — bare mode uses minimal config, normal mode loads full.231  const settings = isBareMode(argv.bare)232    ? createMinimalSettings()233    : loadSettings();234 235  // Propagate corruption state to child process via env vars so236  // relaunchAppInChildProcess() doesn't lose the marker.237  if (settings.corruptedPath) {238    process.env[ENV_CORRUPTED_PATH] = settings.corruptedPath;239    process.env[ENV_WAS_RECOVERED] = settings.wasRecovered ? '1' : '0';240  }241  await cleanupCheckpoints();242  // Performance checkpoint243  profileCheckpoint('after_load_settings');244 245  // Emit settings warnings early so the parent process surfaces them246  // before relaunchAppInChildProcess() exits (the child has empty247  // migrationWarnings because the parent already renamed the file).248  const settingsWarnings = getSettingsWarnings(settings);249  for (const warning of settingsWarnings) {250    writeStderrLine(warning);251  }252  // Corruption notification no longer goes through migrationWarnings —253  // check corruptedPath directly to keep stderr visible in relaunch.254  if (settings.corruptedPath) {255    writeStderrLine(256      'Warning: Settings file had invalid JSON and was reset. ' +257        'A copy of the corrupted file has been saved at: ' +258        settings.corruptedPath,259    );260  }261 262  if (argv.listExtensions) {263    await initializeI18n(264      resolveLanguageSetting(settings.merged.general?.language as string),265    );266    const { handleList: handleListExtensions } = await import(267      './commands/extensions/list.js'268    );269    await handleListExtensions();270    process.exit(0);271  }272 273  // Check for invalid input combinations early to prevent crashes274  if (argv.promptInteractive && !process.stdin.isTTY) {275    writeStderrLine(276      'Error: The --prompt-interactive flag cannot be used when input is piped from stdin.',277    );278    process.exit(1);279  }280 281  const isDebugMode = cliConfig.isDebugMode(argv);282 283  dns.setDefaultResultOrder(284    validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder),285  );286 287  const { themeManager, AUTO_THEME_NAME } = await import(288    './ui/themes/theme-manager.js'289  );290  // Load custom themes from settings291  themeManager.loadCustomThemes(settings.merged.ui?.customThemes);292 293  const configuredTheme = settings.merged.ui?.theme;294  if (configuredTheme && configuredTheme !== AUTO_THEME_NAME) {295    if (!themeManager.setActiveTheme(configuredTheme)) {296      // If the theme is not found during initial load, log a warning and continue.297      // The useThemeCommand hook in AppContainer.tsx will handle opening the dialog.298      writeStderrLine(`Warning: Theme "${configuredTheme}" not found.`);299    }300  } else {301    // 'auto' or unset: resolve a synchronous baseline (COLORFGBG + macOS)302    // so non-interactive runs and any pre-render UI (e.g. the --resume303    // session picker) already have a sensible theme. The interactive304    // startup block refines this with an OSC 11 probe later on, which is305    // intentionally deferred to run inside the early-capture window so306    // terminal response bytes cannot leak into the TUI input.307    themeManager.setActiveTheme(AUTO_THEME_NAME);308  }309 310  // hop into sandbox if we are outside and sandboxing is enabled311  if (!process.env['SANDBOX']) {312    const memoryArgs = settings.merged.advanced?.autoConfigureMemory313      ? getNodeMemoryArgs(isDebugMode)314      : [];315    const sandboxConfig = await loadSandboxConfig(settings.merged, argv);316    // We intentially omit the list of extensions here because extensions317    // should not impact auth or setting up the sandbox.318    // TODO(jacobr): refactor loadCliConfig so there is a minimal version319    // that only initializes enough config to enable refreshAuth or find320    // another way to decouple refreshAuth from requiring a config.321 322    if (sandboxConfig) {323      const partialConfig = await loadCliConfig(324        settings.merged,325        argv,326        undefined,327        [],328        // Pass separated hooks for proper source attribution329        {330          userHooks: settings.getUserHooks(),331          projectHooks: settings.getProjectHooks(),332        },333        buildDisabledSkillNamesProvider(settings),334      );335 336      if (!settings.merged.security?.auth?.useExternal) {337        // Validate authentication here because the sandbox will interfere with the Oauth2 web redirect.338        try {339          const authType = partialConfig.getModelsConfig().getCurrentAuthType();340          // Fresh users may not have selected/persisted an authType yet.341          // In that case, defer auth prompting/selection to the main interactive flow.342          if (authType) {343            const err = validateAuthMethod(authType, partialConfig);344            if (err) {345              throw new Error(err);346            }347 348            await partialConfig.refreshAuth(authType);349          }350        } catch (err) {351          writeStderrLine(`Error authenticating: ${err}`);352          process.exit(1);353        }354      }355      // For stream-json and ACP modes, don't read stdin here — stdin carries356      // protocol data (not a user prompt) and should be forwarded to the sandbox357      // intact via stdio: 'inherit'.358      const inputFormat = argv.inputFormat as string | undefined;359      const isAcpMode = argv.acp || argv.experimentalAcp;360      let stdinData = '';361      if (!process.stdin.isTTY && inputFormat !== 'stream-json' && !isAcpMode) {362        stdinData = await readStdin();363      }364 365      // This function is a copy of the one from sandbox.ts366      // It is moved here to decouple sandbox.ts from the CLI's argument structure.367      const injectStdinIntoArgs = (368        args: string[],369        stdinData?: string,370      ): string[] => {371        const finalArgs = [...args];372        if (stdinData) {373          const promptIndex = finalArgs.findIndex(374            (arg) => arg === '--prompt' || arg === '-p',375          );376          if (promptIndex > -1 && finalArgs.length > promptIndex + 1) {377            // If there's a prompt argument, prepend stdin to it378            finalArgs[promptIndex + 1] =379              `${stdinData}\n\n${finalArgs[promptIndex + 1]}`;380          } else {381            // If there's no prompt argument, add stdin as the prompt382            finalArgs.push('--prompt', stdinData);383          }384        }385        return finalArgs;386      };387 388      const injectSandboxSessionIdIntoArgs = (389        args: string[],390        sessionId: string,391      ): string[] => {392        const separatorIndex = args.indexOf('--');393        const cliArgs =394          separatorIndex < 0 ? args : args.slice(0, separatorIndex);395        const hasArg = (names: string[]) =>396          cliArgs.some((arg) =>397            names.some((name) => arg === name || arg.startsWith(`${name}=`)),398          );399        if (400          hasArg(['--session-id', '--sandbox-session-id']) ||401          hasArg(['--continue', '-c']) ||402          hasArg(['--resume', '-r'])403        ) {404          return args;405        }406 407        const sessionArgs = ['--sandbox-session-id', sessionId];408        if (separatorIndex < 0) {409          return [...args, ...sessionArgs];410        }411 412        return [...cliArgs, ...sessionArgs, ...args.slice(separatorIndex)];413      };414 415      const sessionId = partialConfig.getSessionId();416      const sandboxArgs = sessionId417        ? injectSandboxSessionIdIntoArgs(418            injectStdinIntoArgs(process.argv, stdinData),419            sessionId,420          )421        : injectStdinIntoArgs(process.argv, stdinData);422 423      await relaunchOnExitCode(() =>424        start_sandbox(sandboxConfig, memoryArgs, partialConfig, sandboxArgs),425      );426      process.exit(0);427    } else {428      // Relaunch app so we always have a child process that can be internally429      // restarted if needed.430      await relaunchAppInChildProcess(memoryArgs, [], {431        afterSpawn: clearCorruptionEnvVars,432      });433    }434  }435 436  // When --worktree is going to chdir us into a worktree below, resolve437  // any relative-path argv fields to absolute paths now — BEFORE the438  // chdir. Otherwise downstream `fs.existsSync('./mcp.json')` calls in439  // `loadCliConfig` re-resolve against the worktree dir, where the file440  // doesn't exist. Only touches values that look like paths (mcpConfig441  // also accepts inline JSON — skip those).442  //443  // The list of fields below is hand-maintained. If you add a new444  // CLI flag that takes a relative path, register it here too,445  // otherwise --worktree silently breaks for that flag.446  if (argv.worktree !== undefined) {447    const launchCwdForPaths = process.cwd();448    const looksLikeInlineJson = (v: string): boolean => {449      const t = v.trim();450      return t.startsWith('{') || t.startsWith('[');451    };452    const resolveIfPath = (v: string | undefined): string | undefined => {453      if (typeof v !== 'string' || v.length === 0) return v;454      if (looksLikeInlineJson(v)) return v;455      return path.resolve(launchCwdForPaths, v);456    };457    argv.mcpConfig = resolveIfPath(argv.mcpConfig);458    argv.openaiLoggingDir = resolveIfPath(argv.openaiLoggingDir);459    argv.jsonFile = resolveIfPath(argv.jsonFile);460    argv.inputFile = resolveIfPath(argv.inputFile);461    argv.telemetryOutfile = resolveIfPath(argv.telemetryOutfile);462    if (Array.isArray(argv.includeDirectories)) {463      argv.includeDirectories = argv.includeDirectories.map((d) =>464        typeof d === 'string' && d.length > 0465          ? path.resolve(launchCwdForPaths, d)466          : d,467      );468    }469    // `--json-schema` accepts either an inline schema or `@<path>`. The470    // `@`-prefixed form is read from disk inside `resolveJsonSchemaArg`471    // (`packages/cli/src/config/config.ts`), AFTER chdir, so a relative472    // value would resolve against the worktree — fix the prefix path473    // here.474    if (typeof argv.jsonSchema === 'string') {475      const trimmedSchema = argv.jsonSchema.trim();476      if (trimmedSchema.startsWith('@')) {477        const rel = trimmedSchema.slice(1);478        if (rel.length > 0 && !path.isAbsolute(rel)) {479          argv.jsonSchema = '@' + path.resolve(launchCwdForPaths, rel);480        }481      }482    }483  }484 485  // Phase D-1: process --worktree before the resume picker so the picker486  // (which uses process.cwd() to scope its session search) finds sessions487  // saved inside the target worktree. Creates the worktree directory on488  // disk and chdirs into it; on failure we emit to stderr and exit before489  // any expensive initialization runs.490  //491  // ACP mode is exempt: the ACP host (Zed, etc.) supplies its own per-session492  // cwd, and the startup-level chdir would not propagate. Reject the493  // combination with a clear error rather than silently dropping --worktree.494  let startupWorktreeContext: StartupWorktreeContext | null = null;495  if (argv.worktree !== undefined && (argv.acp || argv.experimentalAcp)) {496    writeStderrLine(497      '--worktree cannot be combined with --acp / --experimental-acp. ' +498        'Pass the worktree path as the cwd of the ACP loadSession / newSession ' +499        'request instead.',500    );501    process.exit(1);502  }503  {504    const startupRes = await setupStartupWorktree(argv.worktree, {505      symlinkDirectories: settings.merged.worktree?.symlinkDirectories,506    });507    if (startupRes !== null) {508      if (!startupRes.ok) {509        writeStderrLine(startupRes.error);510        process.exit(1);511      }512      startupWorktreeContext = startupRes.context;513    }514  }515 516  // Handle --resume without a session ID, or with a custom title, by showing517  // the session picker. Set the runtime output dir early so the picker can find518  // sessions stored under a custom runtimeOutputDir (setRuntimeBaseDir is519  // idempotent and will be called again inside loadCliConfig).520  if (argv.resume !== undefined) {521    Storage.setRuntimeBaseDir(522      settings.merged.advanced?.runtimeOutputDir,523      process.cwd(),524    );525 526    let resolvedSessionId: string | undefined;527 528    if (argv.resume === '') {529      // No argument — show picker530      const { showResumeSessionPicker } = await import(531        './ui/components/StandaloneSessionPicker.js'532      );533      resolvedSessionId = await showResumeSessionPicker();534    } else if (!cliConfig.isValidSessionId(argv.resume)) {535      // Non-UUID argument — treat as custom title search536      const sessionService = new SessionService(process.cwd());537      const matches = await sessionService.findSessionsByTitle(argv.resume);538      if (matches.length === 1) {539        resolvedSessionId = matches[0].sessionId;540      } else if (matches.length > 1) {541        // Multiple matches — show picker to let user choose542        writeStderrLine(543          `Multiple sessions found with title "${argv.resume}". Please select one:`,544        );545        const { showResumeSessionPicker } = await import(546          './ui/components/StandaloneSessionPicker.js'547        );548        resolvedSessionId = await showResumeSessionPicker(549          process.cwd(),550          matches,551        );552      }553      // matches.length === 0 → resolvedSessionId stays undefined, handled below554    }555 556    if (resolvedSessionId !== undefined) {557      argv = { ...argv, resume: resolvedSessionId };558    } else if (argv.resume === '' || !cliConfig.isValidSessionId(argv.resume)) {559      // User cancelled the picker or no sessions found for the title560      if (argv.resume !== '') {561        writeStderrLine(`No saved session found with title "${argv.resume}".`);562        process.exit(1);563      } else {564        process.exit(0);565      }566    }567    // else: argv.resume is already a valid UUID, pass through to loadCliConfig568  }569 570  // We are now past the logic handling potentially launching a child process571  // to run Qwen Code. It is now safe to perform expensive initialization that572  // may have side effects.573  profileCheckpoint('after_sandbox_check');574 575  // Initialize output language file before config loads to ensure it's included in context576  if (!isBareMode(argv.bare)) {577    initializeLlmOutputLanguage(settings.merged.general?.outputLanguage);578  }579 580  {581    // Start settings file watcher (skip in bare mode)582    const settingsWatcher = isBareMode(argv.bare)583      ? undefined584      : new SettingsWatcher(settings);585    settingsWatcher?.startWatching();586 587    const config = await loadCliConfig(588      settings.merged,589      argv,590      process.cwd(),591      argv.extensions,592      // Pass separated hooks for proper source attribution593      {594        userHooks: settings.getUserHooks(),595        projectHooks: settings.getProjectHooks(),596      },597      buildDisabledSkillNamesProvider(settings),598      undefined,599      settingsWatcher,600    );601    profileCheckpoint('after_load_cli_config');602 603    // Subscribe the running Config to settings changes so MCP servers604    // reconnect / disconnect / restart without a session restart (#3696,605    // sub-task 3). Skipped in bare mode (no watcher).606    if (settingsWatcher) {607      const disposeMcpHotReload = registerMcpHotReload(608        settingsWatcher,609        settings,610        config,611        config.getTopTierMcpServers(),612      );613      registerCleanup(disposeMcpHotReload);614    }615 616    registerLspHotReload(config, registerCleanup);617 618    // Phase D-1: persist the WorktreeSession sidecar so Phase C's restore619    // machinery on a subsequent `--resume` picks the worktree back up, and620    // capture any override of a previously-resumed session's worktree so621    // we can emit a one-shot notice on the model's first prompt.622    //623    // The notice is set BEFORE the persist attempt and AGAIN inside the624    // try block (so the override addendum can be appended on success).625    // A persist failure must NOT silently drop the notice — the cwd is626    // already switched, and the model needs to know which worktree it's627    // operating in regardless of whether the sidecar landed.628    if (startupWorktreeContext) {629      config.setPendingStartupWorktreeNotice(630        buildStartupWorktreeNotice(startupWorktreeContext),631      );632      try {633        const startupWorktreePersist = await persistStartupWorktreeSidecar(634          config,635          startupWorktreeContext,636        );637        if (startupWorktreePersist.overrodeResumedWorktree) {638          writeStderrLine(639            `--worktree overrode the resumed session's previous worktree ` +640              `"${startupWorktreePersist.overriddenSlug ?? '(unknown)'}". ` +641              `That worktree directory was left intact on disk.`,642          );643        }644        // Refresh the notice with the override addendum (if any). When645        // there is no override this is a no-op text-wise; on override it646        // gives the model the "you overrode <previous-slug>" hint. TUI647        // and headless consume this via Config.consumePendingStartupWorktreeNotice();648        // ACP is excluded above (`--worktree` × `--acp` is mutually649        // exclusive — see the mutex check earlier in this function).650        config.setPendingStartupWorktreeNotice(651          buildStartupWorktreeNotice(652            startupWorktreeContext,653            startupWorktreePersist,654          ),655        );656      } catch (error) {657        debugLogger.warn(658          `--worktree sidecar persist failed (non-fatal, notice preserved): ${error instanceof Error ? error.message : String(error)}`,659        );660      }661    }662 663    // Persist session usage for cross-session reports (must run before664    // config.shutdown() which clears telemetry state).665    // sessionStartTime is read from uiTelemetryService so it stays correct666    // after /clear resets the session (reset() updates the internal timestamp).667    registerCleanup(() => {668      try {669        const metrics = uiTelemetryService.getMetrics();670        const hasActivity = Object.values(metrics.models).some(671          (m) => m.api.totalRequests > 0,672        );673        if (!hasActivity) return;674        persistSessionUsage({675          sessionId: config.getSessionId(),676          startTime: uiTelemetryService.getSessionStartTime(),677          endTime: new Date(),678          project: config.getProjectRoot(),679          metrics,680        });681      } catch {682        // Best-effort — don't block shutdown683      }684    });685 686    // Register cleanup for MCP clients as early as possible687    // This ensures MCP server subprocesses are properly terminated on exit688    registerCleanup(() => config.shutdown());689 690    startEarlyStartupPrefetches(config);691 692    const wasRaw = process.stdin.isRaw;693    let kittyProtocolDetectionComplete: Promise<boolean> | undefined;694    let themeAutoDetectionComplete: Promise<void> | undefined;695    if (config.isInteractive()) {696      registerCleanup(installInteractiveSignalHandlers(wasRaw));697    }698    if (config.isInteractive() && !wasRaw && process.stdin.isTTY) {699      const { startEarlyInputCapture, stopAndGetCapturedInput } = await import(700        './utils/earlyInputCapture.js'701      );702      const { detectAndEnableKittyProtocol } = await import(703        './ui/utils/kittyProtocolDetector.js'704      );705      // Set this as early as possible to avoid spurious characters from706      // input showing up in the output.707      process.stdin.setRawMode(true);708 709      // Startup optimization: start early input capture710      startEarlyInputCapture();711      // Ensure the stdin listener is removed on any exit path (error, signal, etc.)712      registerCleanup(() => stopAndGetCapturedInput());713 714      // Detect and enable Kitty keyboard protocol once at startup.715      kittyProtocolDetectionComplete = detectAndEnableKittyProtocol();716 717      // Auto-detect theme (OSC 11 + COLORFGBG + macOS) when the user has718      // opted into 'auto' or has not configured a theme at all. Kicked off719      // here without awaiting so the OSC 11 timeout overlaps with the720      // heavier startup work below (initializeApp, warnings) instead of721      // blocking the critical path. The synchronous baseline picked above722      // keeps the active theme valid in the meantime; this probe only723      // refines it. Running inside the early-capture window is deliberate:724      // the filter in startEarlyInputCapture absorbs the OSC 11 response725      // bytes so they cannot leak into the TUI input, even though our726      // probe attaches its own listener to parse the RGB value.727      if (!configuredTheme || configuredTheme === AUTO_THEME_NAME) {728        themeAutoDetectionComplete = themeManager729          .resolveAutoThemeAsync()730          .catch((err) => {731            debugLogger.warn('Async theme auto-detection failed:', err);732          });733      }734    }735 736    if (config.isInteractive()) {737      const { setMaxSizedBoxDebugging } = await import(738        './ui/components/shared/MaxSizedBox.js'739      );740      setMaxSizedBoxDebugging(isDebugMode);741    }742 743    // Check input format early to determine initialization flow744    // In TTY mode, ignore stream-json input format to prevent process from hanging745    const inputFormat = process.stdin.isTTY746      ? InputFormat.TEXT747      : typeof config.getInputFormat === 'function'748        ? config.getInputFormat()749        : InputFormat.TEXT;750 751    // For stream-json mode, defer config.initialize() until after the initialize control request752    // For other modes, initialize normally753    const { initializeApp } = await import('./core/initializer.js');754    let input = config.getQuestion();755    const hasRemoteInput = Boolean(config.getInputFile?.());756    const deferIdeConnection =757      config.isInteractive() &&758      !config.getExperimentalZedIntegration() &&759      !input &&760      !hasRemoteInput;761    const initializationResult = await initializeApp(config, settings, {762      deferIdeConnection,763    });764    profileCheckpoint('after_initialize_app');765 766    if (config.getExperimentalZedIntegration()) {767      const { runAcpAgent } = await import('./acp-integration/acpAgent.js');768      await runAcpAgent(config, settings, argv);769      // Clean up child processes and force exit, matching other non-interactive modes770      await runExitCleanup();771      process.exit(0);772    }773 774    const startupWarnings = [775      ...new Set([776        ...(config.isSafeMode()777          ? [778              '⚠ SAFE MODE — all customizations disabled (hooks, extensions, skills, MCP servers, QWEN.md). Restart without --safe-mode to resume normal operation.',779            ]780          : []),781        ...(await getStartupWarnings()),782        ...(await getUserStartupWarnings({783          workspaceRoot: process.cwd(),784          useRipgrep: settings.merged.tools?.useRipgrep ?? true,785          useBuiltinRipgrep: settings.merged.tools?.useBuiltinRipgrep ?? true,786        })),787        ...getSettingsWarnings(settings),788        ...config.getWarnings(),789        ...(config.getModelsConfig().getCurrentAuthType() ===790        AuthType.QWEN_OAUTH791          ? [792              'Qwen OAuth free tier was discontinued on 2026-04-15. Run /auth to switch to Coding Plan or another provider.',793            ]794          : []),795      ]),796    ];797    const emittedStartupWarnings = new Set(startupWarnings);798 799    // Surface critical startup warnings (corrupted settings, recovery, etc.)800    // to stderr so they are visible regardless of UI mode. In interactive801    // mode the TUI's Notifications component also renders them, but the802    // onboarding flow can obscure the notification area, leaving users803    // unaware that their settings were reset. Writing to stderr before804    // the TUI takes over ensures the message is visible in the terminal805    // scrollback. In non-interactive mode this is the *only* channel.806    for (const warning of startupWarnings) {807      writeStderrLine(warning);808    }809 810    // Render UI, passing necessary config values. Check that there is no command line question.811    profileCheckpoint('before_render');812 813    if (config.isInteractive()) {814      // --json-schema is a headless-only contract: the synthetic815      // structured_output tool only terminates the run inside816      // runNonInteractive's main/drain loops. In TUI mode the same call817      // would just emit "Structured output accepted." and keep the chat818      // alive, which silently strands the user's run. Parse-time gating819      // can't catch this case (`qwen --json-schema '...'` on a TTY with820      // no prompt routes to interactive only after stdin TTY detection),821      // so reject here before the UI launches.822      if (config.getJsonSchema?.()) {823        writeStderrLine(824          'Error: --json-schema is a headless-only flag. Provide a one-shot prompt via -p / --prompt or pipe one in via stdin.',825        );826        // Run cleanup so MCP subprocesses + telemetry exporters that the827        // earlier initializeApp() / loadCliConfig() registered get shut828        // down — process.exit() doesn't drain them on its own.829        await runExitCleanup();830        process.exit(1);831      }832      // For the interactive path, the profile is finalized by AppContainer833      // after `config.initialize()` and `input_enabled` are recorded — that's834      // the only way `first_paint`, `config_initialize_*`, `input_enabled`,835      // and the MCP events are captured. See AppContainer's mount effect.836      setInteractiveMode(true);837      // Need kitty detection to be complete before we can start the interactive UI.838      await kittyProtocolDetectionComplete;839      // Drain the auto-theme probe before render so the OSC 11 response is840      // absorbed by the early-capture filter (which is closed inside841      // startInteractiveUI) and so the first paint uses the refined theme842      // when the probe finishes in time.843      await themeAutoDetectionComplete;844      const { startInteractiveUI } = await import('./ui/startInteractiveUI.js');845      await startInteractiveUI(846        config,847        settings,848        startupWarnings,849        process.cwd(),850        initializationResult!,851        {852          postRenderConnectIde: deferIdeConnection,853        },854      );855      // Clean up corruption env vars so subsequent relaunch children856      // and subprocesses don't inherit stale state.857      clearCorruptionEnvVars();858      return;859    }860 861    // Also clean up env vars for non-interactive paths so that862    // subprocesses don't inherit stale state.863    clearCorruptionEnvVars();864 865    // Non-interactive: defer finalize until after `config.initialize()` runs866    // so MCP discovery events (mcp_first_tool_registered, mcp_all_servers_settled,867    // gemini_tools_updated) are captured in the profile.868 869    // Print debug mode notice to stderr for non-interactive mode870    if (config.getDebugMode()) {871      writeStderrLine('Debug mode enabled');872      writeStderrLine(873        `Logging to: ${Storage.getDebugLogPath(config.getSessionId())}`,874      );875      if (isDebugLoggingDegraded()) {876        writeStderrLine(877          'Warning: Debug logging is degraded (write failures occurred)',878        );879      }880    }881 882    // Headless + YOLO without a sandbox lets the model auto-approve and883    // execute shell / write / edit tools at the current process's884    // privilege level. Emit a one-line stderr warning so unattended runs885    // have at least an observable signal. Interactive runs are excluded886    // because the user is at the keyboard and the TUI shows approval887    // state directly. See issue #4103.888    if (!config.isInteractive()) {889      const yoloWarning = getHeadlessYoloSafetyWarning(config);890      if (yoloWarning) writeStderrLine(yoloWarning);891    }892 893    // For non-stream-json mode, initialize config here. Stream-json defers894    // `config.initialize()` to inside `Session.ensureConfigInitialized`895    // because the initial control_request may register SDK MCP servers896    // that must be in place before discovery runs (see session.ts).897    if (inputFormat !== InputFormat.STREAM_JSON) {898      profileCheckpoint('config_initialize_start');899      await config.initialize();900      for (const warning of config.getWarnings()) {901        if (emittedStartupWarnings.has(warning)) continue;902        emittedStartupWarnings.add(warning);903        writeStderrLine(warning);904      }905      profileCheckpoint('config_initialize_end');906 907      // Non-interactive paths feed a prompt to the model immediately after908      // init. Under PR-A's progressive MCP availability,909      // `config.initialize()` returns BEFORE MCP servers settle, so910      // without this wait the first sendMessage would see only built-in911      // tools — a silent regression versus the legacy synchronous912      // behavior. Interactive paths skip this (AppContainer's batch-flush913      // subscriber updates the tool list as MCP servers come online).914      await config.waitForMcpReady();915      // Surface MCP server failures on stderr so non-interactive runs916      // (--prompt / piped stdin / scripts) don't silently regress to917      // built-in-tools-only when a server cannot connect. The legacy918      // synchronous MCP path was visibly noisy on failures because919      // per-server errors logged to stderr during the blocking920      // `discoverAllMcpTools` call; PR-A moves discovery to a921      // background promise whose per-server errors are caught inside922      // `discoverAllMcpToolsIncremental` and never reach a TTY. This923      // helper closes that gap without re-introducing blocking.924      // Defensive against tests that pass a stubbed Config without925      // `getFailedMcpServerNames` — the warning is best-effort visibility926      // and never gates startup.927      const failedMcpServers =928        typeof config.getFailedMcpServerNames === 'function'929          ? config.getFailedMcpServerNames()930          : [];931      if (failedMcpServers.length > 0) {932        writeStderrLine(933          `Warning: MCP server(s) failed to start: ${failedMcpServers.join(', ')}. ` +934            `Continuing with built-in tools and any servers that did connect. ` +935            `Re-run with QWEN_CODE_DEBUG=1 to see per-server reasons.`,936        );937      }938      // Finalize the non-interactive startup profile here so MCP events939      // emitted during initialize() / waitForMcpReady() are captured.940      // Subsequent stdin reads / auth checks / prompt execution are not941      // part of the "first-screen" budget.942      //943      // For stream-json we deliberately do NOT finalize here: the profile944      // is finalized inside Session.ensureConfigInitialized() after MCP945      // settles, so its `config_initialize_*` and MCP events make it into946      // the file. Finalizing here would write an empty profile and the947      // module-level `finalized` guard would suppress every subsequent948      // event.949      finalizeStartupProfile(config.getSessionId());950    }951 952    // Only read stdin if NOT in stream-json mode953    // In stream-json mode, stdin is used for protocol messages (control requests, etc.)954    // and should be consumed by StreamJsonInputReader instead955    if (inputFormat !== InputFormat.STREAM_JSON && !process.stdin.isTTY) {956      const stdinData = await readStdin();957      if (stdinData) {958        input = `${stdinData}\n\n${input}`;959      }960    }961 962    const { validateNonInteractiveAuth } = await import(963      './validateNonInterActiveAuth.js'964    );965    const nonInteractiveConfig = await validateNonInteractiveAuth(966      settings.merged.security?.auth?.useExternal,967      config,968      settings,969    );970 971    const prompt_id = createNonInteractivePromptId(config.getSessionId());972 973    if (inputFormat === InputFormat.STREAM_JSON) {974      const trimmedInput = (input ?? '').trim();975      const { runNonInteractiveStreamJson } = await import(976        './nonInteractive/session.js'977      );978 979      await runNonInteractiveStreamJson(980        nonInteractiveConfig,981        trimmedInput.length > 0 ? trimmedInput : '',982        settings,983      );984      await runExitCleanup();985      // `runNonInteractiveStreamJson` doesn't return an explicit exit986      // code yet, so a cleanup task that mutates `process.exitCode`987      // could clobber a non-zero failure signal. This is currently safe988      // because `--json-schema` is rejected at parse time when combined989      // with `--input-format stream-json` (see the yargs `.check` in990      // resolveCliGenerationConfig), so structured-output failures991      // never reach this branch. If a future stream-json equivalent of992      // structured output is added, plumb the exit code through the993      // function's return value the way `runNonInteractive` below does.994      process.exit(process.exitCode ?? 0);995    }996 997    if (!input) {998      writeStderrLine(999        `No input provided via stdin. Input can be provided by piping data into gemini or using the --prompt option.`,1000      );1001      process.exit(1);1002    }1003 1004    logUserPrompt(config, {1005      'event.name': 'user_prompt',1006      'event.timestamp': new Date().toISOString(),1007      prompt: input,1008      prompt_id,1009      auth_type: config.getContentGeneratorConfig()?.authType,1010      prompt_length: input.length,1011    });1012 1013    debugLogger.debug(`Session ID: ${config.getSessionId()}`);1014 1015    const { runNonInteractive } = await import('./nonInteractiveCli.js');1016    const exitCode = await runNonInteractive(1017      nonInteractiveConfig,1018      settings,1019      input,1020      prompt_id,1021    );1022    // Call cleanup before process.exit, which causes cleanup to not run.1023    // Capture the exit code BEFORE cleanup so any cleanup task that1024    // mutates process.exitCode can't silently turn a structured-output1025    // failure (or other explicit non-zero return from runNonInteractive)1026    // into a zero exit.1027    await runExitCleanup();1028    process.exit(exitCode);1029  }1030}1031 1032export function createNonInteractivePromptId(sessionId: string): string {1033  return `${sessionId}########0`;1034}1035 1036/**1037 * Watches `.lsp.json` for changes and reconciles running LSP servers1038 * (add / remove / restart) without requiring a session restart.1039 *1040 * Silently no-ops when LSP is disabled or the active client does not1041 * support runtime reinitialization.1042 *1043 * Emits {@link AppEvent.LspStatusChanged} after every successful reload1044 * so the UI can reflect the new server state.1045 */1046export function registerLspHotReload(1047  config: Config,1048  registerCleanup: (fn: () => void | Promise<void>) => void,1049): void {1050  if (1051    config.isLspEnabled?.() !== true ||1052    !config.getLspClient?.()?.reinitialize1053  ) {1054    return;1055  }1056  const lspConfigWatcher = new LspConfigWatcher(config.getProjectRoot());1057  debugLogger.info(1058    `Registering LSP config hot reload watcher for ${config.getProjectRoot()}`,1059  );1060  lspConfigWatcher.startWatching(async (event) => {1061    if (event.changeType === 'invalid') {1062      debugLogger.warn(`Invalid LSP config file ${event.path}: ${event.error}`);1063      appEvents.emit(AppEvent.LogError, event.error);1064      return;1065    }1066    debugLogger.info(1067      `Reloading LSP server settings: changeType=${event.changeType}, path=${event.path}`,1068    );1069    let errorReported = false;1070    try {1071      const result = await config.reinitializeLsp();1072      if (result) {1073        const failedServers = getRuntimeReloadFailedNames(result.reconcile);1074        debugLogger.info(1075          `Reloaded LSP server settings: added=${formatRuntimeReloadNames(1076            result.reconcile.added,1077          )}, removed=${formatRuntimeReloadNames(1078            result.reconcile.removed,1079          )}, restarted=${formatRuntimeReloadNames(1080            result.reconcile.restarted,1081          )}, unchanged=${formatRuntimeReloadNames(1082            result.reconcile.unchanged,1083          )}, failed=${formatRuntimeReloadNames(1084            failedServers,1085          )}, skipped=${formatRuntimeReloadNames(1086            result.skipped.map((server) => server.name),1087          )}`,1088        );1089        if (failedServers.length > 0) {1090          appEvents.emit(AppEvent.LspStatusChanged);1091          const changedServers = [1092            ...result.reconcile.added,1093            ...result.reconcile.removed,1094            ...result.reconcile.restarted,1095          ];1096          const message = `LSP reload partially completed: changed=${formatRuntimeReloadNames(1097            changedServers,1098          )}, failed=${formatRuntimeReloadNames(1099            failedServers,1100          )}. Run with --debug for details.`;1101          appEvents.emit(AppEvent.LogError, message);1102          errorReported = true;1103          throw new Error(message);1104        }1105      } else {1106        debugLogger.info(1107          'Skipped LSP server settings reload because LSP is disabled or no client is available',1108        );1109      }1110      appEvents.emit(AppEvent.LspStatusChanged);1111    } catch (error) {1112      debugLogger.warn('Failed to reload LSP server settings:', error);1113      if (!errorReported) {1114        const message =1115          error instanceof Error1116            ? `Failed to reload LSP server settings: ${error.message}. Some LSP servers may have been partially updated. Run with --debug for details.`1117            : 'Failed to reload LSP server settings; some LSP servers may have been partially updated. Run with --debug for details.';1118        appEvents.emit(AppEvent.LogError, message);1119      }1120      throw error;1121    }1122  });1123  registerCleanup(() => lspConfigWatcher.stopWatching());1124}1125 1126function formatRuntimeReloadNames(names: readonly string[]): string {1127  return names.length === 0 ? '<none>' : names.join(',');1128}1129 1130/**1131 * Reads the optional failed bucket defensively because the CLI may typecheck1132 * against stale core dist declarations during local development.1133 */1134function getRuntimeReloadFailedNames(reconcile: {1135  failed?: readonly string[];1136}): readonly string[] {1137  return reconcile.failed ?? [];1138}1139 
basant307/AI_Governance_Project · CoolFace