Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 24 additions & 27 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export default tseslint.config(
},
],
'no-unsafe-finally': 'error',
'no-console': 'error',
'no-unused-expressions': 'off', // Disable base rule
'@typescript-eslint/no-unused-expressions': [
// Enable TS version
Expand All @@ -169,6 +170,7 @@ export default tseslint.config(
...vitest.configs.recommended.rules,
'vitest/expect-expect': 'off',
'vitest/no-commented-out-tests': 'off',
'no-console': 'off', // Allow console in tests
'@typescript-eslint/no-unused-vars': [
'error',
{
Expand All @@ -190,6 +192,7 @@ export default tseslint.config(
},
},
rules: {
'no-console': 'off', // Allow console in scripts
'@typescript-eslint/no-unused-vars': [
'error',
{
Expand All @@ -214,38 +217,30 @@ export default tseslint.config(
'no-undef': 'off',
},
},
// ==================== no-console allowlist ====================
// The following files/packages are allowed to use console.*

// VS Code IDE companion - out of scope for no-console rule
{
files: ['packages/vscode-ide-companion/esbuild.js'],
languageOptions: {
globals: {
...globals.node,
process: 'readonly',
console: 'readonly',
},
},
rules: {
'no-restricted-syntax': 'off',
'@typescript-eslint/no-require-imports': 'off',
},
files: ['packages/vscode-ide-companion/**/*.ts', 'packages/vscode-ide-companion/**/*.tsx', 'packages/vscode-ide-companion/**/*.js'],
rules: { 'no-console': 'off' },
},
// extra settings for scripts that we run directly with node
// WebUI package - UI component library with Storybook
{
files: ['packages/vscode-ide-companion/scripts/**/*.js'],
languageOptions: {
globals: {
...globals.node,
process: 'readonly',
console: 'readonly',
},
},
rules: {
'no-restricted-syntax': 'off',
'@typescript-eslint/no-require-imports': 'off',
},
files: ['packages/webui/**/*.ts', 'packages/webui/**/*.tsx', 'packages/webui/**/*.js'],
rules: { 'no-console': 'off' },
},
// extra settings for core package scripts
// Specific CLI files that intentionally wrap console usage
{
files: ['packages/core/scripts/**/*.js'],
files: [
'packages/cli/src/acp-integration/acpAgent.ts', // console infrastructure for ACP mode
'packages/cli/src/utils/stdioHelpers.ts', // wraps console.clear()
],
rules: { 'no-console': 'off' },
},
// Specific esbuild configs not covered by scripts pattern
{
files: ['packages/vscode-ide-companion/esbuild.js'],
languageOptions: {
globals: {
...globals.node,
Expand All @@ -256,6 +251,7 @@ export default tseslint.config(
rules: {
'no-restricted-syntax': 'off',
'@typescript-eslint/no-require-imports': 'off',
'no-console': 'off',
},
},
// Settings for export-html assets
Expand Down Expand Up @@ -290,6 +286,7 @@ export default tseslint.config(
},
},
rules: {
'no-console': 'off', // Allow console in integration tests
'@typescript-eslint/no-unused-vars': [
'error',
{
Expand Down
46 changes: 39 additions & 7 deletions packages/cli/src/acp-integration/acp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +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';
import { pickAuthMethodsForDetails } from './authMethods.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;

Expand Down Expand Up @@ -222,8 +224,16 @@ class Connection {
const trimmedLine = line.trim();

if (trimmedLine) {
const message = JSON.parse(trimmedLine);
this.#processMessage(message);
try {
const message = JSON.parse(trimmedLine);
this.#processMessage(message);
} catch (error) {
debugLogger.error('ACP parse error for inbound message.', {
code: ACP_ERROR_CODES.PARSE_ERROR,
line: trimmedLine,
error,
});
}
}
}
}
Expand Down Expand Up @@ -260,13 +270,23 @@ class Connection {
return { result: result ?? null };
} catch (error: unknown) {
if (error instanceof RequestError) {
debugLogger.debug('ACP handler returned request error.', {
method,
code: error.code,
message: error.message,
details: error.data?.details,
});
return error.toResult();
}

if (error instanceof z.ZodError) {
return RequestError.invalidParams(
JSON.stringify(error.format(), undefined, 2),
).toResult();
const formattedDetails = JSON.stringify(error.format(), undefined, 2);
debugLogger.debug('ACP handler validation error.', {
method,
code: ACP_ERROR_CODES.INVALID_PARAMS,
details: formattedDetails,
});
return RequestError.invalidParams(formattedDetails).toResult();
}

let errorName;
Expand All @@ -291,6 +311,11 @@ class Connection {
).toResult();
}

debugLogger.error(
'ACP handler failed with internal error.',
{ method, errorName, details },
error,
);
return RequestError.internalError(details).toResult();
}
}
Expand All @@ -301,7 +326,14 @@ class Connection {
if ('result' in response) {
pendingResponse.resolve(response.result);
} else if ('error' in response) {
pendingResponse.reject(response.error);
const { error } = response;
debugLogger.warn('ACP response error received.', {
id: response.id,
code: error.code,
message: error.message,
data: error.data,
});
pendingResponse.reject(error);
}
this.#pendingResponses.delete(response.id);
}
Expand Down Expand Up @@ -333,7 +365,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;
}
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/acp-integration/acpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
APPROVAL_MODES,
AuthType,
clearCachedCredentialFile,
createDebugLogger,
QwenOAuth2Event,
qwenOAuth2Events,
MCPServerConfig,
Expand All @@ -35,6 +36,8 @@ import { loadCliConfig } from '../config/config.js';
import { Session } from './session/Session.js';
import { formatAcpModelId } from '../utils/acpModelUtils.js';

const debugLogger = createDebugLogger('ACP_AGENT');

export async function runAcpAgent(
config: Config,
settings: LoadedSettings,
Expand Down Expand Up @@ -291,7 +294,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,
this.pickAuthMethodsForAuthRequired(selectedType, e),
Expand Down
5 changes: 0 additions & 5 deletions packages/cli/src/acp-integration/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,6 @@ describe('Session', () => {
});

it('swallows errors and does not throw', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined);
getAvailableCommandsSpy.mockRejectedValueOnce(
new Error('Command discovery failed'),
);
Expand All @@ -198,8 +195,6 @@ describe('Session', () => {
session.sendAvailableCommandsUpdate(),
).resolves.toBeUndefined();
expect(mockClient.sessionUpdate).not.toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
});

Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/acp-integration/session/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
AuthType,
ApprovalMode,
convertToFunctionResponse,
createDebugLogger,
DiscoveredMCPTool,
StreamEventType,
ToolConfirmationOutcome,
Expand Down Expand Up @@ -68,6 +69,8 @@ import { PlanEmitter } from './emitters/PlanEmitter.js';
import { MessageEmitter } from './emitters/MessageEmitter.js';
import { SubAgentTracker } from './SubAgentTracker.js';

const debugLogger = createDebugLogger('SESSION');

/**
* Session represents an active conversation session with the AI model.
* It uses modular components for consistent event emission:
Expand Down Expand Up @@ -319,7 +322,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);
}
}

Expand Down Expand Up @@ -927,7 +930,7 @@ export class Session implements SessionContext {

debug(msg: string): void {
if (this.config.getDebugMode()) {
console.warn(msg);
debugLogger.warn(msg);
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/acp-integration/session/SubAgentTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ import type {
import {
SubAgentEventType,
ToolConfirmationOutcome,
createDebugLogger,
} from '@qwen-code/qwen-code-core';
import { z } from 'zod';
import type { SessionContext } from './types.js';
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.
*/
Expand Down Expand Up @@ -151,7 +154,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);
}
}

Expand Down Expand Up @@ -268,7 +271,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,
);
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/extensions/consent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ConfirmationRequest } from '../../ui/types.js';
import chalk from 'chalk';
import prompts from 'prompts';
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
Expand All @@ -22,7 +23,7 @@ import { t } from '../../i18n/index.js';
export async function requestConsentNonInteractive(
consentDescription: string,
): Promise<boolean> {
console.info(consentDescription);
writeStdoutLine(consentDescription);
const result = await promptForConsentNonInteractive(
t('Do you want to continue? [Y/n]: '),
);
Expand Down
Loading