diff --git a/packages/cli/src/acp-integration/acp.ts b/packages/cli/src/acp-integration/acp.ts index 9725cdb01..c4ed85e90 100644 --- a/packages/cli/src/acp-integration/acp.ts +++ b/packages/cli/src/acp-integration/acp.ts @@ -7,12 +7,15 @@ /* ACP defines a schema for a simple (experimental) JSON-RPC protocol that allows GUI applications to interact with agents. */ import { z } from 'zod'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; import * as schema from './schema.js'; import { ACP_ERROR_CODES } from './errorCodes.js'; export * from './schema.js'; import type { WritableStream, ReadableStream } from 'node:stream/web'; +const debugLogger = createDebugLogger('ACP_PROTOCOL'); + export class AgentSideConnection implements Client { #connection: Connection; @@ -328,7 +331,7 @@ class Connection { }) .catch((error) => { // Continue processing writes on error - console.error('ACP write error:', error); + debugLogger.error('ACP write error:', error); }); return this.#writeQueue; } diff --git a/packages/cli/src/acp-integration/acpAgent.ts b/packages/cli/src/acp-integration/acpAgent.ts index 6c40bffee..96d0157a2 100644 --- a/packages/cli/src/acp-integration/acpAgent.ts +++ b/packages/cli/src/acp-integration/acpAgent.ts @@ -11,6 +11,7 @@ import { APPROVAL_MODES, AuthType, clearCachedCredentialFile, + createDebugLogger, QwenOAuth2Event, qwenOAuth2Events, MCPServerConfig, @@ -33,6 +34,8 @@ import { loadCliConfig } from '../config/config.js'; // Import the modular Session class import { Session } from './session/Session.js'; +const debugLogger = createDebugLogger('ACP_AGENT'); + export async function runAcpAgent( config: Config, settings: LoadedSettings, @@ -301,7 +304,7 @@ class GeminiAgent { // Use true for the second argument to ensure only cached credentials are used await config.refreshAuth(selectedType, true); } catch (e) { - console.error(`Authentication failed: ${e}`); + debugLogger.error(`Authentication failed: ${e}`); throw acp.RequestError.authRequired( 'Authentication failed: ' + (e as Error).message, ); diff --git a/packages/cli/src/acp-integration/session/Session.ts b/packages/cli/src/acp-integration/session/Session.ts index 5348d78df..462076829 100644 --- a/packages/cli/src/acp-integration/session/Session.ts +++ b/packages/cli/src/acp-integration/session/Session.ts @@ -24,6 +24,7 @@ import { DiscoveredMCPTool, StreamEventType, ToolConfirmationOutcome, + createDebugLogger, logToolCall, logUserPrompt, getErrorStatus, @@ -67,6 +68,8 @@ import { PlanEmitter } from './emitters/PlanEmitter.js'; import { MessageEmitter } from './emitters/MessageEmitter.js'; import { SubAgentTracker } from './SubAgentTracker.js'; +const debugLogger = createDebugLogger('ACP_SESSION'); + /** * Session represents an active conversation session with the AI model. * It uses modular components for consistent event emission: @@ -318,7 +321,7 @@ export class Session implements SessionContext { await this.sendUpdate(update); } catch (error) { // Log error but don't fail session creation - console.error('Error sending available commands update:', error); + debugLogger.error('Error sending available commands update:', error); } } @@ -844,7 +847,7 @@ export class Session implements SessionContext { const reason = respectGitIgnore ? 'git-ignored and will be skipped' : 'ignored by custom patterns'; - console.warn(`Path ${pathName} is ${reason}.`); + debugLogger.warn(`Path ${pathName} is ${reason}.`); continue; } let currentPathSpec = pathName; @@ -911,7 +914,7 @@ export class Session implements SessionContext { ); } } catch (globError) { - console.error( + debugLogger.error( `Error during glob search for ${pathName}: ${getErrorMessage(globError)}`, ); } @@ -921,7 +924,7 @@ export class Session implements SessionContext { ); } } else { - console.error( + debugLogger.error( `Error stating path ${pathName}. Path ${pathName} will be skipped.`, ); } @@ -1028,9 +1031,7 @@ export class Session implements SessionContext { } debug(msg: string): void { - if (this.config.getDebugMode()) { - console.warn(msg); - } + debugLogger.debug(msg); } } diff --git a/packages/cli/src/acp-integration/session/SubAgentTracker.ts b/packages/cli/src/acp-integration/session/SubAgentTracker.ts index 1e745b925..3687bef57 100644 --- a/packages/cli/src/acp-integration/session/SubAgentTracker.ts +++ b/packages/cli/src/acp-integration/session/SubAgentTracker.ts @@ -17,6 +17,7 @@ import type { import { SubAgentEventType, ToolConfirmationOutcome, + createDebugLogger, } from '@qwen-code/qwen-code-core'; import { z } from 'zod'; import type { SessionContext } from './types.js'; @@ -24,6 +25,8 @@ import { ToolCallEmitter } from './emitters/ToolCallEmitter.js'; import { MessageEmitter } from './emitters/MessageEmitter.js'; import type * as acp from '../acp.js'; +const debugLogger = createDebugLogger('ACP_SUBAGENT_TRACKER'); + /** * Permission option kind type matching ACP schema. */ @@ -135,7 +138,7 @@ export class SubAgentTracker { invocation = tool.build(event.args); } catch (e) { // If building fails, continue with defaults - console.warn(`Failed to build subagent tool ${event.name}:`, e); + debugLogger.warn(`Failed to build subagent tool ${event.name}:`, e); } } @@ -250,7 +253,7 @@ export class SubAgentTracker { await event.respond(outcome); } catch (error) { // If permission request fails, cancel the tool call - console.error( + debugLogger.error( `Permission request failed for subagent tool ${event.name}:`, error, ); diff --git a/packages/cli/src/commands/extensions/consent.ts b/packages/cli/src/commands/extensions/consent.ts index c3da6a282..5fd441715 100644 --- a/packages/cli/src/commands/extensions/consent.ts +++ b/packages/cli/src/commands/extensions/consent.ts @@ -7,6 +7,7 @@ import type { import type { ConfirmationRequest } from '../../ui/types.js'; import chalk from 'chalk'; import { t } from '../../i18n/index.js'; +import { writeStdoutLine } from '../../utils/stdioHelpers.js'; /** * Requests consent from the user to perform an action, by reading a Y/n @@ -20,7 +21,7 @@ import { t } from '../../i18n/index.js'; export async function requestConsentNonInteractive( consentDescription: string, ): Promise { - console.info(consentDescription); + writeStdoutLine(consentDescription); const result = await promptForConsentNonInteractive( t('Do you want to continue? [Y/n]: '), ); diff --git a/packages/cli/src/commands/extensions/disable.ts b/packages/cli/src/commands/extensions/disable.ts index 92ebd6fa8..f13e3f550 100644 --- a/packages/cli/src/commands/extensions/disable.ts +++ b/packages/cli/src/commands/extensions/disable.ts @@ -7,6 +7,7 @@ import { type CommandModule } from 'yargs'; import { SettingScope } from '../../config/settings.js'; import { getErrorMessage } from '../../utils/errors.js'; +import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js'; import { getExtensionManager } from './utils.js'; import { t } from '../../i18n/index.js'; @@ -23,14 +24,14 @@ export async function handleDisable(args: DisableArgs) { } else { extensionManager.disableExtension(args.name, SettingScope.User); } - console.log( + writeStdoutLine( t('Extension "{{name}}" successfully disabled for scope "{{scope}}".', { name: args.name, scope: args.scope || SettingScope.User, }), ); } catch (error) { - console.error(getErrorMessage(error)); + writeStderrLine(getErrorMessage(error)); process.exit(1); } } diff --git a/packages/cli/src/commands/extensions/enable.ts b/packages/cli/src/commands/extensions/enable.ts index b36e50ac9..b02e6ff75 100644 --- a/packages/cli/src/commands/extensions/enable.ts +++ b/packages/cli/src/commands/extensions/enable.ts @@ -7,6 +7,7 @@ import { type CommandModule } from 'yargs'; import { FatalConfigError, getErrorMessage } from '@qwen-code/qwen-code-core'; import { SettingScope } from '../../config/settings.js'; +import { writeStdoutLine } from '../../utils/stdioHelpers.js'; import { getExtensionManager } from './utils.js'; import { t } from '../../i18n/index.js'; @@ -25,14 +26,14 @@ export async function handleEnable(args: EnableArgs) { extensionManager.enableExtension(args.name, SettingScope.User); } if (args.scope) { - console.log( + writeStdoutLine( t('Extension "{{name}}" successfully enabled for scope "{{scope}}".', { name: args.name, scope: args.scope, }), ); } else { - console.log( + writeStdoutLine( t('Extension "{{name}}" successfully enabled in all scopes.', { name: args.name, }), diff --git a/packages/cli/src/commands/extensions/install.ts b/packages/cli/src/commands/extensions/install.ts index 6a9ce4929..17012b896 100644 --- a/packages/cli/src/commands/extensions/install.ts +++ b/packages/cli/src/commands/extensions/install.ts @@ -11,6 +11,7 @@ import { parseInstallSource, } from '@qwen-code/qwen-code-core'; import { getErrorMessage } from '../../utils/errors.js'; +import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js'; import { isWorkspaceTrusted } from '../../config/trustedFolders.js'; import { loadSettings } from '../../config/settings.js'; import { @@ -66,13 +67,13 @@ export async function handleInstall(args: InstallArgs) { }, requestConsent, ); - console.log( + writeStdoutLine( t('Extension "{{name}}" installed successfully and enabled.', { name: extension.name, }), ); } catch (error) { - console.error(getErrorMessage(error)); + writeStderrLine(getErrorMessage(error)); process.exit(1); } } diff --git a/packages/cli/src/commands/extensions/link.ts b/packages/cli/src/commands/extensions/link.ts index 545899cda..f03b51e46 100644 --- a/packages/cli/src/commands/extensions/link.ts +++ b/packages/cli/src/commands/extensions/link.ts @@ -7,6 +7,7 @@ import type { CommandModule } from 'yargs'; import { type ExtensionInstallMetadata } from '@qwen-code/qwen-code-core'; import { getErrorMessage } from '../../utils/errors.js'; +import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js'; import { requestConsentNonInteractive, requestConsentOrFail, @@ -31,16 +32,16 @@ export async function handleLink(args: InstallArgs) { requestConsentOrFail.bind(null, requestConsentNonInteractive), ); if (!extension) { - console.log(t('Link extension failed to install.')); + writeStdoutLine(t('Link extension failed to install.')); return; } - console.log( + writeStdoutLine( t('Extension "{{name}}" linked successfully and enabled.', { name: extension.name, }), ); } catch (error) { - console.error(getErrorMessage(error)); + writeStderrLine(getErrorMessage(error)); process.exit(1); } } diff --git a/packages/cli/src/commands/extensions/list.ts b/packages/cli/src/commands/extensions/list.ts index 6f5653be3..4444fba67 100644 --- a/packages/cli/src/commands/extensions/list.ts +++ b/packages/cli/src/commands/extensions/list.ts @@ -6,6 +6,7 @@ import type { CommandModule } from 'yargs'; import { getErrorMessage } from '../../utils/errors.js'; +import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js'; import { extensionToOutputString, getExtensionManager } from './utils.js'; import { t } from '../../i18n/index.js'; @@ -15,10 +16,10 @@ export async function handleList() { const extensions = extensionManager.getLoadedExtensions(); if (!extensions || extensions.length === 0) { - console.log(t('No extensions installed.')); + writeStdoutLine(t('No extensions installed.')); return; } - console.log( + writeStdoutLine( extensions .map((extension, _): string => extensionToOutputString(extension, extensionManager, process.cwd()), @@ -26,7 +27,7 @@ export async function handleList() { .join('\n\n'), ); } catch (error) { - console.error(getErrorMessage(error)); + writeStderrLine(getErrorMessage(error)); process.exit(1); } } diff --git a/packages/cli/src/commands/extensions/new.ts b/packages/cli/src/commands/extensions/new.ts index 27f9c6dd0..f47648ab9 100644 --- a/packages/cli/src/commands/extensions/new.ts +++ b/packages/cli/src/commands/extensions/new.ts @@ -9,6 +9,7 @@ import { join, dirname, basename } from 'node:path'; import type { CommandModule } from 'yargs'; import { fileURLToPath } from 'node:url'; import { getErrorMessage } from '../../utils/errors.js'; +import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js'; interface NewArgs { path: string; @@ -52,7 +53,7 @@ async function handleNew(args: NewArgs) { try { if (args.template) { await copyDirectory(args.template, args.path); - console.log( + writeStdoutLine( `Successfully created new extension from template "${args.template}" at ${args.path}.`, ); } else { @@ -66,13 +67,13 @@ async function handleNew(args: NewArgs) { join(args.path, 'qwen-extension.json'), JSON.stringify(manifest, null, 2), ); - console.log(`Successfully created new extension at ${args.path}.`); + writeStdoutLine(`Successfully created new extension at ${args.path}.`); } - console.log( + writeStdoutLine( `You can install this using "qwen extensions link ${args.path}" to test it out.`, ); } catch (error) { - console.error(getErrorMessage(error)); + writeStderrLine(getErrorMessage(error)); throw error; } } diff --git a/packages/cli/src/commands/extensions/settings.ts b/packages/cli/src/commands/extensions/settings.ts index 49baf2cc4..65c54b570 100644 --- a/packages/cli/src/commands/extensions/settings.ts +++ b/packages/cli/src/commands/extensions/settings.ts @@ -13,6 +13,7 @@ import { updateSetting, } from '@qwen-code/qwen-code-core'; import { t } from '../../i18n/index.js'; +import { writeStdoutLine } from '../../utils/stdioHelpers.js'; // --- SET COMMAND --- interface SetArgs { @@ -50,7 +51,7 @@ const setCommand: CommandModule = { if (!extensions || extensions.length === 0) return; const extension = extensions.find((e) => e.name === name); if (!extension) { - console.log(t('Extension "{{name}}" not found.', { name })); + writeStdoutLine(t('Extension "{{name}}" not found.', { name })); return; } await updateSetting( @@ -85,11 +86,11 @@ const listCommand: CommandModule = { if (!extensions || extensions.length === 0) return; const extension = extensions.find((e) => e.name === name); if (!extension) { - console.log(t('Extension "{{name}}" not found.', { name })); + writeStdoutLine(t('Extension "{{name}}" not found.', { name })); return; } if (!extension || !extension.settings || extension.settings.length === 0) { - console.log( + writeStdoutLine( t('Extension "{{name}}" has no settings to configure.', { name }), ); return; @@ -107,7 +108,7 @@ const listCommand: CommandModule = { ); const mergedSettings = { ...userSettings, ...workspaceSettings }; - console.log(t('Settings for "{{name}}":', { name })); + writeStdoutLine(t('Settings for "{{name}}":', { name })); for (const setting of extension.settings) { const value = mergedSettings[setting.envVar]; let displayValue: string; @@ -126,10 +127,10 @@ const listCommand: CommandModule = { } else { displayValue = value; } - console.log(` + writeStdoutLine(` - ${setting.name} (${setting.envVar})`); - console.log(` ${t('Description:')} ${setting.description}`); - console.log(` ${t('Value:')} ${displayValue}${scopeInfo}`); + writeStdoutLine(` ${t('Description:')} ${setting.description}`); + writeStdoutLine(` ${t('Value:')} ${displayValue}${scopeInfo}`); } }, }; diff --git a/packages/cli/src/commands/extensions/uninstall.ts b/packages/cli/src/commands/extensions/uninstall.ts index 980472a68..551b67771 100644 --- a/packages/cli/src/commands/extensions/uninstall.ts +++ b/packages/cli/src/commands/extensions/uninstall.ts @@ -6,6 +6,7 @@ import type { CommandModule } from 'yargs'; import { getErrorMessage } from '../../utils/errors.js'; +import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js'; import { ExtensionManager } from '@qwen-code/qwen-code-core'; import { requestConsentNonInteractive, @@ -34,11 +35,11 @@ export async function handleUninstall(args: UninstallArgs) { }); await extensionManager.refreshCache(); await extensionManager.uninstallExtension(args.name, false); - console.log( + writeStdoutLine( t('Extension "{{name}}" successfully uninstalled.', { name: args.name }), ); } catch (error) { - console.error(getErrorMessage(error)); + writeStderrLine(getErrorMessage(error)); process.exit(1); } } diff --git a/packages/cli/src/commands/extensions/update.ts b/packages/cli/src/commands/extensions/update.ts index 9325a25f1..d47816b1c 100644 --- a/packages/cli/src/commands/extensions/update.ts +++ b/packages/cli/src/commands/extensions/update.ts @@ -6,6 +6,7 @@ import type { CommandModule } from 'yargs'; import { getErrorMessage } from '../../utils/errors.js'; +import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js'; import { ExtensionUpdateState } from '../../ui/state/extensions.js'; import { checkForExtensionUpdate, @@ -39,11 +40,13 @@ export async function handleUpdate(args: UpdateArgs) { (extension) => extension.name === args.name, ); if (!extension) { - console.log(t('Extension "{{name}}" not found.', { name: args.name })); + writeStdoutLine( + t('Extension "{{name}}" not found.', { name: args.name }), + ); return; } if (!extension.installMetadata) { - console.log( + writeStdoutLine( t( 'Unable to install extension "{{name}}" due to missing install metadata', { name: args.name }, @@ -56,7 +59,7 @@ export async function handleUpdate(args: UpdateArgs) { extensionManager, ); if (updateState !== ExtensionUpdateState.UPDATE_AVAILABLE) { - console.log( + writeStdoutLine( t('Extension "{{name}}" is already up to date.', { name: args.name }), ); return; @@ -71,7 +74,7 @@ export async function handleUpdate(args: UpdateArgs) { updatedExtensionInfo.originalVersion !== updatedExtensionInfo.updatedVersion ) { - console.log( + writeStdoutLine( t( 'Extension "{{name}}" successfully updated: {{oldVersion}} → {{newVersion}}.', { @@ -82,12 +85,12 @@ export async function handleUpdate(args: UpdateArgs) { ), ); } else { - console.log( + writeStdoutLine( t('Extension "{{name}}" is already up to date.', { name: args.name }), ); } } catch (error) { - console.error(getErrorMessage(error)); + writeStderrLine(getErrorMessage(error)); } } if (args.all) { @@ -109,12 +112,12 @@ export async function handleUpdate(args: UpdateArgs) { (info) => info.originalVersion !== info.updatedVersion, ); if (updateInfos.length === 0) { - console.log(t('No extensions to update.')); + writeStdoutLine(t('No extensions to update.')); return; } - console.log(updateInfos.map((info) => updateOutput(info)).join('\n')); + writeStdoutLine(updateInfos.map((info) => updateOutput(info)).join('\n')); } catch (error) { - console.error(getErrorMessage(error)); + writeStderrLine(getErrorMessage(error)); } } } diff --git a/packages/cli/src/commands/mcp/add.ts b/packages/cli/src/commands/mcp/add.ts index 852f8bdae..bbaf79961 100644 --- a/packages/cli/src/commands/mcp/add.ts +++ b/packages/cli/src/commands/mcp/add.ts @@ -7,6 +7,7 @@ // File for 'gemini mcp add' command import type { CommandModule } from 'yargs'; import { loadSettings, SettingScope } from '../../config/settings.js'; +import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js'; import type { MCPServerConfig } from '@qwen-code/qwen-code-core'; async function addMcpServer( @@ -41,7 +42,7 @@ async function addMcpServer( const inHome = settings.workspace.path === settings.user.path; if (scope === 'project' && inHome) { - console.error( + writeStderrLine( 'Error: Please use --scope user to edit settings in the home directory.', ); process.exit(1); @@ -116,7 +117,7 @@ async function addMcpServer( const isExistingServer = !!mcpServers[name]; if (isExistingServer) { - console.log( + writeStdoutLine( `MCP server "${name}" is already configured within ${scope} settings.`, ); } @@ -126,9 +127,9 @@ async function addMcpServer( settings.setValue(settingsScope, 'mcpServers', mcpServers); if (isExistingServer) { - console.log(`MCP server "${name}" updated in ${scope} settings.`); + writeStdoutLine(`MCP server "${name}" updated in ${scope} settings.`); } else { - console.log( + writeStdoutLine( `MCP server "${name}" added to ${scope} settings. (${transport})`, ); } diff --git a/packages/cli/src/commands/mcp/list.ts b/packages/cli/src/commands/mcp/list.ts index 8836e55c0..b754b2754 100644 --- a/packages/cli/src/commands/mcp/list.ts +++ b/packages/cli/src/commands/mcp/list.ts @@ -7,6 +7,7 @@ // File for 'gemini mcp list' command import type { CommandModule } from 'yargs'; import { loadSettings } from '../../config/settings.js'; +import { writeStdoutLine } from '../../utils/stdioHelpers.js'; import type { MCPServerConfig } from '@qwen-code/qwen-code-core'; import { MCPServerStatus, @@ -96,11 +97,11 @@ export async function listMcpServers(): Promise { const serverNames = Object.keys(mcpServers); if (serverNames.length === 0) { - console.log('No MCP servers configured.'); + writeStdoutLine('No MCP servers configured.'); return; } - console.log('Configured MCP servers:\n'); + writeStdoutLine('Configured MCP servers:\n'); for (const serverName of serverNames) { const server = mcpServers[serverName]; @@ -134,7 +135,7 @@ export async function listMcpServers(): Promise { serverInfo += `${server.command} ${server.args?.join(' ') || ''} (stdio)`; } - console.log(`${statusIndicator} ${serverInfo} - ${statusText}`); + writeStdoutLine(`${statusIndicator} ${serverInfo} - ${statusText}`); } } diff --git a/packages/cli/src/commands/mcp/remove.ts b/packages/cli/src/commands/mcp/remove.ts index bcaa5ad4b..87d73cf6c 100644 --- a/packages/cli/src/commands/mcp/remove.ts +++ b/packages/cli/src/commands/mcp/remove.ts @@ -7,6 +7,7 @@ // File for 'gemini mcp remove' command import type { CommandModule } from 'yargs'; import { loadSettings, SettingScope } from '../../config/settings.js'; +import { writeStdoutLine } from '../../utils/stdioHelpers.js'; async function removeMcpServer( name: string, @@ -23,7 +24,7 @@ async function removeMcpServer( const mcpServers = existingSettings.mcpServers || {}; if (!mcpServers[name]) { - console.log(`Server "${name}" not found in ${scope} settings.`); + writeStdoutLine(`Server "${name}" not found in ${scope} settings.`); return; } @@ -31,7 +32,7 @@ async function removeMcpServer( settings.setValue(settingsScope, 'mcpServers', mcpServers); - console.log(`Server "${name}" removed from ${scope} settings.`); + writeStdoutLine(`Server "${name}" removed from ${scope} settings.`); } export const removeCommand: CommandModule = { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 318ed2956..7e70b2cf7 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -46,6 +46,7 @@ import { mcpCommand } from '../commands/mcp.js'; import { isWorkspaceTrusted } from './trustedFolders.js'; import { buildWebSearchConfig } from './webSearch.js'; +import { writeStderrLine } from '../utils/stdioHelpers.js'; const VALID_APPROVAL_MODE_VALUES = [ 'plan', @@ -501,7 +502,7 @@ export async function parseArguments(settings: Settings): Promise { ) // Ensure validation flows through .fail() for clean UX .fail((msg: string, err: Error | undefined, yargs: Argv) => { - console.error(msg || err?.message || 'Unknown error'); + writeStderrLine(msg || err?.message || 'Unknown error'); yargs.showHelp(); process.exit(1); }) @@ -593,7 +594,7 @@ export async function parseArguments(settings: Settings): Promise { // Handle deprecated --experimental-acp flag if (result['experimentalAcp']) { - console.warn( + writeStderrLine( '\x1b[33m⚠ Warning: --experimental-acp is deprecated and will be removed in a future release. Please use --acp instead.\x1b[0m', ); // Map experimental-acp to acp if acp is not explicitly set @@ -727,7 +728,7 @@ export async function loadCliConfig( approvalMode !== ApprovalMode.DEFAULT && approvalMode !== ApprovalMode.PLAN ) { - console.warn( + writeStderrLine( `Approval mode overridden to "default" because the current folder is not trusted.`, ); approvalMode = ApprovalMode.DEFAULT; @@ -894,7 +895,7 @@ export async function loadCliConfig( sessionData = await sessionService.loadSession(argv.resume); if (!sessionData) { const message = `No saved session found with ID ${argv.resume}. Run \`qwen --resume\` without an ID to choose from existing sessions.`; - console.log(message); + writeStderrLine(message); process.exit(1); } } diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 0f213acf3..ee3e43882 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -30,6 +30,7 @@ import { import { resolveEnvVarsInObject } from '../utils/envVarResolver.js'; import { customDeepMerge, type MergeableObject } from '../utils/deepMerge.js'; import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js'; +import { writeStderrLine } from '../utils/stdioHelpers.js'; function getMergeStrategyForPath(path: string[]): MergeStrategy | undefined { let current: SettingDefinition | undefined = undefined; @@ -747,7 +748,7 @@ export function loadSettings( 'utf-8', ); } catch (e) { - console.error( + writeStderrLine( `Error migrating settings file on disk: ${getErrorMessage( e, )}`, @@ -769,7 +770,7 @@ export function loadSettings( 'utf-8', ); } catch (e) { - console.error( + writeStderrLine( `Error adding version to settings file: ${getErrorMessage(e)}`, ); } @@ -907,9 +908,6 @@ export function migrateDeprecatedSettings( legacySkills !== undefined && settings.experimental?.skills === undefined ) { - console.log( - `Migrating deprecated tools.experimental.skills setting from ${scope} settings...`, - ); loadedSettings.setValue(scope, 'experimental.skills', legacySkills); } }; @@ -939,7 +937,8 @@ export function saveSettings(settingsFile: SettingsFile): void { settingsToSave as Record, ); } catch (error) { - console.error('Error saving user settings file:', error); + writeStderrLine('Error saving user settings file.'); + writeStderrLine(error instanceof Error ? error.message : String(error)); throw error; } } diff --git a/packages/cli/src/config/trustedFolders.ts b/packages/cli/src/config/trustedFolders.ts index 60a897f19..355146025 100644 --- a/packages/cli/src/config/trustedFolders.ts +++ b/packages/cli/src/config/trustedFolders.ts @@ -15,6 +15,7 @@ import { } from '@qwen-code/qwen-code-core'; import type { Settings } from './settings.js'; import stripJsonComments from 'strip-json-comments'; +import { writeStderrLine } from '../utils/stdioHelpers.js'; export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json'; export const SETTINGS_DIRECTORY_NAME = '.qwen'; @@ -184,7 +185,8 @@ export function saveTrustedFolders( { encoding: 'utf-8', mode: 0o600 }, ); } catch (error) { - console.error('Error saving trusted folders file:', error); + writeStderrLine('Error saving trusted folders file.'); + writeStderrLine(error instanceof Error ? error.message : String(error)); } } diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 5e0074714..8ae1edf9d 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -10,6 +10,7 @@ import { logUserPrompt, Storage, type Config, + createDebugLogger, } from '@qwen-code/qwen-code-core'; import { render } from 'ink'; import dns from 'node:dns'; @@ -55,10 +56,13 @@ import { start_sandbox } from './utils/sandbox.js'; import { getStartupWarnings } from './utils/startupWarnings.js'; import { getUserStartupWarnings } from './utils/userStartupWarnings.js'; import { getCliVersion } from './utils/version.js'; +import { writeStderrLine } from './utils/stdioHelpers.js'; import { computeWindowTitle } from './utils/windowTitle.js'; import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js'; import { showResumeSessionPicker } from './ui/components/StandaloneSessionPicker.js'; +const debugLogger = createDebugLogger('STARTUP'); + export function validateDnsResolutionOrder( order: string | undefined, ): DnsResolutionOrder { @@ -70,7 +74,7 @@ export function validateDnsResolutionOrder( return order; } // We don't want to throw here, just warn and use the default. - console.warn( + writeStderrLine( `Invalid value for dnsResolutionOrder in settings: "${order}". Using default "${defaultValue}".`, ); return defaultValue; @@ -86,7 +90,7 @@ function getNodeMemoryArgs(isDebugMode: boolean): string[] { // Set target to 50% of total memory const targetMaxOldSpaceSizeInMB = Math.floor(totalMemoryMB * 0.5); if (isDebugMode) { - console.debug( + writeStderrLine( `Current heap size ${currentMaxOldSpaceSizeMb.toFixed(2)} MB`, ); } @@ -97,7 +101,7 @@ function getNodeMemoryArgs(isDebugMode: boolean): string[] { if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) { if (isDebugMode) { - console.debug( + writeStderrLine( `Need to relaunch with more memory: ${targetMaxOldSpaceSizeInMB.toFixed(2)} MB`, ); } @@ -193,9 +197,7 @@ export async function startInteractiveUI( }) .catch((err) => { // Silently ignore update check errors. - if (config.getDebugMode()) { - console.error('Update check failed:', err); - } + debugLogger.warn(`Update check failed: ${err}`); }); } @@ -211,7 +213,7 @@ export async function main() { // Check for invalid input combinations early to prevent crashes if (argv.promptInteractive && !process.stdin.isTTY) { - console.error( + writeStderrLine( 'Error: The --prompt-interactive flag cannot be used when input is piped from stdin.', ); process.exit(1); @@ -230,7 +232,9 @@ export async function main() { if (!themeManager.setActiveTheme(settings.merged.ui?.theme)) { // If the theme is not found during initial load, log a warning and continue. // The useThemeCommand hook in AppContainer.tsx will handle opening the dialog. - console.warn(`Warning: Theme "${settings.merged.ui?.theme}" not found.`); + writeStderrLine( + `Warning: Theme "${settings.merged.ui?.theme}" not found.`, + ); } } @@ -269,7 +273,7 @@ export async function main() { await partialConfig.refreshAuth(authType); } } catch (err) { - console.error('Error authenticating:', err); + writeStderrLine(`Error authenticating: ${err}`); process.exit(1); } } @@ -426,12 +430,12 @@ export async function main() { // Print debug mode notice to stderr for non-interactive mode if (config.getDebugMode()) { - console.error('Debug mode enabled'); - console.error( + writeStderrLine('Debug mode enabled'); + writeStderrLine( `Logging to: ${Storage.getDebugLogPath(config.getSessionId())}`, ); if (isDebugLoggingDegraded()) { - console.error( + writeStderrLine( 'Warning: Debug logging is degraded (write failures occurred)', ); } @@ -472,7 +476,7 @@ export async function main() { } if (!input) { - console.error( + writeStderrLine( `No input provided via stdin. Input can be provided by piping data into gemini or using the --prompt option.`, ); process.exit(1); @@ -487,9 +491,7 @@ export async function main() { prompt_length: input.length, }); - if (config.getDebugMode()) { - console.log('Session ID: %s', config.getSessionId()); - } + debugLogger.debug(`Session ID: ${config.getSessionId()}`); await runNonInteractive(nonInteractiveConfig, settings, input, prompt_id); // Call cleanup before process.exit, which causes cleanup to not run diff --git a/packages/cli/src/i18n/index.ts b/packages/cli/src/i18n/index.ts index 1338fb571..6d95a49cc 100644 --- a/packages/cli/src/i18n/index.ts +++ b/packages/cli/src/i18n/index.ts @@ -8,6 +8,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { homedir } from 'node:os'; +import { writeStderrLine } from '../utils/stdioHelpers.js'; import { type SupportedLanguage, getLanguageNameFromLocale, @@ -25,7 +26,6 @@ type TranslationValue = string | string[]; type TranslationDict = Record; const translationCache: Record = {}; const loadingPromises: Record> = {}; - // Path helpers const getBuiltinLocalesDir = (): string => { const __filename = fileURLToPath(import.meta.url); @@ -143,16 +143,13 @@ async function loadTranslationsAsync( } catch (error) { // Log warning but continue to next directory if (isUser) { - console.warn( - `Failed to load translations from user directory for ${lang}:`, - error, + writeStderrLine( + `Failed to load translations from user directory for ${lang}: ${error instanceof Error ? error.message : String(error)}`, ); } else { - console.warn(`Failed to load JS translations for ${lang}:`, error); - if (error instanceof Error) { - console.warn(`Error details: ${error.message}`); - console.warn(`Stack: ${error.stack}`); - } + writeStderrLine( + `Failed to load JS translations for ${lang}: ${error instanceof Error ? error.message : String(error)}`, + ); } // Continue to next directory continue; @@ -211,7 +208,7 @@ export function setLanguage(lang: SupportedLanguage | 'auto'): void { const userJsPath = getLocalePath(resolvedLang, true); const builtinJsPath = getLocalePath(resolvedLang, false); if (fs.existsSync(userJsPath) || fs.existsSync(builtinJsPath)) { - console.warn( + writeStderrLine( `Language file for ${resolvedLang} requires async loading. ` + `Use setLanguageAsync() instead, or call initializeI18n() first.`, ); diff --git a/packages/cli/src/nonInteractive/control/ControlDispatcher.ts b/packages/cli/src/nonInteractive/control/ControlDispatcher.ts index d6dc79a46..d3723a463 100644 --- a/packages/cli/src/nonInteractive/control/ControlDispatcher.ts +++ b/packages/cli/src/nonInteractive/control/ControlDispatcher.ts @@ -38,6 +38,9 @@ import type { ControlResponse, ControlRequestPayload, } from '../types.js'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; + +const debugLogger = createDebugLogger('CONTROL_DISPATCHER'); /** * Tracks an incoming request from SDK awaiting CLI response @@ -135,11 +138,9 @@ export class ControlDispatcher implements IPendingRequestRegistry { const pending = this.pendingOutgoingRequests.get(requestId); if (!pending) { // No pending request found - may have timed out or been cancelled - if (this.context.debugMode) { - console.error( - `[ControlDispatcher] No pending outgoing request for: ${requestId}`, - ); - } + debugLogger.debug( + `[ControlDispatcher] No pending outgoing request for: ${requestId}`, + ); return; } @@ -181,11 +182,9 @@ export class ControlDispatcher implements IPendingRequestRegistry { this.deregisterIncomingRequest(requestId); this.sendErrorResponse(requestId, 'Request cancelled'); - if (this.context.debugMode) { - console.error( - `[ControlDispatcher] Cancelled incoming request: ${requestId}`, - ); - } + debugLogger.debug( + `[ControlDispatcher] Cancelled incoming request: ${requestId}`, + ); } } else { // Cancel ALL pending incoming requests @@ -199,11 +198,9 @@ export class ControlDispatcher implements IPendingRequestRegistry { } } - if (this.context.debugMode) { - console.error( - `[ControlDispatcher] Cancelled all ${requestIds.length} pending incoming requests`, - ); - } + debugLogger.debug( + `[ControlDispatcher] Cancelled all ${requestIds.length} pending incoming requests`, + ); } } @@ -211,9 +208,7 @@ export class ControlDispatcher implements IPendingRequestRegistry { * Stops all pending requests and cleans up all controllers */ shutdown(): void { - if (this.context.debugMode) { - console.error('[ControlDispatcher] Shutting down'); - } + debugLogger.debug('[ControlDispatcher] Shutting down'); // Cancel all incoming requests for (const [ @@ -324,18 +319,16 @@ export class ControlDispatcher implements IPendingRequestRegistry { while (this.pendingIncomingRequests.size > 0) { if (Date.now() - startTime > timeoutMs) { - if (this.context.debugMode) { - console.error( - `[ControlDispatcher] Timeout waiting for ${this.pendingIncomingRequests.size} pending incoming requests`, - ); - } + debugLogger.warn( + `[ControlDispatcher] Timeout waiting for ${this.pendingIncomingRequests.size} pending incoming requests`, + ); break; } await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); } - if (this.context.debugMode && this.pendingIncomingRequests.size === 0) { - console.error('[ControlDispatcher] All incoming requests completed'); + if (this.pendingIncomingRequests.size === 0) { + debugLogger.debug('[ControlDispatcher] All incoming requests completed'); } } diff --git a/packages/cli/src/nonInteractive/control/controllers/baseController.ts b/packages/cli/src/nonInteractive/control/controllers/baseController.ts index dcb9e7c99..5b754168c 100644 --- a/packages/cli/src/nonInteractive/control/controllers/baseController.ts +++ b/packages/cli/src/nonInteractive/control/controllers/baseController.ts @@ -16,6 +16,8 @@ */ import { randomUUID } from 'node:crypto'; +import type { DebugLogger } from '@qwen-code/qwen-code-core'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; import type { IControlContext } from '../ControlContext.js'; import type { ControlRequestPayload, @@ -57,6 +59,7 @@ export abstract class BaseController { protected context: IControlContext; protected registry: IPendingRequestRegistry; protected controllerName: string; + protected debugLogger: DebugLogger; constructor( context: IControlContext, @@ -66,6 +69,7 @@ export abstract class BaseController { this.context = context; this.registry = registry; this.controllerName = controllerName; + this.debugLogger = createDebugLogger(); } /** @@ -83,9 +87,9 @@ export abstract class BaseController { const timeoutId = setTimeout(() => { requestAbortController.abort(); this.registry.deregisterIncomingRequest(requestId); - if (this.context.debugMode) { - console.error(`[${this.controllerName}] Request timeout: ${requestId}`); - } + this.debugLogger.warn( + `[${this.controllerName}] Request timeout: ${requestId}`, + ); }, DEFAULT_REQUEST_TIMEOUT_MS); // Register with central registry @@ -136,11 +140,9 @@ export abstract class BaseController { const abortHandler = () => { this.registry.deregisterOutgoingRequest(requestId); reject(new Error('Request aborted')); - if (this.context.debugMode) { - console.error( - `[${this.controllerName}] Outgoing request aborted: ${requestId}`, - ); - } + this.debugLogger.warn( + `[${this.controllerName}] Outgoing request aborted: ${requestId}`, + ); }; if (signal) { @@ -154,11 +156,9 @@ export abstract class BaseController { } this.registry.deregisterOutgoingRequest(requestId); reject(new Error('Control request timeout')); - if (this.context.debugMode) { - console.error( - `[${this.controllerName}] Outgoing request timeout: ${requestId}`, - ); - } + this.debugLogger.warn( + `[${this.controllerName}] Outgoing request timeout: ${requestId}`, + ); }, timeoutMs); // Wrap resolve/reject to clean up abort listener diff --git a/packages/cli/src/nonInteractive/control/controllers/hookController.ts b/packages/cli/src/nonInteractive/control/controllers/hookController.ts index 1043b7b8c..df6eb4c0e 100644 --- a/packages/cli/src/nonInteractive/control/controllers/hookController.ts +++ b/packages/cli/src/nonInteractive/control/controllers/hookController.ts @@ -42,9 +42,9 @@ export class HookController extends BaseController { private async handleHookCallback( payload: CLIHookCallbackRequest, ): Promise> { - if (this.context.debugMode) { - console.error(`[HookController] Hook callback: ${payload.callback_id}`); - } + this.debugLogger.debug( + `[HookController] Hook callback: ${payload.callback_id}`, + ); // Hook callback processing not yet implemented return { diff --git a/packages/cli/src/nonInteractive/control/controllers/permissionController.ts b/packages/cli/src/nonInteractive/control/controllers/permissionController.ts index 4cec3b00f..a4712ce26 100644 --- a/packages/cli/src/nonInteractive/control/controllers/permissionController.ts +++ b/packages/cli/src/nonInteractive/control/controllers/permissionController.ts @@ -228,11 +228,9 @@ export class PermissionController extends BaseController { this.context.permissionMode = mode; this.context.config.setApprovalMode(mode as ApprovalMode); - if (this.context.debugMode) { - console.error( - `[PermissionController] Permission mode updated to: ${mode}`, - ); - } + this.debugLogger.info( + `[PermissionController] Permission mode updated to: ${mode}`, + ); return { status: 'updated', mode }; } @@ -463,12 +461,10 @@ export class PermissionController extends BaseController { ); } } catch (error) { - if (this.context.debugMode) { - console.error( - '[PermissionController] Outgoing permission failed:', - error, - ); - } + this.debugLogger.error( + '[PermissionController] Outgoing permission failed:', + error, + ); // On error, use default cancel message // Only pass payload for exec and mcp types that support it const confirmationType = toolCall.confirmationDetails.type; diff --git a/packages/cli/src/nonInteractive/control/controllers/sdkMcpController.ts b/packages/cli/src/nonInteractive/control/controllers/sdkMcpController.ts index 5d0264fbb..058226c09 100644 --- a/packages/cli/src/nonInteractive/control/controllers/sdkMcpController.ts +++ b/packages/cli/src/nonInteractive/control/controllers/sdkMcpController.ts @@ -82,12 +82,10 @@ export class SdkMcpController extends BaseController { serverName: string, message: JSONRPCMessage, ): Promise { - if (this.context.debugMode) { - console.error( - `[SdkMcpController] Sending MCP message to SDK server '${serverName}':`, - JSON.stringify(message), - ); - } + this.debugLogger.debug( + `[SdkMcpController] Sending MCP message to SDK server '${serverName}':`, + JSON.stringify(message), + ); // Send control request to SDK with the MCP message const response = await this.sendControlRequest( @@ -110,12 +108,10 @@ export class SdkMcpController extends BaseController { ); } - if (this.context.debugMode) { - console.error( - `[SdkMcpController] Received MCP response from SDK server '${serverName}':`, - JSON.stringify(mcpResponse), - ); - } + this.debugLogger.debug( + `[SdkMcpController] Received MCP response from SDK server '${serverName}':`, + JSON.stringify(mcpResponse), + ); return mcpResponse; } diff --git a/packages/cli/src/nonInteractive/control/controllers/systemController.ts b/packages/cli/src/nonInteractive/control/controllers/systemController.ts index 824858aa3..06923e963 100644 --- a/packages/cli/src/nonInteractive/control/controllers/systemController.ts +++ b/packages/cli/src/nonInteractive/control/controllers/systemController.ts @@ -22,11 +22,14 @@ import type { } from '../../types.js'; import { getAvailableCommands } from '../../../nonInteractiveCliCommands.js'; import { + createDebugLogger, MCPServerConfig, AuthProviderType, type MCPOAuthConfig, } from '@qwen-code/qwen-code-core'; +const debugLogger = createDebugLogger('SYSTEM_CONTROLLER'); + export class SystemController extends BaseController { /** * Handle system control requests @@ -122,18 +125,14 @@ export class SystemController extends BaseController { if (sdkServerCount > 0) { try { this.context.config.addMcpServers(sdkServers); - if (this.context.debugMode) { - console.error( - `[SystemController] Added ${sdkServerCount} SDK MCP servers to config`, - ); - } + debugLogger.debug( + `[SystemController] Added ${sdkServerCount} SDK MCP servers to config`, + ); } catch (error) { - if (this.context.debugMode) { - console.error( - '[SystemController] Failed to add SDK MCP servers:', - error, - ); - } + debugLogger.error( + '[SystemController] Failed to add SDK MCP servers:', + error, + ); } } } @@ -158,18 +157,14 @@ export class SystemController extends BaseController { if (externalCount > 0) { try { this.context.config.addMcpServers(externalServers); - if (this.context.debugMode) { - console.error( - `[SystemController] Added ${externalCount} external MCP servers to config`, - ); - } + debugLogger.debug( + `[SystemController] Added ${externalCount} external MCP servers to config`, + ); } catch (error) { - if (this.context.debugMode) { - console.error( - '[SystemController] Failed to add external MCP servers:', - error, - ); - } + debugLogger.error( + '[SystemController] Failed to add external MCP servers:', + error, + ); } } } @@ -178,29 +173,23 @@ export class SystemController extends BaseController { try { this.context.config.setSessionSubagents(payload.agents); - if (this.context.debugMode) { - console.error( - `[SystemController] Added ${payload.agents.length} session subagents to config`, - ); - } + debugLogger.debug( + `[SystemController] Added ${payload.agents.length} session subagents to config`, + ); } catch (error) { - if (this.context.debugMode) { - console.error( - '[SystemController] Failed to add session subagents:', - error, - ); - } + debugLogger.error( + '[SystemController] Failed to add session subagents:', + error, + ); } } // Build capabilities for response const capabilities = this.buildControlCapabilities(); - if (this.context.debugMode) { - console.error( - `[SystemController] Initialized with ${this.context.sdkMcpServers.size} SDK MCP servers`, - ); - } + debugLogger.debug( + `[SystemController] Initialized with ${this.context.sdkMcpServers.size} SDK MCP servers`, + ); return { subtype: 'initialize', @@ -234,11 +223,9 @@ export class SystemController extends BaseController { config?: CLIMcpServerConfig, ): MCPServerConfig | null { if (!config || typeof config !== 'object') { - if (this.context.debugMode) { - console.error( - `[SystemController] Ignoring invalid MCP server config for '${serverName}'`, - ); - } + debugLogger.warn( + `[SystemController] Ignoring invalid MCP server config for '${serverName}'`, + ); return null; } @@ -282,11 +269,9 @@ export class SystemController extends BaseController { case AuthProviderType.SERVICE_ACCOUNT_IMPERSONATION: return value; default: - if (this.context.debugMode) { - console.error( - `[SystemController] Unsupported authProviderType '${value}', skipping`, - ); - } + debugLogger.warn( + `[SystemController] Unsupported authProviderType '${value}', skipping`, + ); return undefined; } } @@ -326,14 +311,10 @@ export class SystemController extends BaseController { // Abort the main signal to cancel ongoing operations if (this.context.abortSignal && !this.context.abortSignal.aborted) { // Note: We can't directly abort the signal, but the onInterrupt callback should handle this - if (this.context.debugMode) { - console.error('[SystemController] Interrupt signal triggered'); - } + debugLogger.debug('[SystemController] Interrupt signal triggered'); } - if (this.context.debugMode) { - console.error('[SystemController] Interrupt handled'); - } + debugLogger.debug('[SystemController] Interrupt handled'); return { subtype: 'interrupt' }; } @@ -362,9 +343,7 @@ export class SystemController extends BaseController { // Attempt to set the model using config await this.context.config.setModel(model); - if (this.context.debugMode) { - console.error(`[SystemController] Model switched to: ${model}`); - } + debugLogger.info(`[SystemController] Model switched to: ${model}`); return { subtype: 'set_model', @@ -374,12 +353,10 @@ export class SystemController extends BaseController { const errorMessage = error instanceof Error ? error.message : 'Failed to set model'; - if (this.context.debugMode) { - console.error( - `[SystemController] Failed to set model ${model}:`, - error, - ); - } + debugLogger.error( + `[SystemController] Failed to set model ${model}:`, + error, + ); throw new Error(errorMessage); } @@ -431,12 +408,10 @@ export class SystemController extends BaseController { return []; } - if (this.context.debugMode) { - console.error( - '[SystemController] Failed to load slash commands:', - error, - ); - } + debugLogger.error( + '[SystemController] Failed to load slash commands:', + error, + ); return []; } } diff --git a/packages/cli/src/nonInteractive/session.ts b/packages/cli/src/nonInteractive/session.ts index e8e6da129..cdec712e1 100644 --- a/packages/cli/src/nonInteractive/session.ts +++ b/packages/cli/src/nonInteractive/session.ts @@ -8,6 +8,7 @@ import type { Config, ConfigInitializeOptions, } from '@qwen-code/qwen-code-core'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; import { StreamJsonInputReader } from './io/StreamJsonInputReader.js'; import { StreamJsonOutputAdapter } from './io/StreamJsonOutputAdapter.js'; import { ControlContext } from './control/ControlContext.js'; @@ -34,6 +35,8 @@ import { createMinimalSettings } from '../config/settings.js'; import { runNonInteractive } from '../nonInteractiveCli.js'; import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js'; +const debugLogger = createDebugLogger('NON_INTERACTIVE_SESSION'); + class Session { private userMessageQueue: CLIUserMessage[] = []; private abortController: AbortController; @@ -46,7 +49,6 @@ class Session { private dispatcher: ControlDispatcher | null = null; private controlService: ControlService | null = null; private controlSystemEnabled: boolean | null = null; - private debugMode: boolean; private shutdownHandler: (() => void) | null = null; private initialPrompt: CLIUserMessage | null = null; private processingPromise: Promise | null = null; @@ -62,7 +64,6 @@ class Session { constructor(config: Config, initialPrompt?: CLIUserMessage) { this.config = config; this.sessionId = config.getSessionId(); - this.debugMode = config.getDebugMode(); this.abortController = new AbortController(); this.initialPrompt = initialPrompt ?? null; @@ -105,17 +106,13 @@ class Session { return; } - if (this.debugMode) { - console.error('[Session] Initializing config'); - } + debugLogger.debug('[Session] Initializing config'); try { await this.config.initialize(options); this.configInitialized = true; } catch (error) { - if (this.debugMode) { - console.error('[Session] Failed to initialize config:', error); - } + debugLogger.error('[Session] Failed to initialize config:', error); throw error; } } @@ -125,9 +122,7 @@ class Session { */ private completeInitialization(): void { if (this.initializationResolve) { - if (this.debugMode) { - console.error('[Session] Initialization complete'); - } + debugLogger.debug('[Session] Initialization complete'); this.initializationResolve(); this.initializationResolve = null; this.initializationReject = null; @@ -139,9 +134,7 @@ class Session { */ private failInitialization(error: Error): void { if (this.initializationReject) { - if (this.debugMode) { - console.error('[Session] Initialization failed:', error); - } + debugLogger.error('[Session] Initialization failed:', error); this.initializationReject(error); this.initializationResolve = null; this.initializationReject = null; @@ -213,11 +206,9 @@ class Session { return; } - if (this.debugMode) { - console.error( - '[Session] Ignoring non-initialize control request during initialization', - ); - } + debugLogger.debug( + '[Session] Ignoring non-initialize control request during initialization', + ); return; } @@ -254,9 +245,7 @@ class Session { // Initialization complete! this.completeInitialization(); } catch (error) { - if (this.debugMode) { - console.error('[Session] SDK mode initialization failed:', error); - } + debugLogger.error('[Session] SDK mode initialization failed:', error); this.failInitialization( error instanceof Error ? error : new Error(String(error)), ); @@ -281,9 +270,7 @@ class Session { // Enqueue the first user message for processing this.enqueueUserMessage(userMessage); } catch (error) { - if (this.debugMode) { - console.error('[Session] Direct mode initialization failed:', error); - } + debugLogger.error('[Session] Direct mode initialization failed:', error); this.failInitialization( error instanceof Error ? error : new Error(String(error)), ); @@ -297,18 +284,14 @@ class Session { private handleControlRequestAsync(request: CLIControlRequest): void { const dispatcher = this.getDispatcher(); if (!dispatcher) { - if (this.debugMode) { - console.error('[Session] Control system not enabled'); - } + debugLogger.warn('[Session] Control system not enabled'); return; } // Fire-and-forget: dispatch runs concurrently // The dispatcher's pendingIncomingRequests tracks completion void dispatcher.dispatch(request).catch((error) => { - if (this.debugMode) { - console.error('[Session] Control request dispatch error:', error); - } + debugLogger.error('[Session] Control request dispatch error:', error); // Error response is already sent by dispatcher.dispatch() }); } @@ -338,9 +321,7 @@ class Session { private async processUserMessage(userMessage: CLIUserMessage): Promise { const input = extractUserMessageText(userMessage); if (!input) { - if (this.debugMode) { - console.error('[Session] No text content in user message'); - } + debugLogger.debug('[Session] No text content in user message'); return; } @@ -362,9 +343,7 @@ class Session { }, ); } catch (error) { - if (this.debugMode) { - console.error('[Session] Query execution error:', error); - } + debugLogger.error('[Session] Query execution error:', error); } } @@ -382,9 +361,7 @@ class Session { try { await this.processUserMessage(userMessage); } catch (error) { - if (this.debugMode) { - console.error('[Session] Error processing user message:', error); - } + debugLogger.error('[Session] Error processing user message:', error); this.emitErrorResult(error); } } @@ -430,18 +407,14 @@ class Session { } private handleInterrupt(): void { - if (this.debugMode) { - console.error('[Session] Interrupt requested'); - } + debugLogger.info('[Session] Interrupt requested'); this.abortController.abort(); this.abortController = new AbortController(); } private setupSignalHandlers(): void { this.shutdownHandler = () => { - if (this.debugMode) { - console.error('[Session] Shutdown signal received'); - } + debugLogger.info('[Session] Shutdown signal received'); this.isShuttingDown = true; this.abortController.abort(); }; @@ -458,16 +431,17 @@ class Session { try { await this.waitForInitialization(); } catch (error) { - if (this.debugMode) { - console.error('[Session] Initialization error during shutdown:', error); - } + debugLogger.error( + '[Session] Initialization error during shutdown:', + error, + ); } // 2. Wait for all control request handlers using dispatcher's tracking if (this.dispatcher) { const pendingCount = this.dispatcher.getPendingIncomingRequestCount(); - if (pendingCount > 0 && this.debugMode) { - console.error( + if (pendingCount > 0) { + debugLogger.debug( `[Session] Waiting for ${pendingCount} pending control request handlers`, ); } @@ -476,23 +450,17 @@ class Session { // 3. Wait for user message processing queue while (this.processingPromise) { - if (this.debugMode) { - console.error('[Session] Waiting for user message processing'); - } + debugLogger.debug('[Session] Waiting for user message processing'); try { await this.processingPromise; } catch (error) { - if (this.debugMode) { - console.error('[Session] Error in user message processing:', error); - } + debugLogger.error('[Session] Error in user message processing:', error); } } } private async shutdown(): Promise { - if (this.debugMode) { - console.error('[Session] Shutting down'); - } + debugLogger.debug('[Session] Shutting down'); this.isShuttingDown = true; @@ -528,9 +496,7 @@ class Session { */ async run(): Promise { try { - if (this.debugMode) { - console.error('[Session] Starting session', this.sessionId); - } + debugLogger.info('[Session] Starting session', this.sessionId); // Handle initial prompt if provided (fire-and-forget) if (this.initialPrompt !== null) { @@ -571,18 +537,16 @@ class Session { } else if (isCLIUserMessage(message)) { // User messages are enqueued, processing runs separately this.enqueueUserMessage(message as CLIUserMessage); - } else if (this.debugMode) { - if ( - !isCLIAssistantMessage(message) && - !isCLISystemMessage(message) && - !isCLIResultMessage(message) && - !isCLIPartialAssistantMessage(message) - ) { - console.error( - '[Session] Unknown message type:', - JSON.stringify(message, null, 2), - ); - } + } else if ( + !isCLIAssistantMessage(message) && + !isCLISystemMessage(message) && + !isCLIResultMessage(message) && + !isCLIPartialAssistantMessage(message) + ) { + debugLogger.warn( + '[Session] Unknown message type:', + JSON.stringify(message, null, 2), + ); } if (this.isShuttingDown) { @@ -590,9 +554,7 @@ class Session { } } } catch (streamError) { - if (this.debugMode) { - console.error('[Session] Stream reading error:', streamError); - } + debugLogger.error('[Session] Stream reading error:', streamError); throw streamError; } @@ -600,9 +562,7 @@ class Session { await this.waitForAllPendingWork(); await this.shutdown(); } catch (error) { - if (this.debugMode) { - console.error('[Session] Error:', error); - } + debugLogger.error('[Session] Error:', error); await this.shutdown(); throw error; } finally { diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index 40197fa71..5bd5e2e10 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -22,6 +22,7 @@ import { InputFormat, uiTelemetryService, parseAndFormatApiError, + createDebugLogger, } from '@qwen-code/qwen-code-core'; import type { Content, Part, PartListUnion } from '@google/genai'; import type { CLIUserMessage, PermissionMode } from './nonInteractive/types.js'; @@ -38,6 +39,8 @@ import { handleCancellationError, handleMaxTurnsExceededError, } from './utils/errors.js'; + +const debugLogger = createDebugLogger('NON_INTERACTIVE_CLI'); import { normalizePartList, extractPartsFromUserMessage, @@ -156,9 +159,7 @@ export async function runNonInteractive( // Setup signal handlers for graceful shutdown const shutdownHandler = () => { - if (config.getDebugMode()) { - console.error('[runNonInteractive] Shutdown signal received'); - } + debugLogger.debug('[runNonInteractive] Shutdown signal received'); abortController.abort(); }; diff --git a/packages/cli/src/nonInteractiveCliCommands.ts b/packages/cli/src/nonInteractiveCliCommands.ts index 8e2e28dd4..a26f4dbca 100644 --- a/packages/cli/src/nonInteractiveCliCommands.ts +++ b/packages/cli/src/nonInteractiveCliCommands.ts @@ -10,6 +10,7 @@ import { Logger, uiTelemetryService, type Config, + createDebugLogger, } from '@qwen-code/qwen-code-core'; import { CommandService } from './services/CommandService.js'; import { BuiltinCommandLoader } from './services/BuiltinCommandLoader.js'; @@ -25,6 +26,8 @@ import type { LoadedSettings } from './config/settings.js'; import type { SessionStatsState } from './ui/contexts/SessionContext.js'; import { t } from './i18n/index.js'; +const debugLogger = createDebugLogger('NON_INTERACTIVE_COMMANDS'); + /** * Built-in commands that are allowed in non-interactive modes (CLI and ACP). * Only safe, read-only commands that don't require interactive UI. @@ -377,7 +380,7 @@ export const getAvailableCommands = async ( return filteredCommands.filter((cmd) => !cmd.hidden); } catch (error) { // Handle errors gracefully - log and return empty array - console.error('Error loading available commands:', error); + debugLogger.error('Error loading available commands:', error); return []; } }; diff --git a/packages/cli/src/services/FileCommandLoader.ts b/packages/cli/src/services/FileCommandLoader.ts index ef81f9e69..f92004afb 100644 --- a/packages/cli/src/services/FileCommandLoader.ts +++ b/packages/cli/src/services/FileCommandLoader.ts @@ -11,7 +11,11 @@ import toml from '@iarna/toml'; import { glob } from 'glob'; import { z } from 'zod'; import type { Config } from '@qwen-code/qwen-code-core'; -import { EXTENSIONS_CONFIG_FILENAME, Storage } from '@qwen-code/qwen-code-core'; +import { + createDebugLogger, + EXTENSIONS_CONFIG_FILENAME, + Storage, +} from '@qwen-code/qwen-code-core'; import type { ICommandLoader } from './types.js'; import { parseMarkdownCommand, @@ -28,6 +32,8 @@ interface CommandDirectory { extensionName?: string; } +const debugLogger = createDebugLogger('FILE_COMMAND_LOADER'); + /** * Defines the Zod schema for a command definition file. This serves as the * single source of truth for both validation and type inference. @@ -129,7 +135,7 @@ export class FileCommandLoader implements ICommandLoader { const isAbortError = error instanceof Error && error.name === 'AbortError'; if (!isEnoent && !isAbortError) { - console.error( + debugLogger.error( `[FileCommandLoader] Error loading commands from ${dirInfo.path}:`, error, ); @@ -214,7 +220,10 @@ export class FileCommandLoader implements ICommandLoader { } } } catch (error) { - console.warn(`Failed to read extension config for ${ext.name}:`, error); + debugLogger.warn( + `Failed to read extension config for ${ext.name}:`, + error, + ); } // Default fallback: use 'commands' directory @@ -246,7 +255,7 @@ export class FileCommandLoader implements ICommandLoader { try { fileContent = await fs.readFile(filePath, 'utf-8'); } catch (error: unknown) { - console.error( + debugLogger.error( `[FileCommandLoader] Failed to read file ${filePath}:`, error instanceof Error ? error.message : String(error), ); @@ -257,7 +266,7 @@ export class FileCommandLoader implements ICommandLoader { try { parsed = toml.parse(fileContent); } catch (error: unknown) { - console.error( + debugLogger.error( `[FileCommandLoader] Failed to parse TOML file ${filePath}:`, error instanceof Error ? error.message : String(error), ); @@ -267,7 +276,7 @@ export class FileCommandLoader implements ICommandLoader { const validationResult = TomlCommandDefSchema.safeParse(parsed); if (!validationResult.success) { - console.error( + debugLogger.error( `[FileCommandLoader] Skipping invalid command file: ${filePath}. Validation errors:`, validationResult.error.flatten(), ); @@ -302,7 +311,7 @@ export class FileCommandLoader implements ICommandLoader { try { fileContent = await fs.readFile(filePath, 'utf-8'); } catch (error: unknown) { - console.error( + debugLogger.error( `[FileCommandLoader] Failed to read file ${filePath}:`, error instanceof Error ? error.message : String(error), ); @@ -313,7 +322,7 @@ export class FileCommandLoader implements ICommandLoader { try { parsed = parseMarkdownCommand(fileContent); } catch (error: unknown) { - console.error( + debugLogger.error( `[FileCommandLoader] Failed to parse Markdown file ${filePath}:`, error instanceof Error ? error.message : String(error), ); @@ -323,7 +332,7 @@ export class FileCommandLoader implements ICommandLoader { const validationResult = MarkdownCommandDefSchema.safeParse(parsed); if (!validationResult.success) { - console.error( + debugLogger.error( `[FileCommandLoader] Skipping invalid command file: ${filePath}. Validation errors:`, validationResult.error.flatten(), ); diff --git a/packages/cli/src/services/command-factory.ts b/packages/cli/src/services/command-factory.ts index 95eec70c3..1720401e1 100644 --- a/packages/cli/src/services/command-factory.ts +++ b/packages/cli/src/services/command-factory.ts @@ -10,6 +10,7 @@ */ import path from 'node:path'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; import type { CommandContext, SlashCommand, @@ -37,6 +38,8 @@ export interface CommandDefinition { description?: string; } +const debugLogger = createDebugLogger('COMMAND_FACTORY'); + /** * Creates a SlashCommand from a parsed command definition. * This function is used by both TOML and Markdown command loaders. @@ -113,7 +116,7 @@ export function createSlashCommandFromDefinition( _args: string, ): Promise => { if (!context.invocation) { - console.error( + debugLogger.error( `[FileCommandLoader] Critical error: Command '${baseCommandName}' was executed without invocation context.`, ); return { diff --git a/packages/cli/src/services/prompt-processors/atFileProcessor.ts b/packages/cli/src/services/prompt-processors/atFileProcessor.ts index 3d8737b1f..118b1c1e0 100644 --- a/packages/cli/src/services/prompt-processors/atFileProcessor.ts +++ b/packages/cli/src/services/prompt-processors/atFileProcessor.ts @@ -7,6 +7,7 @@ import { flatMapTextParts, readPathFromWorkspace, + createDebugLogger, } from '@qwen-code/qwen-code-core'; import type { CommandContext } from '../../ui/commands/types.js'; import { MessageType } from '../../ui/types.js'; @@ -17,6 +18,8 @@ import { } from './types.js'; import { extractInjections } from './injectionParser.js'; +const debugLogger = createDebugLogger('AT_FILE_PROCESSOR'); + export class AtFileProcessor implements IPromptProcessor { constructor(private readonly commandName?: string) {} @@ -68,7 +71,7 @@ export class AtFileProcessor implements IPromptProcessor { error instanceof Error ? error.message : String(error); const uiMessage = `Failed to inject content for '@{${pathStr}}': ${message}`; - console.error( + debugLogger.error( `[AtFileProcessor] ${uiMessage}. Leaving placeholder in prompt.`, ); context.ui.addItem( diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index c5717192d..d83864df5 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -34,6 +34,7 @@ import { type IdeContext, IdeClient, ideContextStore, + createDebugLogger, getErrorMessage, getAllGeminiMdFilenames, ShellExecutionService, @@ -63,6 +64,7 @@ import ansiEscapes from 'ansi-escapes'; import * as fs from 'node:fs'; import { basename } from 'node:path'; import { computeWindowTitle } from '../utils/windowTitle.js'; +import { clearScreen } from '../utils/stdioHelpers.js'; import { useTextBuffer } from './components/shared/text-buffer.js'; import { useLogger } from './hooks/useLogger.js'; import { useGeminiStream } from './hooks/useGeminiStream.js'; @@ -110,6 +112,7 @@ import { } from '../commands/extensions/consent.js'; const CTRL_EXIT_PROMPT_DURATION_MS = 1000; +const debugLogger = createDebugLogger('APP_CONTAINER'); function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) { return pendingHistoryItems.some((item) => { @@ -627,14 +630,12 @@ export const AppContainer = (props: AppContainerProps) => { }, Date.now(), ); - if (config.getDebugMode()) { - console.log( - `[DEBUG] Refreshed memory content in config: ${memoryContent.substring( - 0, - 200, - )}...`, - ); - } + debugLogger.debug( + `[DEBUG] Refreshed memory content in config: ${memoryContent.substring( + 0, + 200, + )}...`, + ); } catch (error) { const errorMessage = getErrorMessage(error); historyManager.addItem( @@ -644,7 +645,7 @@ export const AppContainer = (props: AppContainerProps) => { }, Date.now(), ); - console.error('Error refreshing memory:', error); + debugLogger.error('Error refreshing memory:', error); } }, [config, historyManager, settings.merged]); @@ -749,7 +750,7 @@ export const AppContainer = (props: AppContainerProps) => { const handleClearScreen = useCallback(() => { historyManager.clearItems(); clearConsoleMessagesState(); - console.clear(); + clearScreen(); refreshStatic(); }, [historyManager, clearConsoleMessagesState, refreshStatic]); @@ -1159,7 +1160,7 @@ export const AppContainer = (props: AppContainerProps) => { (key: Key) => { // Debug log keystrokes if enabled if (settings.merged.general?.debugKeystrokeLogging) { - console.log('[DEBUG] Keystroke:', JSON.stringify(key)); + debugLogger.debug('[DEBUG] Keystroke:', JSON.stringify(key)); } if (keyMatchers[Command.QUIT](key)) { diff --git a/packages/cli/src/ui/commands/extensionsCommand.ts b/packages/cli/src/ui/commands/extensionsCommand.ts index e13df24f7..132f92901 100644 --- a/packages/cli/src/ui/commands/extensionsCommand.ts +++ b/packages/cli/src/ui/commands/extensionsCommand.ts @@ -18,10 +18,12 @@ import { parseInstallSource, type ExtensionUpdateInfo, } from '@qwen-code/qwen-code-core'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; import { SettingScope } from '../../config/settings.js'; import open from 'open'; import { extensionToOutputString } from '../../commands/extensions/utils.js'; +const debugLogger = createDebugLogger('EXTENSIONS_COMMAND'); const EXTENSION_EXPLORE_URL = { Gemini: 'https://geminicli.com/extensions/', ClaudeCode: 'https://claudemarketplaces.com/', @@ -240,7 +242,7 @@ async function updateAction(context: CommandContext, args: string) { async function installAction(context: CommandContext, args: string) { const extensionManager = context.services.config?.getExtensionManager(); if (!(extensionManager instanceof ExtensionManager)) { - console.error( + debugLogger.error( `Cannot ${context.invocation?.name} extensions in this environment`, ); return; @@ -297,7 +299,7 @@ async function installAction(context: CommandContext, args: string) { async function uninstallAction(context: CommandContext, args: string) { const extensionManager = context.services.config?.getExtensionManager(); if (!(extensionManager instanceof ExtensionManager)) { - console.error( + debugLogger.error( `Cannot ${context.invocation?.name} extensions in this environment`, ); return; @@ -357,7 +359,7 @@ function getEnableDisableContext( } | null { const extensionManager = context.services.config?.getExtensionManager(); if (!(extensionManager instanceof ExtensionManager)) { - console.error( + debugLogger.error( `Cannot ${context.invocation?.name} extensions in this environment`, ); return null; @@ -479,7 +481,7 @@ async function enableAction(context: CommandContext, args: string) { async function detailAction(context: CommandContext, args: string) { const extensionManager = context.services.config?.getExtensionManager(); if (!(extensionManager instanceof ExtensionManager)) { - console.error( + debugLogger.error( `Cannot ${context.invocation?.name} extensions in this environment`, ); return; diff --git a/packages/cli/src/ui/commands/languageCommand.ts b/packages/cli/src/ui/commands/languageCommand.ts index e4158ce5c..f75c628bb 100644 --- a/packages/cli/src/ui/commands/languageCommand.ts +++ b/packages/cli/src/ui/commands/languageCommand.ts @@ -25,6 +25,9 @@ import { resolveOutputLanguage, updateOutputLanguageFile, } from '../../utils/languageUtils.js'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; + +const debugLogger = createDebugLogger('LANGUAGE_COMMAND'); /** * Gets the current LLM output language setting and its resolved value. @@ -94,7 +97,7 @@ async function setUiLanguage( try { services.settings.setValue(SettingScope.User, 'general.language', lang); } catch (error) { - console.warn('Failed to save language setting:', error); + debugLogger.warn('Failed to save language setting:', error); } } @@ -136,7 +139,7 @@ async function setOutputLanguage( settingValue, ); } catch (error) { - console.warn('Failed to save output language setting:', error); + debugLogger.warn('Failed to save output language setting:', error); } } diff --git a/packages/cli/src/ui/commands/skillsCommand.ts b/packages/cli/src/ui/commands/skillsCommand.ts index 8e41a1ce9..c154a479e 100644 --- a/packages/cli/src/ui/commands/skillsCommand.ts +++ b/packages/cli/src/ui/commands/skillsCommand.ts @@ -14,6 +14,9 @@ import { MessageType, type HistoryItemSkillsList } from '../types.js'; import { t } from '../../i18n/index.js'; import { AsyncFzf } from 'fzf'; import type { SkillConfig } from '@qwen-code/qwen-code-core'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; + +const debugLogger = createDebugLogger('SKILLS_COMMAND'); export const skillsCommand: SlashCommand = { name: 'skills', @@ -123,7 +126,7 @@ async function getSkillMatches( .map((result) => skillMap.get(result.item)) .filter((skill): skill is SkillConfig => !!skill); } catch (error) { - console.error('[skillsCommand] Fuzzy match failed:', error); + debugLogger.error('[skillsCommand] Fuzzy match failed:', error); const lowerQuery = query.toLowerCase(); return skills.filter((skill) => skill.name.toLowerCase().startsWith(lowerQuery), diff --git a/packages/cli/src/ui/components/EditorSettingsDialog.tsx b/packages/cli/src/ui/components/EditorSettingsDialog.tsx index cd77c230e..f2df5edaa 100644 --- a/packages/cli/src/ui/components/EditorSettingsDialog.tsx +++ b/packages/cli/src/ui/components/EditorSettingsDialog.tsx @@ -18,10 +18,15 @@ import { ScopeSelector } from './shared/ScopeSelector.js'; import type { LoadedSettings } from '../../config/settings.js'; import { SettingScope } from '../../config/settings.js'; import type { EditorType } from '@qwen-code/qwen-code-core'; -import { isEditorAvailable } from '@qwen-code/qwen-code-core'; +import { + createDebugLogger, + isEditorAvailable, +} from '@qwen-code/qwen-code-core'; import { useKeypress } from '../hooks/useKeypress.js'; import { t } from '../../i18n/index.js'; +const debugLogger = createDebugLogger('EDITOR_SETTINGS_DIALOG'); + interface EditorDialogProps { onSelect: (editorType: EditorType | undefined, scope: SettingScope) => void; settings: LoadedSettings; @@ -61,7 +66,7 @@ export function EditorSettingsDialog({ ) : 0; if (editorIndex === -1) { - console.error(`Editor is not supported: ${currentPreference}`); + debugLogger.error(`Editor is not supported: ${currentPreference}`); editorIndex = 0; } diff --git a/packages/cli/src/ui/components/IdeTrustChangeDialog.tsx b/packages/cli/src/ui/components/IdeTrustChangeDialog.tsx index ba1288afc..6780792ef 100644 --- a/packages/cli/src/ui/components/IdeTrustChangeDialog.tsx +++ b/packages/cli/src/ui/components/IdeTrustChangeDialog.tsx @@ -9,11 +9,14 @@ import { theme } from '../semantic-colors.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { relaunchApp } from '../../utils/processUtils.js'; import { type RestartReason } from '../hooks/useIdeTrustListener.js'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; interface IdeTrustChangeDialogProps { reason: RestartReason; } +const debugLogger = createDebugLogger('IDE_TRUST_DIALOG'); + export const IdeTrustChangeDialog = ({ reason }: IdeTrustChangeDialogProps) => { useKeypress( (key) => { @@ -27,7 +30,7 @@ export const IdeTrustChangeDialog = ({ reason }: IdeTrustChangeDialogProps) => { let message = 'Workspace trust has changed.'; if (reason === 'NONE') { // This should not happen, but provides a fallback and a debug log. - console.error( + debugLogger.error( 'IdeTrustChangeDialog rendered with unexpected reason "NONE"', ); } else if (reason === 'CONNECTION_CHANGE') { diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 1d46d03ab..2e564ca7c 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -22,7 +22,7 @@ import { useKeypress } from '../hooks/useKeypress.js'; import { keyMatchers, Command } from '../keyMatchers.js'; import type { CommandContext, SlashCommand } from '../commands/types.js'; import type { Config } from '@qwen-code/qwen-code-core'; -import { ApprovalMode } from '@qwen-code/qwen-code-core'; +import { ApprovalMode, createDebugLogger } from '@qwen-code/qwen-code-core'; import { parseInputForHighlighting, buildSegmentsForVisualSlice, @@ -38,6 +38,8 @@ import { SCREEN_READER_USER_PREFIX } from '../textConstants.js'; import { useShellFocusState } from '../contexts/ShellFocusContext.js'; import { useUIState } from '../contexts/UIStateContext.js'; import { FEEDBACK_DIALOG_KEYS } from '../FeedbackDialog.js'; + +const debugLogger = createDebugLogger('INPUT_PROMPT'); export interface InputPromptProps { buffer: TextBuffer; onSubmit: (value: string) => void; @@ -299,7 +301,7 @@ export const InputPrompt: React.FC = ({ } } } catch (error) { - console.error('Error handling clipboard image:', error); + debugLogger.error('Error handling clipboard image:', error); } }, [buffer, config]); diff --git a/packages/cli/src/ui/components/QwenOAuthProgress.tsx b/packages/cli/src/ui/components/QwenOAuthProgress.tsx index d83bfb044..69d42818d 100644 --- a/packages/cli/src/ui/components/QwenOAuthProgress.tsx +++ b/packages/cli/src/ui/components/QwenOAuthProgress.tsx @@ -12,6 +12,7 @@ import Link from 'ink-link'; import qrcode from 'qrcode-terminal'; import { Colors } from '../colors.js'; import type { DeviceAuthorizationData } from '@qwen-code/qwen-code-core'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; import { useKeypress } from '../hooks/useKeypress.js'; import { t } from '../../i18n/index.js'; @@ -29,6 +30,8 @@ interface QwenOAuthProgressProps { authMessage?: string | null; } +const debugLogger = createDebugLogger('QWEN_OAUTH_PROGRESS'); + /** * Static QR Code Display Component * Renders the QR code and URL once and doesn't re-render unless the URL changes @@ -161,7 +164,7 @@ export function QwenOAuthProgress({ }, ); } catch (error) { - console.error('Failed to generate QR code:', error); + debugLogger.error('Failed to generate QR code:', error); setQrCodeData(null); } }; diff --git a/packages/cli/src/ui/components/SettingsDialog.tsx b/packages/cli/src/ui/components/SettingsDialog.tsx index b3bd1c270..8a0decb71 100644 --- a/packages/cli/src/ui/components/SettingsDialog.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.tsx @@ -29,7 +29,7 @@ import { } from '../../utils/settingsUtils.js'; import { updateOutputLanguageFile } from '../../utils/languageUtils.js'; import { useVimMode } from '../contexts/VimModeContext.js'; -import { type Config } from '@qwen-code/qwen-code-core'; +import { createDebugLogger, type Config } from '@qwen-code/qwen-code-core'; import { useKeypress } from '../hooks/useKeypress.js'; import chalk from 'chalk'; import { cpSlice, cpLen, stripUnsafeCharacters } from '../utils/textUtils.js'; @@ -46,6 +46,8 @@ interface SettingsDialogProps { config?: Config; } +const debugLogger = createDebugLogger('SETTINGS_DIALOG'); + const maxItemsToShow = 8; export function SettingsDialog({ @@ -162,7 +164,7 @@ export function SettingsDialog({ {} as Settings, ); - console.log( + debugLogger.debug( `[DEBUG SettingsDialog] Saving ${key} immediately with value:`, newValue, ); @@ -177,7 +179,7 @@ export function SettingsDialog({ if (key === 'general.vimMode' && newValue !== vimEnabled) { // Call toggleVimEnabled to sync the VimModeContext local state toggleVimEnabled().catch((error) => { - console.error('Failed to toggle vim mode:', error); + debugLogger.error('Failed to toggle vim mode:', error); }); } @@ -189,7 +191,7 @@ export function SettingsDialog({ try { config?.setApprovalMode(settings.merged.tools.approvalMode); } catch (error) { - console.error( + debugLogger.error( 'Failed to apply approval mode to current session:', error, ); @@ -663,7 +665,7 @@ export function SettingsDialog({ try { config?.setApprovalMode(settings.merged.tools.approvalMode); } catch (error) { - console.error( + debugLogger.error( 'Failed to apply approval mode to current session:', error, ); diff --git a/packages/cli/src/ui/components/StandaloneSessionPicker.tsx b/packages/cli/src/ui/components/StandaloneSessionPicker.tsx index 13a176c62..244258d79 100644 --- a/packages/cli/src/ui/components/StandaloneSessionPicker.tsx +++ b/packages/cli/src/ui/components/StandaloneSessionPicker.tsx @@ -9,6 +9,7 @@ import { render, Box, useApp } from 'ink'; import { getGitBranch, SessionService } from '@qwen-code/qwen-code-core'; import { KeypressProvider } from '../contexts/KeypressContext.js'; import { SessionPicker } from './SessionPicker.js'; +import { writeStdoutLine } from '../../utils/stdioHelpers.js'; interface StandalonePickerScreenProps { sessionService: SessionService; @@ -70,7 +71,7 @@ export async function showResumeSessionPicker( const sessionService = new SessionService(cwd); const hasSession = await sessionService.loadLastSession(); if (!hasSession) { - console.log('No sessions found. Start a new session with `qwen`.'); + writeStdoutLine('No sessions found. Start a new session with `qwen`.'); return undefined; } diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx index 86b36cd28..0572a0f76 100644 --- a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx +++ b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx @@ -10,8 +10,10 @@ import stringWidth from 'string-width'; import { theme } from '../../semantic-colors.js'; import { toCodePoints } from '../../utils/textUtils.js'; import { useOverflowActions } from '../../contexts/OverflowContext.js'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; let enableDebugLog = false; +const debugLogger = createDebugLogger('MAX_SIZED_BOX'); /** * Minimum height for the MaxSizedBox component. @@ -28,7 +30,7 @@ function debugReportError(message: string, element: React.ReactNode) { if (!enableDebugLog) return; if (!React.isValidElement(element)) { - console.error( + debugLogger.error( message, `Invalid element: '${String(element)}' typeof=${typeof element}`, ); @@ -44,10 +46,13 @@ function debugReportError(message: string, element: React.ReactNode) { const lineNumber = elementWithSource._source?.lineNumber; sourceMessage = fileName ? `${fileName}:${lineNumber}` : ''; } catch (error) { - console.error('Error while trying to get file name:', error); + debugLogger.error('Error while trying to get file name:', error); } - console.error(message, `${String(element.type)}. Source: ${sourceMessage}`); + debugLogger.error( + message, + `${String(element.type)}. Source: ${sourceMessage}`, + ); } interface MaxSizedBoxProps { children?: React.ReactNode; diff --git a/packages/cli/src/ui/components/shared/text-buffer.ts b/packages/cli/src/ui/components/shared/text-buffer.ts index f3aaf9c88..baed1c192 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.ts @@ -9,7 +9,7 @@ import fs from 'node:fs'; import os from 'node:os'; import pathMod from 'node:path'; import { useState, useCallback, useEffect, useMemo, useReducer } from 'react'; -import { unescapePath } from '@qwen-code/qwen-code-core'; +import { createDebugLogger, unescapePath } from '@qwen-code/qwen-code-core'; import { toCodePoints, cpLen, @@ -20,6 +20,8 @@ import { import type { VimAction } from './vim-buffer-actions.js'; import { handleVimAction } from './vim-buffer-actions.js'; +const debugLogger = createDebugLogger('TEXT_BUFFER'); + export type Direction = | 'left' | 'right' @@ -1143,7 +1145,7 @@ function textBufferReducerLogic( break; default: { const exhaustiveCheck: never = dir; - console.error( + debugLogger.error( `Unknown visual movement direction: ${exhaustiveCheck}`, ); return state; @@ -1489,7 +1491,7 @@ function textBufferReducerLogic( default: { const exhaustiveCheck: never = action; - console.error(`Unknown action encountered: ${exhaustiveCheck}`); + debugLogger.error(`Unknown action encountered: ${exhaustiveCheck}`); return state; } } @@ -1858,7 +1860,7 @@ export function useTextBuffer({ newText = newText.replace(/\r\n?/g, '\n'); dispatch({ type: 'set_text', payload: newText, pushToUndo: false }); } catch (err) { - console.error('[useTextBuffer] external editor error', err); + debugLogger.error('[useTextBuffer] external editor error', err); } finally { if (wasRaw) setRawMode?.(true); try { diff --git a/packages/cli/src/ui/components/subagents/create/CreationSummary.tsx b/packages/cli/src/ui/components/subagents/create/CreationSummary.tsx index f9174b663..0cc899b87 100644 --- a/packages/cli/src/ui/components/subagents/create/CreationSummary.tsx +++ b/packages/cli/src/ui/components/subagents/create/CreationSummary.tsx @@ -11,12 +11,15 @@ import type { SubagentManager, SubagentConfig, } from '@qwen-code/qwen-code-core'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; import { theme } from '../../../semantic-colors.js'; import { shouldShowColor, getColorForDisplay } from '../utils.js'; import { useLaunchEditor } from '../../../hooks/useLaunchEditor.js'; import { useKeypress } from '../../../hooks/useKeypress.js'; import { t } from '../../../../i18n/index.js'; +const debugLogger = createDebugLogger('SUBAGENT_CREATION_SUMMARY'); + /** * Step 6: Final confirmation and actions. */ @@ -87,7 +90,7 @@ export function CreationSummary({ } } catch (error) { // Silently handle errors in warning checks - console.warn('Error checking subagent name availability:', error); + debugLogger.warn('Error checking subagent name availability:', error); } // Check length warnings diff --git a/packages/cli/src/ui/components/subagents/manage/AgentDeleteStep.tsx b/packages/cli/src/ui/components/subagents/manage/AgentDeleteStep.tsx index 77cfa47d7..215fe4bb3 100644 --- a/packages/cli/src/ui/components/subagents/manage/AgentDeleteStep.tsx +++ b/packages/cli/src/ui/components/subagents/manage/AgentDeleteStep.tsx @@ -6,6 +6,7 @@ import { Box, Text } from 'ink'; import { type SubagentConfig } from '@qwen-code/qwen-code-core'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; import type { StepNavigationProps } from '../types.js'; import { theme } from '../../../semantic-colors.js'; import { useKeypress } from '../../../hooks/useKeypress.js'; @@ -16,6 +17,8 @@ interface AgentDeleteStepProps extends StepNavigationProps { onDelete: (agent: SubagentConfig) => Promise; } +const debugLogger = createDebugLogger('AGENT_DELETE_STEP'); + export function AgentDeleteStep({ selectedAgent, onDelete, @@ -30,7 +33,7 @@ export function AgentDeleteStep({ await onDelete(selectedAgent); // Navigation will be handled by the parent component after successful deletion } catch (error) { - console.error('Failed to delete agent:', error); + debugLogger.error('Failed to delete agent:', error); } } else if (key.name === 'n') { onNavigateBack(); diff --git a/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx b/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx index f2a5f02e2..79b859707 100644 --- a/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx +++ b/packages/cli/src/ui/components/subagents/manage/AgentsManagerDialog.tsx @@ -17,6 +17,7 @@ import { MANAGEMENT_STEPS } from '../types.js'; import { theme } from '../../../semantic-colors.js'; import { getColorForDisplay, shouldShowColor } from '../utils.js'; import type { SubagentConfig, Config } from '@qwen-code/qwen-code-core'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; import { useKeypress } from '../../../hooks/useKeypress.js'; import { t } from '../../../../i18n/index.js'; @@ -25,6 +26,8 @@ interface AgentsManagerDialogProps { config: Config | null; } +const debugLogger = createDebugLogger('AGENTS_MANAGER_DIALOG'); + /** * Main orchestrator component for the agents management dialog. */ @@ -108,7 +111,7 @@ export function AgentsManagerDialog({ setNavigationStack([MANAGEMENT_STEPS.AGENT_SELECTION]); setSelectedAgentIndex(-1); } catch (error) { - console.error('Failed to delete agent:', error); + debugLogger.error('Failed to delete agent:', error); throw error; // Re-throw to let the component handle the error state } }, @@ -253,7 +256,7 @@ export function AgentsManagerDialog({ await loadAgents(); handleNavigateBack(); } catch (error) { - console.error('Failed to save agent changes:', error); + debugLogger.error('Failed to save agent changes:', error); } } }} @@ -282,7 +285,7 @@ export function AgentsManagerDialog({ await loadAgents(); handleNavigateBack(); } catch (error) { - console.error('Failed to save color changes:', error); + debugLogger.error('Failed to save color changes:', error); } } }} diff --git a/packages/cli/src/ui/components/views/ExtensionsList.tsx b/packages/cli/src/ui/components/views/ExtensionsList.tsx index 50b87d8c4..316e66687 100644 --- a/packages/cli/src/ui/components/views/ExtensionsList.tsx +++ b/packages/cli/src/ui/components/views/ExtensionsList.tsx @@ -7,6 +7,9 @@ import { Box, Text } from 'ink'; import { useUIState } from '../../contexts/UIStateContext.js'; import { ExtensionUpdateState } from '../../state/extensions.js'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; + +const debugLogger = createDebugLogger('EXTENSIONS_LIST'); export const ExtensionsList = () => { const { extensionsUpdateState, commandContext } = useUIState(); @@ -47,7 +50,7 @@ export const ExtensionsList = () => { stateColor = 'green'; break; default: - console.error(`Unhandled ExtensionUpdateState ${state}`); + debugLogger.error(`Unhandled ExtensionUpdateState ${state}`); break; } diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index 0f01712cc..bdd8c33d8 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -8,6 +8,7 @@ import type { Config } from '@qwen-code/qwen-code-core'; import { KittySequenceOverflowEvent, logKittySequenceOverflow, + createDebugLogger, } from '@qwen-code/qwen-code-core'; import { useStdin } from 'ink'; import type React from 'react'; @@ -65,6 +66,7 @@ interface KeypressContextValue { const KeypressContext = createContext( undefined, ); +const debugLogger = createDebugLogger('KEYPRESS'); export function useKeypressContext() { const context = useContext(KeypressContext); @@ -486,7 +488,7 @@ export function KeypressProvider({ key.sequence === `${ESC}${KITTY_CTRL_C}` ) { if (kittySequenceBuffer && debugKeystrokeLogging) { - console.log( + debugLogger.debug( '[DEBUG] Kitty buffer cleared on Ctrl+C:', kittySequenceBuffer, ); @@ -520,7 +522,7 @@ export function KeypressProvider({ kittySequenceBuffer += key.sequence; if (debugKeystrokeLogging) { - console.log( + debugLogger.debug( '[DEBUG] Kitty buffer accumulating:', kittySequenceBuffer, ); @@ -538,7 +540,7 @@ export function KeypressProvider({ const nextStart = kittySequenceBuffer.indexOf(`${ESC}[`, 1); if (nextStart > 0) { if (debugKeystrokeLogging) { - console.log( + debugLogger.debug( '[DEBUG] Skipping incomplete/invalid CSI prefix:', kittySequenceBuffer.slice(0, nextStart), ); @@ -554,12 +556,12 @@ export function KeypressProvider({ parsed.length, ); if (kittySequenceBuffer.length > parsed.length) { - console.log( + debugLogger.debug( '[DEBUG] Kitty sequence parsed successfully (prefix):', parsedSequence, ); } else { - console.log( + debugLogger.debug( '[DEBUG] Kitty sequence parsed successfully:', parsedSequence, ); @@ -576,12 +578,12 @@ export function KeypressProvider({ const codes = Array.from(kittySequenceBuffer).map((ch) => ch.charCodeAt(0), ); - console.warn('Kitty sequence buffer has char codes:', codes); + debugLogger.warn('Kitty sequence buffer has char codes:', codes); } if (kittySequenceBuffer.length > MAX_KITTY_SEQUENCE_LENGTH) { if (debugKeystrokeLogging) { - console.log( + debugLogger.debug( '[DEBUG] Kitty buffer overflow, clearing:', kittySequenceBuffer, ); diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.ts b/packages/cli/src/ui/hooks/atCommandProcessor.ts index f3e41956b..034f8552a 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.ts @@ -9,6 +9,7 @@ import * as path from 'node:path'; import type { PartListUnion, PartUnion } from '@google/genai'; import type { AnyToolInvocation, Config } from '@qwen-code/qwen-code-core'; import { + createDebugLogger, getErrorMessage, isNodeError, unescapePath, @@ -36,6 +37,8 @@ interface AtCommandPart { content: string; } +const debugLogger = createDebugLogger('AT_COMMAND_PROCESSOR'); + /** * Parses a query string to find all '@' commands and text segments. * Handles \ escaped spaces within paths. @@ -280,7 +283,7 @@ export async function handleAtCommand({ ); } } catch (globError) { - console.error( + debugLogger.error( `Error during glob search for ${pathName}: ${getErrorMessage(globError)}`, ); onDebugMessage( @@ -293,7 +296,7 @@ export async function handleAtCommand({ ); } } else { - console.error( + debugLogger.error( `Error stating path ${pathName}: ${getErrorMessage(error)}`, ); onDebugMessage( @@ -372,7 +375,7 @@ export async function handleAtCommand({ } const message = `Ignored ${totalIgnored} files:\n${messages.join('\n')}`; - console.log(message); + debugLogger.info(message); onDebugMessage(message); } diff --git a/packages/cli/src/ui/hooks/shellCommandProcessor.ts b/packages/cli/src/ui/hooks/shellCommandProcessor.ts index fdc69c2e0..65037942b 100644 --- a/packages/cli/src/ui/hooks/shellCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/shellCommandProcessor.ts @@ -16,7 +16,11 @@ import type { GeminiClient, ShellExecutionResult, } from '@qwen-code/qwen-code-core'; -import { isBinary, ShellExecutionService } from '@qwen-code/qwen-code-core'; +import { + createDebugLogger, + isBinary, + ShellExecutionService, +} from '@qwen-code/qwen-code-core'; import { type PartListUnion } from '@google/genai'; import type { UseHistoryManagerReturn } from './useHistoryManager.js'; import { SHELL_COMMAND_NAME } from '../constants.js'; @@ -29,6 +33,7 @@ import { themeManager } from '../../ui/themes/theme-manager.js'; export const OUTPUT_UPDATE_INTERVAL_MS = 1000; const MAX_OUTPUT_LENGTH = 10000; +const debugLogger = createDebugLogger('SHELL_COMMAND_PROCESSOR'); function addShellCommandToGeminiHistory( geminiClient: GeminiClient, @@ -231,7 +236,7 @@ export const useShellCommandProcessor = ( shellExecutionConfig, ); - console.log(terminalHeight, terminalWidth); + debugLogger.debug(terminalHeight, terminalWidth); executionPid = pid; if (pid) { diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index ba2b53fc5..59ff06bcf 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -10,6 +10,7 @@ import type { UseHistoryManagerReturn } from './useHistoryManager.js'; import { type Logger, type Config, + createDebugLogger, GitService, logSlashCommand, makeSlashCommandEvent, @@ -33,12 +34,14 @@ import { BuiltinCommandLoader } from '../../services/BuiltinCommandLoader.js'; import { FileCommandLoader } from '../../services/FileCommandLoader.js'; import { McpPromptLoader } from '../../services/McpPromptLoader.js'; import { parseSlashCommand } from '../../utils/commands.js'; +import { clearScreen } from '../../utils/stdioHelpers.js'; import { type ExtensionUpdateAction, type ExtensionUpdateStatus, } from '../state/extensions.js'; type SerializableHistoryItem = Record; +const debugLogger = createDebugLogger('SLASH_COMMAND_PROCESSOR'); function serializeHistoryItemForRecording( item: Omit, @@ -200,7 +203,7 @@ export const useSlashCommandProcessor = ( addItem, clear: () => { clearItems(); - console.clear(); + clearScreen(); refreshStatic(); }, loadHistory, @@ -600,12 +603,10 @@ export const useSlashCommandProcessor = ( }); } } catch (recordError) { - if (config.getDebugMode()) { - console.error( - '[slashCommand] Failed to record slash command:', - recordError, - ); - } + debugLogger.error( + '[slashCommand] Failed to record slash command:', + recordError, + ); } } if (config && resolvedCommandPath[0] && !hasError) { diff --git a/packages/cli/src/ui/hooks/useFeedbackDialog.ts b/packages/cli/src/ui/hooks/useFeedbackDialog.ts index 18865b1f0..b4392027c 100644 --- a/packages/cli/src/ui/hooks/useFeedbackDialog.ts +++ b/packages/cli/src/ui/hooks/useFeedbackDialog.ts @@ -2,6 +2,7 @@ import { useState, useCallback, useEffect } from 'react'; import * as fs from 'node:fs'; import { type Config, + createDebugLogger, logUserFeedback, UserFeedbackEvent, type UserFeedbackRating, @@ -23,6 +24,7 @@ const MIN_USER_MESSAGES = 5; // Minimum user messages to show feedback dialog // Fatigue mechanism constants const FEEDBACK_COOLDOWN_HOURS = 24; // Hours to wait before showing feedback dialog again +const debugLogger = createDebugLogger('FEEDBACK_DIALOG'); /** * Check if the last message in the conversation history is an AI response @@ -42,7 +44,7 @@ const getFeedbackLastShownTimestampFromFile = (): number => { } } catch (error) { if (isNodeError(error) && error.code !== 'ENOENT') { - console.warn( + debugLogger.warn( 'Failed to read feedbackLastShownTimestamp from settings file:', error, ); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 561c98ed6..c7f4395e0 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -19,6 +19,7 @@ import type { } from '@qwen-code/qwen-code-core'; import { GeminiEventType as ServerGeminiEventType, + createDebugLogger, getErrorMessage, isNodeError, MessageSenderType, @@ -65,6 +66,8 @@ import { useSessionStats } from '../contexts/SessionContext.js'; import { useKeypress } from './useKeypress.js'; import type { LoadedSettings } from '../../config/settings.js'; +const debugLogger = createDebugLogger('GEMINI_STREAM'); + enum StreamProcessingStatus { Completed, UserCancelled, @@ -990,7 +993,7 @@ export const useGeminiStream = ( if (processingStatus === StreamProcessingStatus.UserCancelled) { // Restore original model if it was temporarily overridden restoreOriginalModel().catch((error) => { - console.error('Failed to restore original model:', error); + debugLogger.error('Failed to restore original model:', error); }); isSubmittingQueryRef.current = false; return; @@ -1007,12 +1010,12 @@ export const useGeminiStream = ( // Restore original model if it was temporarily overridden restoreOriginalModel().catch((error) => { - console.error('Failed to restore original model:', error); + debugLogger.error('Failed to restore original model:', error); }); } catch (error: unknown) { // Restore original model if it was temporarily overridden restoreOriginalModel().catch((error) => { - console.error('Failed to restore original model:', error); + debugLogger.error('Failed to restore original model:', error); }); if (error instanceof UnauthorizedError) { @@ -1082,7 +1085,7 @@ export const useGeminiStream = ( ToolConfirmationOutcome.ProceedOnce, ); } catch (error) { - console.error( + debugLogger.error( `Failed to auto-approve tool call ${call.request.callId}:`, error, ); diff --git a/packages/cli/src/ui/hooks/useInputHistoryStore.ts b/packages/cli/src/ui/hooks/useInputHistoryStore.ts index 86e7cd396..f879e31f0 100644 --- a/packages/cli/src/ui/hooks/useInputHistoryStore.ts +++ b/packages/cli/src/ui/hooks/useInputHistoryStore.ts @@ -5,6 +5,7 @@ */ import { useState, useCallback } from 'react'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; interface Logger { getPreviousUserMessages(): Promise; @@ -16,6 +17,8 @@ export interface UseInputHistoryStoreReturn { initializeFromLogger: (logger: Logger | null) => Promise; } +const debugLogger = createDebugLogger('INPUT_HISTORY_STORE'); + /** * Hook for independently managing input history. * Completely separated from chat history and unaffected by /clear commands. @@ -69,7 +72,10 @@ export function useInputHistoryStore(): UseInputHistoryStoreReturn { setIsInitialized(true); } catch (error) { // Start with empty history even if logger initialization fails - console.warn('Failed to initialize input history from logger:', error); + debugLogger.warn( + 'Failed to initialize input history from logger:', + error, + ); setPastSessionMessages([]); recalculateHistory([], []); setIsInitialized(true); diff --git a/packages/cli/src/ui/hooks/useReactToolScheduler.ts b/packages/cli/src/ui/hooks/useReactToolScheduler.ts index 5542718f1..56992f678 100644 --- a/packages/cli/src/ui/hooks/useReactToolScheduler.ts +++ b/packages/cli/src/ui/hooks/useReactToolScheduler.ts @@ -20,7 +20,10 @@ import type { Status as CoreStatus, EditorType, } from '@qwen-code/qwen-code-core'; -import { CoreToolScheduler } from '@qwen-code/qwen-code-core'; +import { + CoreToolScheduler, + createDebugLogger, +} from '@qwen-code/qwen-code-core'; import { useCallback, useState, useMemo } from 'react'; import type { HistoryItemToolGroup, @@ -28,6 +31,8 @@ import type { } from '../types.js'; import { ToolCallStatus } from '../types.js'; +const debugLogger = createDebugLogger('REACT_TOOL_SCHEDULER'); + export type ScheduleFn = ( request: ToolCallRequestInfo | ToolCallRequestInfo[], signal: AbortSignal, @@ -198,7 +203,7 @@ function mapCoreStatusToDisplayStatus(coreStatus: CoreStatus): ToolCallStatus { return ToolCallStatus.Pending; default: { const exhaustiveCheck: never = coreStatus; - console.warn(`Unknown core status encountered: ${exhaustiveCheck}`); + debugLogger.warn(`Unknown core status encountered: ${exhaustiveCheck}`); return ToolCallStatus.Error; } } diff --git a/packages/cli/src/ui/hooks/useSelectionList.ts b/packages/cli/src/ui/hooks/useSelectionList.ts index 5e9a7f181..c09aec802 100644 --- a/packages/cli/src/ui/hooks/useSelectionList.ts +++ b/packages/cli/src/ui/hooks/useSelectionList.ts @@ -5,6 +5,7 @@ */ import { useReducer, useRef, useEffect } from 'react'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; import { useKeypress } from './useKeypress.js'; export interface SelectionListItem { @@ -22,6 +23,8 @@ export interface UseSelectionListOptions { showNumbers?: boolean; } +const debugLogger = createDebugLogger('SELECTION_LIST'); + export interface UseSelectionListResult { activeIndex: number; setActiveIndex: (index: number) => void; @@ -203,7 +206,7 @@ function selectionListReducer( default: { const exhaustiveCheck: never = action; - console.error(`Unknown selection list action: ${exhaustiveCheck}`); + debugLogger.error(`Unknown selection list action: ${exhaustiveCheck}`); return state; } } diff --git a/packages/cli/src/ui/hooks/useShellHistory.ts b/packages/cli/src/ui/hooks/useShellHistory.ts index a25965309..69358d890 100644 --- a/packages/cli/src/ui/hooks/useShellHistory.ts +++ b/packages/cli/src/ui/hooks/useShellHistory.ts @@ -7,9 +7,14 @@ import { useState, useEffect, useCallback } from 'react'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; -import { isNodeError, Storage } from '@qwen-code/qwen-code-core'; +import { + createDebugLogger, + isNodeError, + Storage, +} from '@qwen-code/qwen-code-core'; const MAX_HISTORY_LENGTH = 100; +const debugLogger = createDebugLogger('SHELL_HISTORY'); export interface UseShellHistoryReturn { history: string[]; @@ -52,7 +57,7 @@ async function readHistoryFile(filePath: string): Promise { return result; } catch (err) { if (isNodeError(err) && err.code === 'ENOENT') return []; - console.error('Error reading history:', err); + debugLogger.error('Error reading history:', err); return []; } } @@ -65,7 +70,7 @@ async function writeHistoryFile( await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, history.join('\n')); } catch (error) { - console.error('Error writing shell history:', error); + debugLogger.error('Error writing shell history:', error); } } diff --git a/packages/cli/src/ui/hooks/useShowMemoryCommand.ts b/packages/cli/src/ui/hooks/useShowMemoryCommand.ts index 5c9682082..971e775fe 100644 --- a/packages/cli/src/ui/hooks/useShowMemoryCommand.ts +++ b/packages/cli/src/ui/hooks/useShowMemoryCommand.ts @@ -8,6 +8,9 @@ import type { Message } from '../types.js'; import { MessageType } from '../types.js'; import type { Config } from '@qwen-code/qwen-code-core'; import type { LoadedSettings } from '../../config/settings.js'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; + +const debugLogger = createDebugLogger('SHOW_MEMORY'); export function createShowMemoryAction( config: Config | null, @@ -24,11 +27,7 @@ export function createShowMemoryAction( return; } - const debugMode = config.getDebugMode(); - - if (debugMode) { - console.log('[DEBUG] Show Memory command invoked.'); - } + debugLogger.debug('[DEBUG] Show Memory command invoked.'); const currentMemory = config.getUserMemory(); const fileCount = config.getGeminiMdFileCount(); @@ -37,12 +36,10 @@ export function createShowMemoryAction( ? contextFileName : [contextFileName]; - if (debugMode) { - console.log( - `[DEBUG] Showing memory. Content from config.getUserMemory() (first 200 chars): ${currentMemory.substring(0, 200)}...`, - ); - console.log(`[DEBUG] Number of context files loaded: ${fileCount}`); - } + debugLogger.debug( + `[DEBUG] Showing memory. Content from config.getUserMemory() (first 200 chars): ${currentMemory.substring(0, 200)}...`, + ); + debugLogger.debug(`[DEBUG] Number of context files loaded: ${fileCount}`); if (fileCount > 0) { const allNamesTheSame = new Set(contextFileNames).size < 2; diff --git a/packages/cli/src/ui/hooks/useSlashCompletion.ts b/packages/cli/src/ui/hooks/useSlashCompletion.ts index 4d5fd7874..0247523ee 100644 --- a/packages/cli/src/ui/hooks/useSlashCompletion.ts +++ b/packages/cli/src/ui/hooks/useSlashCompletion.ts @@ -6,6 +6,7 @@ import { useState, useEffect, useMemo } from 'react'; import { AsyncFzf } from 'fzf'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; import type { Suggestion } from '../components/SuggestionsDisplay.js'; import { CommandKind, @@ -29,13 +30,15 @@ interface FzfCommandCacheEntry { commandMap: Map; } +const debugLogger = createDebugLogger('SLASH_COMPLETION'); + // Utility function to safely handle errors without information disclosure function logErrorSafely(error: unknown, context: string): void { if (error instanceof Error) { // Log full error details securely for debugging - console.error(`[${context}]`, error); + debugLogger.error(`[${context}]`, error); } else { - console.error(`[${context}] Non-error thrown:`, error); + debugLogger.error(`[${context}] Non-error thrown:`, error); } } @@ -190,7 +193,7 @@ function useCommandSuggestions( // Safety check: ensure leafCommand and completion exist if (!leafCommand?.completion) { - console.warn( + debugLogger.warn( 'Attempted argument completion without completion function', ); return; diff --git a/packages/cli/src/ui/hooks/vim.ts b/packages/cli/src/ui/hooks/vim.ts index 97b73121d..5a91a35a2 100644 --- a/packages/cli/src/ui/hooks/vim.ts +++ b/packages/cli/src/ui/hooks/vim.ts @@ -5,6 +5,7 @@ */ import { useCallback, useReducer, useEffect } from 'react'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; import type { Key } from './useKeypress.js'; import type { TextBuffer } from '../components/shared/text-buffer.js'; import { useVimMode } from '../contexts/VimModeContext.js'; @@ -16,6 +17,8 @@ const DIGIT_MULTIPLIER = 10; const DEFAULT_COUNT = 1; const DIGIT_1_TO_9 = /^[1-9]$/; +const debugLogger = createDebugLogger('VIM_MODE'); + // Command types const CMD_TYPES = { DELETE_WORD_FORWARD: 'dw', @@ -394,7 +397,7 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) { normalizedKey = normalizeKey(key); } catch (error) { // Handle malformed key inputs gracefully - console.warn('Malformed key input in vim mode:', key, error); + debugLogger.warn('Malformed key input in vim mode:', key, error); return false; } diff --git a/packages/cli/src/ui/themes/color-utils.ts b/packages/cli/src/ui/themes/color-utils.ts index a861ee321..08caf6b00 100644 --- a/packages/cli/src/ui/themes/color-utils.ts +++ b/packages/cli/src/ui/themes/color-utils.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { createDebugLogger } from '@qwen-code/qwen-code-core'; + // Mapping from common CSS color names (lowercase) to hex codes (lowercase) // Excludes names directly supported by Ink export const CSS_NAME_TO_HEX_MAP: Readonly> = { @@ -147,6 +149,8 @@ export const CSS_NAME_TO_HEX_MAP: Readonly> = { yellowgreen: '#9acd32', }; +const debugLogger = createDebugLogger('COLOR_UTILS'); + // Define the set of Ink's named colors for quick lookup export const INK_SUPPORTED_NAMES = new Set([ 'black', @@ -224,7 +228,7 @@ export function resolveColor(colorValue: string): string | undefined { } // 4. Could not resolve - console.warn( + debugLogger.warn( `[ColorUtils] Could not resolve color "${colorValue}" to an Ink-compatible format.`, ); return undefined; diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts index 7daa6a290..e4d8c3dfa 100644 --- a/packages/cli/src/ui/themes/theme-manager.ts +++ b/packages/cli/src/ui/themes/theme-manager.ts @@ -27,6 +27,9 @@ import { ANSI } from './ansi.js'; import { ANSILight } from './ansi-light.js'; import { NoColorTheme } from './no-color.js'; import process from 'node:process'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; + +const debugLogger = createDebugLogger('THEME_MANAGER'); export interface ThemeDisplay { name: string; @@ -79,7 +82,7 @@ class ThemeManager { const validation = validateCustomTheme(customThemeConfig); if (validation.isValid) { if (validation.warning) { - console.warn(`Theme "${name}": ${validation.warning}`); + debugLogger.warn(`Theme "${name}": ${validation.warning}`); } const themeWithDefaults: CustomTheme = { ...DEFAULT_THEME.colors, @@ -92,10 +95,10 @@ class ThemeManager { const theme = createCustomTheme(themeWithDefaults); this.customThemes.set(name, theme); } catch (error) { - console.warn(`Failed to load custom theme "${name}":`, error); + debugLogger.warn(`Failed to load custom theme "${name}":`, error); } } else { - console.warn(`Invalid custom theme "${name}": ${validation.error}`); + debugLogger.warn(`Invalid custom theme "${name}": ${validation.error}`); } } // If the current active theme is a custom theme, keep it if still valid @@ -260,7 +263,7 @@ class ThemeManager { // 2. Perform security check. const homeDir = path.resolve(os.homedir()); if (!canonicalPath.startsWith(homeDir)) { - console.warn( + debugLogger.warn( `Theme file at "${themePath}" is outside your home directory. ` + `Only load themes from trusted sources.`, ); @@ -273,14 +276,14 @@ class ThemeManager { const validation = validateCustomTheme(customThemeConfig); if (!validation.isValid) { - console.warn( + debugLogger.warn( `Invalid custom theme from file "${themePath}": ${validation.error}`, ); return undefined; } if (validation.warning) { - console.warn(`Theme from "${themePath}": ${validation.warning}`); + debugLogger.warn(`Theme from "${themePath}": ${validation.warning}`); } // 4. Create and cache the theme. @@ -300,7 +303,10 @@ class ThemeManager { if ( !(error instanceof Error && 'code' in error && error.code === 'ENOENT') ) { - console.warn(`Could not load theme from file "${themePath}":`, error); + debugLogger.warn( + `Could not load theme from file "${themePath}":`, + error, + ); } return undefined; } diff --git a/packages/cli/src/ui/utils/CodeColorizer.tsx b/packages/cli/src/ui/utils/CodeColorizer.tsx index 644248fd0..0dabddb22 100644 --- a/packages/cli/src/ui/utils/CodeColorizer.tsx +++ b/packages/cli/src/ui/utils/CodeColorizer.tsx @@ -21,9 +21,11 @@ import { MINIMUM_MAX_HEIGHT, } from '../components/shared/MaxSizedBox.js'; import type { LoadedSettings } from '../../config/settings.js'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; // Configure theming and parsing utilities. const lowlight = createLowlight(common); +const debugLogger = createDebugLogger('CODE_COLORIZER'); function renderHastNode( node: Root | Element | HastText | RootContent, @@ -188,7 +190,7 @@ export function colorizeCode( ); } catch (error) { - console.error( + debugLogger.error( `[colorizeCode] Error highlighting code for language "${language}":`, error, ); diff --git a/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx b/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx index 48efc6e80..ce31078d1 100644 --- a/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx +++ b/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { Text } from 'ink'; import { theme } from '../semantic-colors.js'; import stringWidth from 'string-width'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; // Constants for Markdown parsing const BOLD_MARKER_LENGTH = 2; // For "**" @@ -17,6 +18,8 @@ const INLINE_CODE_MARKER_LENGTH = 1; // For "`" const UNDERLINE_TAG_START_LENGTH = 3; // For "" const UNDERLINE_TAG_END_LENGTH = 4; // For "" +const debugLogger = createDebugLogger('INLINE_MARKDOWN'); + interface RenderInlineProps { text: string; textColor?: string; @@ -143,7 +146,7 @@ const RenderInlineInternal: React.FC = ({ ); } } catch (e) { - console.error('Error parsing inline markdown part:', fullMatch, e); + debugLogger.error('Error parsing inline markdown part:', fullMatch, e); renderedNode = null; } diff --git a/packages/cli/src/ui/utils/clipboardUtils.ts b/packages/cli/src/ui/utils/clipboardUtils.ts index 9ccca7b6c..d995d0d87 100644 --- a/packages/cli/src/ui/utils/clipboardUtils.ts +++ b/packages/cli/src/ui/utils/clipboardUtils.ts @@ -6,10 +6,12 @@ import * as fs from 'node:fs/promises'; import * as path from 'node:path'; -import { execCommand } from '@qwen-code/qwen-code-core'; +import { createDebugLogger, execCommand } from '@qwen-code/qwen-code-core'; const MACOS_CLIPBOARD_TIMEOUT_MS = 1500; +const debugLogger = createDebugLogger('CLIPBOARD_UTILS'); + /** * Checks if the system clipboard contains an image (macOS only for now) * @returns true if clipboard contains an image @@ -115,7 +117,7 @@ export async function saveClipboardImage( // No format worked return null; } catch (error) { - console.error('Error saving clipboard image:', error); + debugLogger.error('Error saving clipboard image:', error); return null; } } diff --git a/packages/cli/src/ui/utils/commandUtils.ts b/packages/cli/src/ui/utils/commandUtils.ts index 89d1045ac..802107f6b 100644 --- a/packages/cli/src/ui/utils/commandUtils.ts +++ b/packages/cli/src/ui/utils/commandUtils.ts @@ -6,6 +6,7 @@ import type { SpawnOptions } from 'node:child_process'; import { spawn } from 'node:child_process'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; /** * Common Windows console code pages (CP) used for encoding conversions. @@ -61,6 +62,8 @@ export const isSlashCommand = (query: string): boolean => { return true; }; +const debugLogger = createDebugLogger('COMMAND_UTILS'); + // Copies a string snippet to the clipboard for different platforms export const copyToClipboard = async (text: string): Promise => { const run = (cmd: string, args: string[], options?: SpawnOptions) => @@ -162,7 +165,7 @@ export const getUrlOpenCommand = (): string => { default: // Default to xdg-open, which appears to be supported for the less popular operating systems. openCmd = 'xdg-open'; - console.warn( + debugLogger.warn( `Unknown platform: ${process.platform}. Attempting to open URLs with: ${openCmd}.`, ); break; diff --git a/packages/cli/src/ui/utils/terminalSetup.ts b/packages/cli/src/ui/utils/terminalSetup.ts index 04d5aa9c4..2c548eded 100644 --- a/packages/cli/src/ui/utils/terminalSetup.ts +++ b/packages/cli/src/ui/utils/terminalSetup.ts @@ -114,7 +114,7 @@ async function backupFile(filePath: string): Promise { await fs.copyFile(filePath, backupPath); } catch (error) { // Log backup errors but continue with operation - console.warn(`Failed to create backup of ${filePath}:`, error); + debugLogger.warn(`Failed to create backup of ${filePath}:`, error); } } diff --git a/packages/cli/src/ui/utils/updateCheck.ts b/packages/cli/src/ui/utils/updateCheck.ts index cbc538ed5..b13467251 100644 --- a/packages/cli/src/ui/utils/updateCheck.ts +++ b/packages/cli/src/ui/utils/updateCheck.ts @@ -8,6 +8,9 @@ import type { UpdateInfo } from 'update-notifier'; import updateNotifier from 'update-notifier'; import semver from 'semver'; import { getPackageJson } from '../../utils/package.js'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; + +const debugLogger = createDebugLogger('UPDATE_CHECK'); export const FETCH_TIMEOUT_MS = 2000; @@ -95,7 +98,7 @@ export async function checkForUpdates(): Promise { return null; } catch (e) { - console.warn('Failed to check for updates: ' + e); + debugLogger.warn('Failed to check for updates: ' + e); return null; } } diff --git a/packages/cli/src/utils/attentionNotification.ts b/packages/cli/src/utils/attentionNotification.ts index e166444ff..89da6826c 100644 --- a/packages/cli/src/utils/attentionNotification.ts +++ b/packages/cli/src/utils/attentionNotification.ts @@ -5,6 +5,7 @@ */ import process from 'node:process'; +import { createDebugLogger } from '@qwen-code/qwen-code-core'; export enum AttentionNotificationReason { ToolApproval = 'tool_approval', @@ -17,6 +18,7 @@ export interface TerminalNotificationOptions { } const TERMINAL_BELL = '\u0007'; +const debugLogger = createDebugLogger('ATTENTION_NOTIFICATION'); /** * Grabs the user's attention by emitting the terminal bell character. @@ -43,7 +45,7 @@ export function notifyTerminalAttention( stream.write(TERMINAL_BELL); return true; } catch (error) { - console.warn('Failed to send terminal bell:', error); + debugLogger.warn('Failed to send terminal bell:', error); return false; } } diff --git a/packages/cli/src/utils/commentJson.ts b/packages/cli/src/utils/commentJson.ts index bf325d9af..58b83e948 100644 --- a/packages/cli/src/utils/commentJson.ts +++ b/packages/cli/src/utils/commentJson.ts @@ -6,6 +6,7 @@ import * as fs from 'node:fs'; import { parse, stringify } from 'comment-json'; +import { writeStderrLine } from './stdioHelpers.js'; /** * Updates a JSON file while preserving comments and formatting. @@ -25,8 +26,9 @@ export function updateSettingsFilePreservingFormat( try { parsed = parse(originalContent) as Record; } catch (error) { - console.error('Error parsing settings file:', error); - console.error( + writeStderrLine('Error parsing settings file.'); + writeStderrLine(error instanceof Error ? error.message : String(error)); + writeStderrLine( 'Settings file may be corrupted. Please check the JSON syntax.', ); return; diff --git a/packages/cli/src/utils/errors.ts b/packages/cli/src/utils/errors.ts index 68459c670..598f535b7 100644 --- a/packages/cli/src/utils/errors.ts +++ b/packages/cli/src/utils/errors.ts @@ -12,7 +12,11 @@ import { FatalTurnLimitedError, FatalCancellationError, ToolErrorType, + createDebugLogger, } from '@qwen-code/qwen-code-core'; +import { writeStderrLine } from './stdioHelpers.js'; + +const debugLogger = createDebugLogger('CLI_ERRORS'); export function getErrorMessage(error: unknown): string { if (error instanceof Error) { @@ -101,10 +105,10 @@ export function handleError( errorCode, ); - console.error(formattedError); + writeStderrLine(formattedError); process.exit(getNumericExitCode(errorCode)); } else { - console.error(errorMessage); + writeStderrLine(errorMessage); throw error; } } @@ -143,12 +147,9 @@ export function handleToolError( process.stderr.write(warningMessage); } - // Always log detailed error in debug mode - if (config.getDebugMode()) { - console.error( - `Error executing tool ${toolName}: ${resultDisplay || toolError.message}`, - ); - } + debugLogger.error( + `Error executing tool ${toolName}: ${resultDisplay || toolError.message}`, + ); } /** @@ -164,10 +165,10 @@ export function handleCancellationError(config: Config): never { cancellationError.exitCode, ); - console.error(formattedError); + writeStderrLine(formattedError); process.exit(cancellationError.exitCode); } else { - console.error(cancellationError.message); + writeStderrLine(cancellationError.message); process.exit(cancellationError.exitCode); } } @@ -187,10 +188,10 @@ export function handleMaxTurnsExceededError(config: Config): never { maxTurnsError.exitCode, ); - console.error(formattedError); + writeStderrLine(formattedError); process.exit(maxTurnsError.exitCode); } else { - console.error(maxTurnsError.message); + writeStderrLine(maxTurnsError.message); process.exit(maxTurnsError.exitCode); } } diff --git a/packages/cli/src/utils/installationInfo.ts b/packages/cli/src/utils/installationInfo.ts index 0805ad218..691408570 100644 --- a/packages/cli/src/utils/installationInfo.ts +++ b/packages/cli/src/utils/installationInfo.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { isGitRepository } from '@qwen-code/qwen-code-core'; +import { createDebugLogger, isGitRepository } from '@qwen-code/qwen-code-core'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as childProcess from 'node:child_process'; @@ -21,6 +21,8 @@ export enum PackageManager { UNKNOWN = 'unknown', } +const debugLogger = createDebugLogger('INSTALLATION_INFO'); + export interface InstallationInfo { packageManager: PackageManager; isGlobal: boolean; @@ -170,7 +172,7 @@ export function getInstallationInfo( : 'Installed with npm. Attempting to automatically update now...', }; } catch (error) { - console.log(error); + debugLogger.error('Failed to detect installation info:', error); return { packageManager: PackageManager.UNKNOWN, isGlobal: false }; } } diff --git a/packages/cli/src/utils/modelConfigUtils.ts b/packages/cli/src/utils/modelConfigUtils.ts index e9cd050c3..948e6b253 100644 --- a/packages/cli/src/utils/modelConfigUtils.ts +++ b/packages/cli/src/utils/modelConfigUtils.ts @@ -13,6 +13,7 @@ import { type ProviderModelConfig, } from '@qwen-code/qwen-code-core'; import type { Settings } from '../config/settings.js'; +import { writeStderrLine } from './stdioHelpers.js'; export interface CliGenerationConfigInputs { argv: { @@ -131,7 +132,7 @@ export function resolveCliGenerationConfig( // Log warnings if any for (const warning of resolved.warnings) { - console.warn(warning); + writeStderrLine(warning); } // Resolve OpenAI logging config (CLI-specific, not part of core resolver) diff --git a/packages/cli/src/utils/nonInteractiveHelpers.ts b/packages/cli/src/utils/nonInteractiveHelpers.ts index fbffd5dee..ad52aeec3 100644 --- a/packages/cli/src/utils/nonInteractiveHelpers.ts +++ b/packages/cli/src/utils/nonInteractiveHelpers.ts @@ -211,12 +211,10 @@ async function loadSlashCommandNames( // Extract command names and sort return commands.map((cmd) => cmd.name).sort(); } catch (error) { - if (config.getDebugMode()) { - console.error( - '[buildSystemMessage] Failed to load slash commands:', - error, - ); - } + debugLogger.error( + '[buildSystemMessage] Failed to load slash commands:', + error, + ); return []; } finally { controller.abort(); @@ -272,9 +270,7 @@ export async function buildSystemMessage( const subagents = await subagentManager.listSubagents(); agentNames = subagents.map((subagent) => subagent.name); } catch (error) { - if (config.getDebugMode()) { - console.error('[buildSystemMessage] Failed to load subagents:', error); - } + debugLogger.error('[buildSystemMessage] Failed to load subagents:', error); } const systemMessage: CLISystemMessage = { diff --git a/packages/cli/src/utils/readStdin.ts b/packages/cli/src/utils/readStdin.ts index 3ccdaee75..b95dddc74 100644 --- a/packages/cli/src/utils/readStdin.ts +++ b/packages/cli/src/utils/readStdin.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { writeStderrLine } from './stdioHelpers.js'; + export async function readStdin(): Promise { const MAX_STDIN_SIZE = 8 * 1024 * 1024; // 8MB return new Promise((resolve, reject) => { @@ -30,7 +32,7 @@ export async function readStdin(): Promise { if (totalSize + chunk.length > MAX_STDIN_SIZE) { const remainingSize = MAX_STDIN_SIZE - totalSize; data += chunk.slice(0, remainingSize); - console.warn( + writeStderrLine( `Warning: stdin input truncated to ${MAX_STDIN_SIZE} bytes.`, ); process.stdin.destroy(); // Stop reading further diff --git a/packages/cli/src/utils/relaunch.ts b/packages/cli/src/utils/relaunch.ts index 80d243c70..f80a6a2c3 100644 --- a/packages/cli/src/utils/relaunch.ts +++ b/packages/cli/src/utils/relaunch.ts @@ -6,6 +6,7 @@ import { spawn } from 'node:child_process'; import { RELAUNCH_EXIT_CODE } from './processUtils.js'; +import { writeStderrLine } from './stdioHelpers.js'; export async function relaunchOnExitCode(runner: () => Promise) { while (true) { @@ -17,7 +18,8 @@ export async function relaunchOnExitCode(runner: () => Promise) { } } catch (error) { process.stdin.resume(); - console.error('Fatal error: Failed to relaunch the CLI process.', error); + writeStderrLine('Fatal error: Failed to relaunch the CLI process.'); + writeStderrLine(error instanceof Error ? error.message : String(error)); process.exit(1); } } diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index 71f5c47d8..ae5cc82af 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -19,6 +19,7 @@ import type { Config, SandboxConfig } from '@qwen-code/qwen-code-core'; import { FatalSandboxError } from '@qwen-code/qwen-code-core'; import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js'; import { randomBytes } from 'node:crypto'; +import { writeStdoutLine, writeStderrLine } from './stdioHelpers.js'; const execAsync = promisify(exec); @@ -81,7 +82,7 @@ async function shouldUseCurrentUserInSandbox(): Promise { ); if (debugEnv) { // Use stderr so it doesn't clutter normal STDOUT output (e.g. in `--prompt` runs). - console.error( + writeStderrLine( 'INFO: Using current user UID/GID in Linux sandbox. Set SANDBOX_SET_UID_GID=false to disable.', ); } @@ -210,7 +211,7 @@ export async function start_sandbox( ); } // Log on STDERR so it doesn't clutter the output on STDOUT - console.error(`using macos seatbelt (profile: ${profile}) ...`); + writeStderrLine(`using macos seatbelt (profile: ${profile}) ...`); // if DEBUG is set, convert to --inspect-brk in NODE_OPTIONS const nodeOptions = [ ...(process.env['DEBUG'] ? ['--inspect-brk'] : []), @@ -299,7 +300,7 @@ export async function start_sandbox( }); // install handlers to stop proxy on exit/signal const stopProxy = () => { - console.log('stopping proxy ...'); + writeStdoutLine('stopping proxy ...'); if (proxyProcess?.pid) { process.kill(-proxyProcess.pid, 'SIGTERM'); } @@ -313,7 +314,7 @@ export async function start_sandbox( // console.info(data.toString()); // }); proxyProcess.stderr?.on('data', (data) => { - console.error(data.toString()); + writeStderrLine(data.toString()); }); proxyProcess.on('close', (code, signal) => { if (sandboxProcess?.pid) { @@ -323,7 +324,7 @@ export async function start_sandbox( `Proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`, ); }); - console.log('waiting for proxy to start ...'); + writeStdoutLine('waiting for proxy to start ...'); await execAsync( `until timeout 0.25 curl -s http://localhost:8877; do sleep 0.25; done`, ); @@ -342,7 +343,7 @@ export async function start_sandbox( }); } - console.error(`hopping into sandbox (command: ${config.command}) ...`); + writeStderrLine(`hopping into sandbox (command: ${config.command}) ...`); // determine full path for gemini-cli to distinguish linked vs installed setting const gcPath = fs.realpathSync(process.argv[1]); @@ -367,7 +368,7 @@ export async function start_sandbox( 'run `npm link ./packages/cli` under QwenCode-cli repo to switch to linked binary.', ); } else { - console.error('building sandbox ...'); + writeStderrLine('building sandbox ...'); const gcRoot = gcPath.split('/packages/')[0]; // if project folder has sandbox.Dockerfile under project settings folder, use that let buildArgs = ''; @@ -376,7 +377,7 @@ export async function start_sandbox( 'sandbox.Dockerfile', ); if (isCustomProjectSandbox) { - console.error(`using ${projectSandboxDockerfile} for sandbox`); + writeStderrLine(`using ${projectSandboxDockerfile} for sandbox`); buildArgs += `-f ${path.resolve(projectSandboxDockerfile)} -i ${image}`; } execSync( @@ -491,7 +492,7 @@ export async function start_sandbox( `Missing mount path '${from}' listed in SANDBOX_MOUNTS`, ); } - console.error(`SANDBOX_MOUNTS: ${from} -> ${to} (${opts})`); + writeStderrLine(`SANDBOX_MOUNTS: ${from} -> ${to} (${opts})`); args.push('--volume', mount); } } @@ -557,7 +558,7 @@ export async function start_sandbox( containerName = `gemini-cli-integration-test-${randomBytes(4).toString( 'hex', )}`; - console.log(`ContainerName: ${containerName}`); + writeStdoutLine(`ContainerName: ${containerName}`); } else { let index = 0; const containerNameCheck = execSync( @@ -569,7 +570,7 @@ export async function start_sandbox( index++; } containerName = `${imageName}-${index}`; - console.log(`ContainerName (regular): ${containerName}`); + writeStdoutLine(`ContainerName (regular): ${containerName}`); } args.push('--name', containerName, '--hostname', containerName); @@ -691,7 +692,7 @@ export async function start_sandbox( for (let env of process.env['SANDBOX_ENV'].split(',')) { if ((env = env.trim())) { if (env.includes('=')) { - console.error(`SANDBOX_ENV: ${env}`); + writeStderrLine(`SANDBOX_ENV: ${env}`); args.push('--env', env); } else { throw new FatalSandboxError( @@ -795,7 +796,7 @@ export async function start_sandbox( }); // install handlers to stop proxy on exit/signal const stopProxy = () => { - console.log('stopping proxy container ...'); + writeStdoutLine('stopping proxy container ...'); execSync(`${config.command} rm -f ${SANDBOX_PROXY_NAME}`); }; process.on('exit', stopProxy); @@ -807,7 +808,7 @@ export async function start_sandbox( // console.info(data.toString()); // }); proxyProcess.stderr?.on('data', (data) => { - console.error(data.toString().trim()); + writeStderrLine(data.toString().trim()); }); proxyProcess.on('close', (code, signal) => { if (sandboxProcess?.pid) { @@ -817,7 +818,7 @@ export async function start_sandbox( `Proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`, ); }); - console.log('waiting for proxy to start ...'); + writeStdoutLine('waiting for proxy to start ...'); await execAsync( `until timeout 0.25 curl -s http://localhost:8877; do sleep 0.25; done`, ); @@ -836,14 +837,14 @@ export async function start_sandbox( return new Promise((resolve, reject) => { sandboxProcess.on('error', (err) => { - console.error('Sandbox process error:', err); + writeStderrLine(`Sandbox process error: ${err}`); reject(err); }); sandboxProcess?.on('close', (code, signal) => { process.stdin.resume(); if (code !== 0 && code !== null) { - console.error( + writeStderrLine( `Sandbox process exited with code: ${code}, signal: ${signal}`, ); } @@ -869,7 +870,7 @@ async function imageExists(sandbox: string, image: string): Promise { } checkProcess.on('error', (err) => { - console.warn( + writeStderrLine( `Failed to start '${sandbox}' command for image check: ${err.message}`, ); resolve(false); @@ -887,7 +888,7 @@ async function imageExists(sandbox: string, image: string): Promise { } async function pullImage(sandbox: string, image: string): Promise { - console.info(`Attempting to pull image ${image} using ${sandbox}...`); + writeStdoutLine(`Attempting to pull image ${image} using ${sandbox}...`); return new Promise((resolve) => { const args = ['pull', image]; const pullProcess = spawn(sandbox, args, { stdio: 'pipe' }); @@ -895,16 +896,16 @@ async function pullImage(sandbox: string, image: string): Promise { let stderrData = ''; const onStdoutData = (data: Buffer) => { - console.info(data.toString().trim()); // Show pull progress + writeStdoutLine(data.toString().trim()); // Show pull progress }; const onStderrData = (data: Buffer) => { stderrData += data.toString(); - console.error(data.toString().trim()); // Show pull errors/info from the command itself + writeStderrLine(data.toString().trim()); // Show pull errors/info from the command itself }; const onError = (err: Error) => { - console.warn( + writeStderrLine( `Failed to start '${sandbox} pull ${image}' command: ${err.message}`, ); cleanup(); @@ -913,11 +914,11 @@ async function pullImage(sandbox: string, image: string): Promise { const onClose = (code: number | null) => { if (code === 0) { - console.info(`Successfully pulled image ${image}.`); + writeStdoutLine(`Successfully pulled image ${image}.`); cleanup(); resolve(true); } else { - console.warn( + writeStderrLine( `Failed to pull image ${image}. '${sandbox} pull ${image}' exited with code ${code}.`, ); if (stderrData.trim()) { @@ -957,13 +958,13 @@ async function ensureSandboxImageIsPresent( sandbox: string, image: string, ): Promise { - console.info(`Checking for sandbox image: ${image}`); + writeStdoutLine(`Checking for sandbox image: ${image}`); if (await imageExists(sandbox, image)) { - console.info(`Sandbox image ${image} found locally.`); + writeStdoutLine(`Sandbox image ${image} found locally.`); return true; } - console.info(`Sandbox image ${image} not found locally.`); + writeStdoutLine(`Sandbox image ${image} not found locally.`); if (image === LOCAL_DEV_SANDBOX_IMAGE_NAME) { // user needs to build the image themselves return false; @@ -972,17 +973,17 @@ async function ensureSandboxImageIsPresent( if (await pullImage(sandbox, image)) { // After attempting to pull, check again to be certain if (await imageExists(sandbox, image)) { - console.info(`Sandbox image ${image} is now available after pulling.`); + writeStdoutLine(`Sandbox image ${image} is now available after pulling.`); return true; } else { - console.warn( + writeStderrLine( `Sandbox image ${image} still not found after a pull attempt. This might indicate an issue with the image name or registry, or the pull command reported success but failed to make the image available.`, ); return false; } } - console.error( + writeStderrLine( `Failed to obtain sandbox image ${image} after check and pull attempt.`, ); return false; // Pull command failed or image still not present diff --git a/packages/cli/src/utils/stdioHelpers.ts b/packages/cli/src/utils/stdioHelpers.ts new file mode 100644 index 000000000..ca5e30f9e --- /dev/null +++ b/packages/cli/src/utils/stdioHelpers.ts @@ -0,0 +1,41 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Utility functions for writing to stdout/stderr in CLI commands. + * + * These helpers are used instead of console.log/console.error in standalone + * CLI commands (like `qwen extensions list`) where the output IS the user-facing + * result, not debug logging. + * + * For debug/diagnostic logging, use `createDebugLogger()` from @qwen-code/qwen-code-core. + */ + +/** + * Writes a message to stdout with a trailing newline. + * Use for normal command output that the user expects to see. + * Avoids double newlines if the message already ends with one. + */ +export const writeStdoutLine = (message: string): void => { + process.stdout.write(message.endsWith('\n') ? message : `${message}\n`); +}; + +/** + * Writes a message to stderr with a trailing newline. + * Use for error messages in CLI commands. + * Avoids double newlines if the message already ends with one. + */ +export const writeStderrLine = (message: string): void => { + process.stderr.write(message.endsWith('\n') ? message : `${message}\n`); +}; + +/** + * Clears the terminal screen. + * Use instead of console.clear() to satisfy no-console lint rules. + */ +export const clearScreen = (): void => { + console.clear(); +}; diff --git a/packages/cli/src/validateNonInterActiveAuth.ts b/packages/cli/src/validateNonInterActiveAuth.ts index f5d71b08d..b1353bd09 100644 --- a/packages/cli/src/validateNonInterActiveAuth.ts +++ b/packages/cli/src/validateNonInterActiveAuth.ts @@ -11,6 +11,7 @@ import { type LoadedSettings } from './config/settings.js'; import { JsonOutputAdapter } from './nonInteractive/io/JsonOutputAdapter.js'; import { StreamJsonOutputAdapter } from './nonInteractive/io/StreamJsonOutputAdapter.js'; import { runExitCleanup } from './utils/cleanup.js'; +import { writeStderrLine } from './utils/stdioHelpers.js'; export async function validateNonInteractiveAuth( useExternalAuth: boolean | undefined, @@ -74,7 +75,7 @@ export async function validateNonInteractiveAuth( } // For other modes (text), use existing error handling - console.error(error instanceof Error ? error.message : String(error)); + writeStderrLine(error instanceof Error ? error.message : String(error)); process.exit(1); } }