Compare commits

..

4 Commits

Author SHA1 Message Date
pomelo-nwu
11b77aa2b4 fix: handle null command in verifyVSCode function 2025-11-06 15:12:08 +08:00
pomelo-nwu
9a9331790a fix: prevent long user queries from appearing in footer debug message 2025-11-06 14:56:16 +08:00
pomelo
decb04efc4 Merge pull request #974 from QwenLM/chore/v0.1.5
chore: pump version to 0.1.5
2025-11-05 21:40:15 +08:00
pomelo-nwu
3bd0cb36c4 chore: pump version to 0.1.5 2025-11-05 19:35:17 +08:00
20 changed files with 427 additions and 1007 deletions

55
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.1.4",
"version": "0.1.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.1.4",
"version": "0.1.5",
"workspaces": [
"packages/*"
],
@@ -555,6 +555,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -578,6 +579,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -2118,6 +2120,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -3279,6 +3282,7 @@
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -3717,6 +3721,7 @@
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -3727,6 +3732,7 @@
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -3932,6 +3938,7 @@
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
@@ -4700,6 +4707,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5054,8 +5062,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/array-includes": {
"version": "3.1.9",
@@ -6220,7 +6227,6 @@
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "5.2.1"
},
@@ -7254,6 +7260,7 @@
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7723,7 +7730,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -7785,7 +7791,6 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -7795,7 +7800,6 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"peer": true,
"dependencies": {
"ms": "2.0.0"
}
@@ -7805,7 +7809,6 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8"
}
@@ -7972,7 +7975,6 @@
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
@@ -7991,7 +7993,6 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"peer": true,
"dependencies": {
"ms": "2.0.0"
}
@@ -8000,15 +8001,13 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/finalhandler/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8"
}
@@ -9047,6 +9046,7 @@
"resolved": "https://registry.npmjs.org/ink/-/ink-6.2.3.tgz",
"integrity": "sha512-fQkfEJjKbLXIcVWEE3MvpYSnwtbbmRsmeNDNz1pIuOFlwE+UF2gsy228J36OXKZGWJWZJKUigphBSqCNMcARtg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.0",
"ansi-escapes": "^7.0.0",
@@ -10950,7 +10950,6 @@
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -12158,8 +12157,7 @@
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/path-type": {
"version": "3.0.0",
@@ -12663,6 +12661,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12673,6 +12672,7 @@
"integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"shell-quote": "^1.6.1",
"ws": "^7"
@@ -12706,6 +12706,7 @@
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -14515,6 +14516,7 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14688,7 +14690,8 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
"license": "0BSD",
"peer": true
},
"node_modules/tsx": {
"version": "4.20.3",
@@ -14696,6 +14699,7 @@
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
@@ -14880,6 +14884,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -15149,7 +15154,6 @@
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.4.0"
}
@@ -15205,6 +15209,7 @@
"integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.6",
@@ -15318,6 +15323,7 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15331,6 +15337,7 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -16009,6 +16016,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -16024,7 +16032,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.1.4",
"version": "0.1.5",
"dependencies": {
"@google/genai": "1.16.0",
"@iarna/toml": "^2.2.5",
@@ -16139,7 +16147,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.1.4",
"version": "0.1.5",
"hasInstallScript": true,
"dependencies": {
"@google/genai": "1.16.0",
@@ -16269,6 +16277,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -16278,7 +16287,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.1.4",
"version": "0.1.5",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -16290,7 +16299,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.1.4",
"version": "0.1.5",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.1.4",
"version": "0.1.5",
"engines": {
"node": ">=20.0.0"
},
@@ -13,7 +13,7 @@
"url": "git+https://github.com/QwenLM/qwen-code.git"
},
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.4"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.5"
},
"scripts": {
"start": "cross-env node scripts/start.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.1.4",
"version": "0.1.5",
"description": "Qwen Code",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.4"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.5"
},
"dependencies": {
"@google/genai": "1.16.0",

View File

@@ -551,6 +551,11 @@ export const AppContainer = (props: AppContainerProps) => {
[visionSwitchResolver],
);
// onDebugMessage should log to console, not update footer debugMessage
const onDebugMessage = useCallback((message: string) => {
console.debug(message);
}, []);
const performMemoryRefresh = useCallback(async () => {
historyManager.addItem(
{
@@ -628,7 +633,7 @@ export const AppContainer = (props: AppContainerProps) => {
historyManager.addItem,
config,
settings,
setDebugMessage,
onDebugMessage,
handleSlashCommand,
shellModeActive,
() => settings.merged.general?.preferredEditor as EditorType,

View File

@@ -8,22 +8,38 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { aboutCommand } from './aboutCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import * as versionUtils from '../../utils/version.js';
import { MessageType } from '../types.js';
import * as systemInfoUtils from '../../utils/systemInfo.js';
import { IdeClient } from '@qwen-code/qwen-code-core';
vi.mock('../../utils/systemInfo.js');
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
return {
...actual,
IdeClient: {
getInstance: vi.fn().mockResolvedValue({
getDetectedIdeDisplayName: vi.fn().mockReturnValue('test-ide'),
}),
},
};
});
vi.mock('../../utils/version.js', () => ({
getCliVersion: vi.fn(),
}));
describe('aboutCommand', () => {
let mockContext: CommandContext;
const originalPlatform = process.platform;
const originalEnv = { ...process.env };
beforeEach(() => {
mockContext = createMockCommandContext({
services: {
config: {
getModel: vi.fn().mockReturnValue('test-model'),
getModel: vi.fn(),
getIdeMode: vi.fn().mockReturnValue(true),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
},
settings: {
merged: {
@@ -40,25 +56,21 @@ describe('aboutCommand', () => {
},
} as unknown as CommandContext);
vi.mocked(systemInfoUtils.getExtendedSystemInfo).mockResolvedValue({
cliVersion: 'test-version',
osPlatform: 'test-os',
osArch: 'x64',
osRelease: '22.0.0',
nodeVersion: 'v20.0.0',
npmVersion: '10.0.0',
sandboxEnv: 'no sandbox',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
ideClient: 'test-ide',
sessionId: 'test-session-id',
memoryUsage: '100 MB',
baseUrl: undefined,
vi.mocked(versionUtils.getCliVersion).mockResolvedValue('test-version');
vi.spyOn(mockContext.services.config!, 'getModel').mockReturnValue(
'test-model',
);
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-gcp-project';
Object.defineProperty(process, 'platform', {
value: 'test-os',
});
});
afterEach(() => {
vi.unstubAllEnvs();
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
process.env = originalEnv;
vi.clearAllMocks();
});
@@ -69,55 +81,30 @@ describe('aboutCommand', () => {
});
it('should call addItem with all version info', async () => {
process.env['SANDBOX'] = '';
if (!aboutCommand.action) {
throw new Error('The about command must have an action.');
}
await aboutCommand.action(mockContext, '');
expect(systemInfoUtils.getExtendedSystemInfo).toHaveBeenCalledWith(
mockContext,
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
{
type: MessageType.ABOUT,
systemInfo: expect.objectContaining({
cliVersion: 'test-version',
osPlatform: 'test-os',
osArch: 'x64',
osRelease: '22.0.0',
nodeVersion: 'v20.0.0',
npmVersion: '10.0.0',
sandboxEnv: 'no sandbox',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
ideClient: 'test-ide',
sessionId: 'test-session-id',
memoryUsage: '100 MB',
baseUrl: undefined,
}),
}),
cliVersion: 'test-version',
osVersion: 'test-os',
sandboxEnv: 'no sandbox',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
gcpProject: 'test-gcp-project',
ideClient: 'test-ide',
},
expect.any(Number),
);
});
it('should show the correct sandbox environment variable', async () => {
vi.mocked(systemInfoUtils.getExtendedSystemInfo).mockResolvedValue({
cliVersion: 'test-version',
osPlatform: 'test-os',
osArch: 'x64',
osRelease: '22.0.0',
nodeVersion: 'v20.0.0',
npmVersion: '10.0.0',
sandboxEnv: 'gemini-sandbox',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
ideClient: 'test-ide',
sessionId: 'test-session-id',
memoryUsage: '100 MB',
baseUrl: undefined,
});
process.env['SANDBOX'] = 'gemini-sandbox';
if (!aboutCommand.action) {
throw new Error('The about command must have an action.');
}
@@ -126,32 +113,15 @@ describe('aboutCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ABOUT,
systemInfo: expect.objectContaining({
sandboxEnv: 'gemini-sandbox',
}),
sandboxEnv: 'gemini-sandbox',
}),
expect.any(Number),
);
});
it('should show sandbox-exec profile when applicable', async () => {
vi.mocked(systemInfoUtils.getExtendedSystemInfo).mockResolvedValue({
cliVersion: 'test-version',
osPlatform: 'test-os',
osArch: 'x64',
osRelease: '22.0.0',
nodeVersion: 'v20.0.0',
npmVersion: '10.0.0',
sandboxEnv: 'sandbox-exec (test-profile)',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
ideClient: 'test-ide',
sessionId: 'test-session-id',
memoryUsage: '100 MB',
baseUrl: undefined,
});
process.env['SANDBOX'] = 'sandbox-exec';
process.env['SEATBELT_PROFILE'] = 'test-profile';
if (!aboutCommand.action) {
throw new Error('The about command must have an action.');
}
@@ -160,31 +130,18 @@ describe('aboutCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
systemInfo: expect.objectContaining({
sandboxEnv: 'sandbox-exec (test-profile)',
}),
sandboxEnv: 'sandbox-exec (test-profile)',
}),
expect.any(Number),
);
});
it('should not show ide client when it is not detected', async () => {
vi.mocked(systemInfoUtils.getExtendedSystemInfo).mockResolvedValue({
cliVersion: 'test-version',
osPlatform: 'test-os',
osArch: 'x64',
osRelease: '22.0.0',
nodeVersion: 'v20.0.0',
npmVersion: '10.0.0',
sandboxEnv: 'no sandbox',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
ideClient: '',
sessionId: 'test-session-id',
memoryUsage: '100 MB',
baseUrl: undefined,
});
vi.mocked(IdeClient.getInstance).mockResolvedValue({
getDetectedIdeDisplayName: vi.fn().mockReturnValue(undefined),
} as unknown as IdeClient);
process.env['SANDBOX'] = '';
if (!aboutCommand.action) {
throw new Error('The about command must have an action.');
}
@@ -194,87 +151,13 @@ describe('aboutCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ABOUT,
systemInfo: expect.objectContaining({
cliVersion: 'test-version',
osPlatform: 'test-os',
osArch: 'x64',
osRelease: '22.0.0',
nodeVersion: 'v20.0.0',
npmVersion: '10.0.0',
sandboxEnv: 'no sandbox',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
ideClient: '',
sessionId: 'test-session-id',
memoryUsage: '100 MB',
baseUrl: undefined,
}),
}),
expect.any(Number),
);
});
it('should show unknown npmVersion when npm command fails', async () => {
vi.mocked(systemInfoUtils.getExtendedSystemInfo).mockResolvedValue({
cliVersion: 'test-version',
osPlatform: 'test-os',
osArch: 'x64',
osRelease: '22.0.0',
nodeVersion: 'v20.0.0',
npmVersion: 'unknown',
sandboxEnv: 'no sandbox',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
ideClient: 'test-ide',
sessionId: 'test-session-id',
memoryUsage: '100 MB',
baseUrl: undefined,
});
if (!aboutCommand.action) {
throw new Error('The about command must have an action.');
}
await aboutCommand.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
systemInfo: expect.objectContaining({
npmVersion: 'unknown',
}),
}),
expect.any(Number),
);
});
it('should show unknown sessionId when config is not available', async () => {
vi.mocked(systemInfoUtils.getExtendedSystemInfo).mockResolvedValue({
cliVersion: 'test-version',
osPlatform: 'test-os',
osArch: 'x64',
osRelease: '22.0.0',
nodeVersion: 'v20.0.0',
npmVersion: '10.0.0',
sandboxEnv: 'no sandbox',
modelVersion: 'Unknown',
selectedAuthType: 'test-auth',
ideClient: '',
sessionId: 'unknown',
memoryUsage: '100 MB',
baseUrl: undefined,
});
if (!aboutCommand.action) {
throw new Error('The about command must have an action.');
}
await aboutCommand.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
systemInfo: expect.objectContaining({
sessionId: 'unknown',
}),
cliVersion: 'test-version',
osVersion: 'test-os',
sandboxEnv: 'no sandbox',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
gcpProject: 'test-gcp-project',
ideClient: '',
}),
expect.any(Number),
);

View File

@@ -4,23 +4,53 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { SlashCommand } from './types.js';
import { getCliVersion } from '../../utils/version.js';
import type { CommandContext, SlashCommand } from './types.js';
import { CommandKind } from './types.js';
import process from 'node:process';
import { MessageType, type HistoryItemAbout } from '../types.js';
import { getExtendedSystemInfo } from '../../utils/systemInfo.js';
import { IdeClient } from '@qwen-code/qwen-code-core';
export const aboutCommand: SlashCommand = {
name: 'about',
description: 'show version info',
kind: CommandKind.BUILT_IN,
action: async (context) => {
const systemInfo = await getExtendedSystemInfo(context);
const osVersion = process.platform;
let sandboxEnv = 'no sandbox';
if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') {
sandboxEnv = process.env['SANDBOX'];
} else if (process.env['SANDBOX'] === 'sandbox-exec') {
sandboxEnv = `sandbox-exec (${
process.env['SEATBELT_PROFILE'] || 'unknown'
})`;
}
const modelVersion = context.services.config?.getModel() || 'Unknown';
const cliVersion = await getCliVersion();
const selectedAuthType =
context.services.settings.merged.security?.auth?.selectedType || '';
const gcpProject = process.env['GOOGLE_CLOUD_PROJECT'] || '';
const ideClient = await getIdeClientName(context);
const aboutItem: Omit<HistoryItemAbout, 'id'> = {
type: MessageType.ABOUT,
systemInfo,
cliVersion,
osVersion,
sandboxEnv,
modelVersion,
selectedAuthType,
gcpProject,
ideClient,
};
context.ui.addItem(aboutItem, Date.now());
},
};
async function getIdeClientName(context: CommandContext) {
if (!context.services.config?.getIdeMode()) {
return '';
}
const ideClient = await IdeClient.getInstance();
return ideClient?.getDetectedIdeDisplayName() ?? '';
}

View File

@@ -8,34 +8,41 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import open from 'open';
import { bugCommand } from './bugCommand.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { getCliVersion } from '../../utils/version.js';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
import { formatMemoryUsage } from '../utils/formatters.js';
import { AuthType } from '@qwen-code/qwen-code-core';
import * as systemInfoUtils from '../../utils/systemInfo.js';
// Mock dependencies
vi.mock('open');
vi.mock('../../utils/systemInfo.js');
vi.mock('../../utils/version.js');
vi.mock('../utils/formatters.js');
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
return {
...actual,
IdeClient: {
getInstance: () => ({
getDetectedIdeDisplayName: vi.fn().mockReturnValue('VSCode'),
}),
},
};
});
vi.mock('node:process', () => ({
default: {
platform: 'test-platform',
version: 'v20.0.0',
// Keep other necessary process properties if needed by other parts of the code
env: process.env,
memoryUsage: () => ({ rss: 0 }),
},
}));
describe('bugCommand', () => {
beforeEach(() => {
vi.mocked(systemInfoUtils.getExtendedSystemInfo).mockResolvedValue({
cliVersion: '0.1.0',
osPlatform: 'test-platform',
osArch: 'x64',
osRelease: '22.0.0',
nodeVersion: 'v20.0.0',
npmVersion: '10.0.0',
sandboxEnv: 'test',
modelVersion: 'qwen3-coder-plus',
selectedAuthType: '',
ideClient: 'VSCode',
sessionId: 'test-session-id',
memoryUsage: '100 MB',
gitCommit:
GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO)
? GIT_COMMIT_INFO
: undefined,
});
vi.mocked(getCliVersion).mockResolvedValue('0.1.0');
vi.mocked(formatMemoryUsage).mockReturnValue('100 MB');
vi.stubEnv('SANDBOX', 'qwen-test');
});
@@ -48,7 +55,19 @@ describe('bugCommand', () => {
const mockContext = createMockCommandContext({
services: {
config: {
getModel: () => 'qwen3-coder-plus',
getBugCommand: () => undefined,
getIdeMode: () => true,
getSessionId: () => 'test-session-id',
},
settings: {
merged: {
security: {
auth: {
selectedType: undefined,
},
},
},
},
},
});
@@ -56,21 +75,14 @@ describe('bugCommand', () => {
if (!bugCommand.action) throw new Error('Action is not defined');
await bugCommand.action(mockContext, 'A test bug');
const gitCommitLine =
GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO)
? `* **Git Commit:** ${GIT_COMMIT_INFO}\n`
: '';
const expectedInfo = `
* **CLI Version:** 0.1.0
${gitCommitLine}* **Model:** qwen3-coder-plus
* **Sandbox:** test
* **OS Platform:** test-platform
* **OS Arch:** x64
* **OS Release:** 22.0.0
* **Node.js Version:** v20.0.0
* **NPM Version:** 10.0.0
* **Git Commit:** ${GIT_COMMIT_INFO}
* **Session ID:** test-session-id
* **Auth Method:**
* **Operating System:** test-platform v20.0.0
* **Sandbox Environment:** test
* **Auth Type:**
* **Model Version:** qwen3-coder-plus
* **Memory Usage:** 100 MB
* **IDE Client:** VSCode
`;
@@ -87,7 +99,19 @@ ${gitCommitLine}* **Model:** qwen3-coder-plus
const mockContext = createMockCommandContext({
services: {
config: {
getModel: () => 'qwen3-coder-plus',
getBugCommand: () => ({ urlTemplate: customTemplate }),
getIdeMode: () => true,
getSessionId: () => 'test-session-id',
},
settings: {
merged: {
security: {
auth: {
selectedType: undefined,
},
},
},
},
},
});
@@ -95,21 +119,14 @@ ${gitCommitLine}* **Model:** qwen3-coder-plus
if (!bugCommand.action) throw new Error('Action is not defined');
await bugCommand.action(mockContext, 'A custom bug');
const gitCommitLine =
GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO)
? `* **Git Commit:** ${GIT_COMMIT_INFO}\n`
: '';
const expectedInfo = `
* **CLI Version:** 0.1.0
${gitCommitLine}* **Model:** qwen3-coder-plus
* **Sandbox:** test
* **OS Platform:** test-platform
* **OS Arch:** x64
* **OS Release:** 22.0.0
* **Node.js Version:** v20.0.0
* **NPM Version:** 10.0.0
* **Git Commit:** ${GIT_COMMIT_INFO}
* **Session ID:** test-session-id
* **Auth Method:**
* **Operating System:** test-platform v20.0.0
* **Sandbox Environment:** test
* **Auth Type:**
* **Model Version:** qwen3-coder-plus
* **Memory Usage:** 100 MB
* **IDE Client:** VSCode
`;
@@ -121,30 +138,25 @@ ${gitCommitLine}* **Model:** qwen3-coder-plus
});
it('should include Base URL when auth type is OpenAI', async () => {
vi.mocked(systemInfoUtils.getExtendedSystemInfo).mockResolvedValue({
cliVersion: '0.1.0',
osPlatform: 'test-platform',
osArch: 'x64',
osRelease: '22.0.0',
nodeVersion: 'v20.0.0',
npmVersion: '10.0.0',
sandboxEnv: 'test',
modelVersion: 'qwen3-coder-plus',
selectedAuthType: AuthType.USE_OPENAI,
ideClient: 'VSCode',
sessionId: 'test-session-id',
memoryUsage: '100 MB',
baseUrl: 'https://api.openai.com/v1',
gitCommit:
GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO)
? GIT_COMMIT_INFO
: undefined,
});
const mockContext = createMockCommandContext({
services: {
config: {
getModel: () => 'qwen3-coder-plus',
getBugCommand: () => undefined,
getIdeMode: () => true,
getSessionId: () => 'test-session-id',
getContentGeneratorConfig: () => ({
baseUrl: 'https://api.openai.com/v1',
}),
},
settings: {
merged: {
security: {
auth: {
selectedType: AuthType.USE_OPENAI,
},
},
},
},
},
});
@@ -152,22 +164,15 @@ ${gitCommitLine}* **Model:** qwen3-coder-plus
if (!bugCommand.action) throw new Error('Action is not defined');
await bugCommand.action(mockContext, 'OpenAI bug');
const gitCommitLine =
GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO)
? `* **Git Commit:** ${GIT_COMMIT_INFO}\n`
: '';
const expectedInfo = `
* **CLI Version:** 0.1.0
${gitCommitLine}* **Model:** qwen3-coder-plus
* **Sandbox:** test
* **OS Platform:** test-platform
* **OS Arch:** x64
* **OS Release:** 22.0.0
* **Node.js Version:** v20.0.0
* **NPM Version:** 10.0.0
* **Git Commit:** ${GIT_COMMIT_INFO}
* **Session ID:** test-session-id
* **Auth Method:** ${AuthType.USE_OPENAI}
* **Operating System:** test-platform v20.0.0
* **Sandbox Environment:** test
* **Auth Type:** ${AuthType.USE_OPENAI}
* **Base URL:** https://api.openai.com/v1
* **Model Version:** qwen3-coder-plus
* **Memory Usage:** 100 MB
* **IDE Client:** VSCode
`;

View File

@@ -5,17 +5,17 @@
*/
import open from 'open';
import process from 'node:process';
import {
type CommandContext,
type SlashCommand,
CommandKind,
} from './types.js';
import { MessageType } from '../types.js';
import { getExtendedSystemInfo } from '../../utils/systemInfo.js';
import {
getSystemInfoFields,
getFieldValue,
} from '../../utils/systemInfoFields.js';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
import { formatMemoryUsage } from '../utils/formatters.js';
import { getCliVersion } from '../../utils/version.js';
import { IdeClient, AuthType } from '@qwen-code/qwen-code-core';
export const bugCommand: SlashCommand = {
name: 'bug',
@@ -23,20 +23,50 @@ export const bugCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
action: async (context: CommandContext, args?: string): Promise<void> => {
const bugDescription = (args || '').trim();
const systemInfo = await getExtendedSystemInfo(context);
const { config } = context.services;
const fields = getSystemInfoFields(systemInfo);
const osVersion = `${process.platform} ${process.version}`;
let sandboxEnv = 'no sandbox';
if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') {
sandboxEnv = process.env['SANDBOX'].replace(/^qwen-(?:code-)?/, '');
} else if (process.env['SANDBOX'] === 'sandbox-exec') {
sandboxEnv = `sandbox-exec (${
process.env['SEATBELT_PROFILE'] || 'unknown'
})`;
}
const modelVersion = config?.getModel() || 'Unknown';
const cliVersion = await getCliVersion();
const memoryUsage = formatMemoryUsage(process.memoryUsage().rss);
const ideClient = await getIdeClientName(context);
const selectedAuthType =
context.services.settings.merged.security?.auth?.selectedType || '';
const baseUrl =
selectedAuthType === AuthType.USE_OPENAI
? config?.getContentGeneratorConfig()?.baseUrl
: undefined;
// Generate bug report info using the same field configuration
let info = '\n';
for (const field of fields) {
info += `* **${field.label}:** ${getFieldValue(field, systemInfo)}\n`;
let info = `
* **CLI Version:** ${cliVersion}
* **Git Commit:** ${GIT_COMMIT_INFO}
* **Session ID:** ${config?.getSessionId() || 'unknown'}
* **Operating System:** ${osVersion}
* **Sandbox Environment:** ${sandboxEnv}
* **Auth Type:** ${selectedAuthType}`;
if (baseUrl) {
info += `\n* **Base URL:** ${baseUrl}`;
}
info += `
* **Model Version:** ${modelVersion}
* **Memory Usage:** ${memoryUsage}
`;
if (ideClient) {
info += `* **IDE Client:** ${ideClient}\n`;
}
let bugReportUrl =
'https://github.com/QwenLM/qwen-code/issues/new?template=bug_report.yml&title={title}&info={info}';
const bugCommandSettings = context.services.config?.getBugCommand();
const bugCommandSettings = config?.getBugCommand();
if (bugCommandSettings?.urlTemplate) {
bugReportUrl = bugCommandSettings.urlTemplate;
}
@@ -68,3 +98,11 @@ export const bugCommand: SlashCommand = {
}
},
};
async function getIdeClientName(context: CommandContext) {
if (!context.services.config?.getIdeMode()) {
return '';
}
const ideClient = await IdeClient.getInstance();
return ideClient.getDetectedIdeDisplayName() ?? '';
}

View File

@@ -7,46 +7,127 @@
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import type { ExtendedSystemInfo } from '../../utils/systemInfo.js';
import {
getSystemInfoFields,
getFieldValue,
type SystemInfoField,
} from '../../utils/systemInfoFields.js';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
type AboutBoxProps = ExtendedSystemInfo;
interface AboutBoxProps {
cliVersion: string;
osVersion: string;
sandboxEnv: string;
modelVersion: string;
selectedAuthType: string;
gcpProject: string;
ideClient: string;
}
export const AboutBox: React.FC<AboutBoxProps> = (props) => {
const fields = getSystemInfoFields(props);
return (
<Box
borderStyle="round"
borderColor={theme.border.default}
flexDirection="column"
padding={1}
marginY={1}
width="100%"
>
<Box marginBottom={1}>
<Text bold color={theme.text.accent}>
About Qwen Code
export const AboutBox: React.FC<AboutBoxProps> = ({
cliVersion,
osVersion,
sandboxEnv,
modelVersion,
selectedAuthType,
gcpProject,
ideClient,
}) => (
<Box
borderStyle="round"
borderColor={theme.border.default}
flexDirection="column"
padding={1}
marginY={1}
width="100%"
>
<Box marginBottom={1}>
<Text bold color={theme.text.accent}>
About Qwen Code
</Text>
</Box>
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
CLI Version
</Text>
</Box>
{fields.map((field: SystemInfoField) => (
<Box key={field.key} flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
{field.label}
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>
{getFieldValue(field, props)}
</Text>
</Box>
</Box>
))}
<Box>
<Text color={theme.text.primary}>{cliVersion}</Text>
</Box>
</Box>
);
};
{GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && (
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
Git Commit
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{GIT_COMMIT_INFO}</Text>
</Box>
</Box>
)}
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
Model
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{modelVersion}</Text>
</Box>
</Box>
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
Sandbox
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{sandboxEnv}</Text>
</Box>
</Box>
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
OS
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{osVersion}</Text>
</Box>
</Box>
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
Auth Method
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>
{selectedAuthType.startsWith('oauth') ? 'OAuth' : selectedAuthType}
</Text>
</Box>
</Box>
{gcpProject && (
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
GCP Project
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{gcpProject}</Text>
</Box>
</Box>
)}
{ideClient && (
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
IDE Client
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{ideClient}</Text>
</Box>
</Box>
)}
</Box>
);

View File

@@ -71,24 +71,15 @@ describe('<HistoryItemDisplay />', () => {
it('renders AboutBox for "about" type', () => {
const item: HistoryItem = {
id: 1,
...baseItem,
type: MessageType.ABOUT,
systemInfo: {
cliVersion: '1.0.0',
osPlatform: 'test-os',
osArch: 'x64',
osRelease: '22.0.0',
nodeVersion: 'v20.0.0',
npmVersion: '10.0.0',
sandboxEnv: 'test-env',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
ideClient: 'test-ide',
sessionId: 'test-session-id',
memoryUsage: '100 MB',
baseUrl: undefined,
gitCommit: undefined,
},
cliVersion: '1.0.0',
osVersion: 'test-os',
sandboxEnv: 'test-env',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
gcpProject: 'test-project',
ideClient: 'test-ide',
};
const { lastFrame } = renderWithProviders(
<HistoryItemDisplay {...baseItem} item={item} />,

View File

@@ -95,7 +95,15 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
<ErrorMessage text={itemForDisplay.text} />
)}
{itemForDisplay.type === 'about' && (
<AboutBox {...itemForDisplay.systemInfo} />
<AboutBox
cliVersion={itemForDisplay.cliVersion}
osVersion={itemForDisplay.osVersion}
sandboxEnv={itemForDisplay.sandboxEnv}
modelVersion={itemForDisplay.modelVersion}
selectedAuthType={itemForDisplay.selectedAuthType}
gcpProject={itemForDisplay.gcpProject}
ideClient={itemForDisplay.ideClient}
/>
)}
{itemForDisplay.type === 'help' && commands && (
<Help commands={commands} />

View File

@@ -138,7 +138,13 @@ export const useSlashCommandProcessor = (
if (message.type === MessageType.ABOUT) {
historyItemContent = {
type: 'about',
systemInfo: message.systemInfo,
cliVersion: message.cliVersion,
osVersion: message.osVersion,
sandboxEnv: message.sandboxEnv,
modelVersion: message.modelVersion,
selectedAuthType: message.selectedAuthType,
gcpProject: message.gcpProject,
ideClient: message.ideClient,
};
} else if (message.type === MessageType.HELP) {
historyItemContent = {

View File

@@ -120,22 +120,13 @@ export type HistoryItemWarning = HistoryItemBase & {
export type HistoryItemAbout = HistoryItemBase & {
type: 'about';
systemInfo: {
cliVersion: string;
osPlatform: string;
osArch: string;
osRelease: string;
nodeVersion: string;
npmVersion: string;
sandboxEnv: string;
modelVersion: string;
selectedAuthType: string;
ideClient: string;
sessionId: string;
memoryUsage: string;
baseUrl?: string;
gitCommit?: string;
};
cliVersion: string;
osVersion: string;
sandboxEnv: string;
modelVersion: string;
selectedAuthType: string;
gcpProject: string;
ideClient: string;
};
export type HistoryItemHelp = HistoryItemBase & {
@@ -297,22 +288,13 @@ export type Message =
| {
type: MessageType.ABOUT;
timestamp: Date;
systemInfo: {
cliVersion: string;
osPlatform: string;
osArch: string;
osRelease: string;
nodeVersion: string;
npmVersion: string;
sandboxEnv: string;
modelVersion: string;
selectedAuthType: string;
ideClient: string;
sessionId: string;
memoryUsage: string;
baseUrl?: string;
gitCommit?: string;
};
cliVersion: string;
osVersion: string;
sandboxEnv: string;
modelVersion: string;
selectedAuthType: string;
gcpProject: string;
ideClient: string;
content?: string; // Optional content, not really used for ABOUT
}
| {

View File

@@ -1,331 +0,0 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import {
getSystemInfo,
getExtendedSystemInfo,
getNpmVersion,
getSandboxEnv,
getIdeClientName,
} from './systemInfo.js';
import type { CommandContext } from '../ui/commands/types.js';
import { createMockCommandContext } from '../test-utils/mockCommandContext.js';
import * as child_process from 'node:child_process';
import os from 'node:os';
import { IdeClient } from '@qwen-code/qwen-code-core';
import * as versionUtils from './version.js';
import type { ExecSyncOptions } from 'node:child_process';
vi.mock('node:child_process');
vi.mock('node:os', () => ({
default: {
release: vi.fn(),
},
}));
vi.mock('./version.js', () => ({
getCliVersion: vi.fn(),
}));
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
return {
...actual,
IdeClient: {
getInstance: vi.fn(),
},
};
});
describe('systemInfo', () => {
let mockContext: CommandContext;
const originalPlatform = process.platform;
const originalArch = process.arch;
const originalVersion = process.version;
const originalEnv = { ...process.env };
beforeEach(() => {
mockContext = createMockCommandContext({
services: {
config: {
getModel: vi.fn().mockReturnValue('test-model'),
getIdeMode: vi.fn().mockReturnValue(true),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
getContentGeneratorConfig: vi.fn().mockReturnValue({
baseUrl: 'https://api.openai.com',
}),
},
settings: {
merged: {
security: {
auth: {
selectedType: 'test-auth',
},
},
},
},
},
} as unknown as CommandContext);
vi.mocked(versionUtils.getCliVersion).mockResolvedValue('test-version');
vi.mocked(child_process.execSync).mockImplementation(
(command: string, options?: ExecSyncOptions) => {
if (
options &&
typeof options === 'object' &&
'encoding' in options &&
options.encoding === 'utf-8'
) {
return '10.0.0';
}
return Buffer.from('10.0.0', 'utf-8');
},
);
vi.mocked(os.release).mockReturnValue('22.0.0');
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-gcp-project';
Object.defineProperty(process, 'platform', {
value: 'test-os',
});
Object.defineProperty(process, 'arch', {
value: 'x64',
});
Object.defineProperty(process, 'version', {
value: 'v20.0.0',
});
});
afterEach(() => {
vi.unstubAllEnvs();
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
Object.defineProperty(process, 'arch', {
value: originalArch,
});
Object.defineProperty(process, 'version', {
value: originalVersion,
});
process.env = originalEnv;
vi.clearAllMocks();
vi.resetAllMocks();
});
describe('getNpmVersion', () => {
it('should return npm version when available', async () => {
vi.mocked(child_process.execSync).mockImplementation(
(command: string, options?: ExecSyncOptions) => {
if (
options &&
typeof options === 'object' &&
'encoding' in options &&
options.encoding === 'utf-8'
) {
return '10.0.0';
}
return Buffer.from('10.0.0', 'utf-8');
},
);
const version = await getNpmVersion();
expect(version).toBe('10.0.0');
});
it('should return unknown when npm command fails', async () => {
vi.mocked(child_process.execSync).mockImplementation(() => {
throw new Error('npm not found');
});
const version = await getNpmVersion();
expect(version).toBe('unknown');
});
});
describe('getSandboxEnv', () => {
it('should return "no sandbox" when SANDBOX is not set', () => {
delete process.env['SANDBOX'];
expect(getSandboxEnv()).toBe('no sandbox');
});
it('should return sandbox-exec info when SANDBOX is sandbox-exec', () => {
process.env['SANDBOX'] = 'sandbox-exec';
process.env['SEATBELT_PROFILE'] = 'test-profile';
expect(getSandboxEnv()).toBe('sandbox-exec (test-profile)');
});
it('should return sandbox name without prefix when stripPrefix is true', () => {
process.env['SANDBOX'] = 'qwen-code-test-sandbox';
expect(getSandboxEnv(true)).toBe('test-sandbox');
});
it('should return sandbox name with prefix when stripPrefix is false', () => {
process.env['SANDBOX'] = 'qwen-code-test-sandbox';
expect(getSandboxEnv(false)).toBe('qwen-code-test-sandbox');
});
it('should handle qwen- prefix removal', () => {
process.env['SANDBOX'] = 'qwen-custom-sandbox';
expect(getSandboxEnv(true)).toBe('custom-sandbox');
});
});
describe('getIdeClientName', () => {
it('should return IDE client name when IDE mode is enabled', async () => {
vi.mocked(IdeClient.getInstance).mockResolvedValue({
getDetectedIdeDisplayName: vi.fn().mockReturnValue('test-ide'),
} as unknown as IdeClient);
const ideClient = await getIdeClientName(mockContext);
expect(ideClient).toBe('test-ide');
});
it('should return empty string when IDE mode is disabled', async () => {
vi.mocked(mockContext.services.config!.getIdeMode).mockReturnValue(false);
const ideClient = await getIdeClientName(mockContext);
expect(ideClient).toBe('');
});
it('should return empty string when IDE client detection fails', async () => {
vi.mocked(IdeClient.getInstance).mockRejectedValue(
new Error('IDE client error'),
);
const ideClient = await getIdeClientName(mockContext);
expect(ideClient).toBe('');
});
});
describe('getSystemInfo', () => {
it('should collect all system information', async () => {
// Ensure SANDBOX is not set for this test
delete process.env['SANDBOX'];
vi.mocked(IdeClient.getInstance).mockResolvedValue({
getDetectedIdeDisplayName: vi.fn().mockReturnValue('test-ide'),
} as unknown as IdeClient);
vi.mocked(child_process.execSync).mockImplementation(
(command: string, options?: ExecSyncOptions) => {
if (
options &&
typeof options === 'object' &&
'encoding' in options &&
options.encoding === 'utf-8'
) {
return '10.0.0';
}
return Buffer.from('10.0.0', 'utf-8');
},
);
const systemInfo = await getSystemInfo(mockContext);
expect(systemInfo).toEqual({
cliVersion: 'test-version',
osPlatform: 'test-os',
osArch: 'x64',
osRelease: '22.0.0',
nodeVersion: 'v20.0.0',
npmVersion: '10.0.0',
sandboxEnv: 'no sandbox',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
ideClient: 'test-ide',
sessionId: 'test-session-id',
});
});
it('should handle missing config gracefully', async () => {
mockContext.services.config = null;
vi.mocked(IdeClient.getInstance).mockResolvedValue({
getDetectedIdeDisplayName: vi.fn().mockReturnValue(''),
} as unknown as IdeClient);
const systemInfo = await getSystemInfo(mockContext);
expect(systemInfo.modelVersion).toBe('Unknown');
expect(systemInfo.sessionId).toBe('unknown');
});
});
describe('getExtendedSystemInfo', () => {
it('should include memory usage and base URL', async () => {
vi.mocked(IdeClient.getInstance).mockResolvedValue({
getDetectedIdeDisplayName: vi.fn().mockReturnValue('test-ide'),
} as unknown as IdeClient);
vi.mocked(child_process.execSync).mockImplementation(
(command: string, options?: ExecSyncOptions) => {
if (
options &&
typeof options === 'object' &&
'encoding' in options &&
options.encoding === 'utf-8'
) {
return '10.0.0';
}
return Buffer.from('10.0.0', 'utf-8');
},
);
const { AuthType } = await import('@qwen-code/qwen-code-core');
// Update the mock context to use OpenAI auth
mockContext.services.settings.merged.security!.auth!.selectedType =
AuthType.USE_OPENAI;
const extendedInfo = await getExtendedSystemInfo(mockContext);
expect(extendedInfo.memoryUsage).toBeDefined();
expect(extendedInfo.memoryUsage).toMatch(/\d+\.\d+ (KB|MB|GB)/);
expect(extendedInfo.baseUrl).toBe('https://api.openai.com');
});
it('should use sandbox env without prefix for bug reports', async () => {
process.env['SANDBOX'] = 'qwen-code-test-sandbox';
vi.mocked(IdeClient.getInstance).mockResolvedValue({
getDetectedIdeDisplayName: vi.fn().mockReturnValue(''),
} as unknown as IdeClient);
vi.mocked(child_process.execSync).mockImplementation(
(command: string, options?: ExecSyncOptions) => {
if (
options &&
typeof options === 'object' &&
'encoding' in options &&
options.encoding === 'utf-8'
) {
return '10.0.0';
}
return Buffer.from('10.0.0', 'utf-8');
},
);
const extendedInfo = await getExtendedSystemInfo(mockContext);
expect(extendedInfo.sandboxEnv).toBe('test-sandbox');
});
it('should not include base URL for non-OpenAI auth', async () => {
vi.mocked(IdeClient.getInstance).mockResolvedValue({
getDetectedIdeDisplayName: vi.fn().mockReturnValue(''),
} as unknown as IdeClient);
vi.mocked(child_process.execSync).mockImplementation(
(command: string, options?: ExecSyncOptions) => {
if (
options &&
typeof options === 'object' &&
'encoding' in options &&
options.encoding === 'utf-8'
) {
return '10.0.0';
}
return Buffer.from('10.0.0', 'utf-8');
},
);
const extendedInfo = await getExtendedSystemInfo(mockContext);
expect(extendedInfo.baseUrl).toBeUndefined();
});
});
});

View File

@@ -1,173 +0,0 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import process from 'node:process';
import os from 'node:os';
import { execSync } from 'node:child_process';
import type { CommandContext } from '../ui/commands/types.js';
import { getCliVersion } from './version.js';
import { IdeClient, AuthType } from '@qwen-code/qwen-code-core';
import { formatMemoryUsage } from '../ui/utils/formatters.js';
import { GIT_COMMIT_INFO } from '../generated/git-commit.js';
/**
* System information interface containing all system-related details
* that can be collected for debugging and reporting purposes.
*/
export interface SystemInfo {
cliVersion: string;
osPlatform: string;
osArch: string;
osRelease: string;
nodeVersion: string;
npmVersion: string;
sandboxEnv: string;
modelVersion: string;
selectedAuthType: string;
ideClient: string;
sessionId: string;
}
/**
* Additional system information for bug reports
*/
export interface ExtendedSystemInfo extends SystemInfo {
memoryUsage: string;
baseUrl?: string;
gitCommit?: string;
}
/**
* Gets the NPM version, handling cases where npm might not be available.
* Returns 'unknown' if npm command fails or is not found.
*/
export async function getNpmVersion(): Promise<string> {
try {
return execSync('npm --version', { encoding: 'utf-8' }).trim();
} catch {
return 'unknown';
}
}
/**
* Gets the IDE client name if IDE mode is enabled.
* Returns empty string if IDE mode is disabled or IDE client is not detected.
*/
export async function getIdeClientName(
context: CommandContext,
): Promise<string> {
if (!context.services.config?.getIdeMode()) {
return '';
}
try {
const ideClient = await IdeClient.getInstance();
return ideClient?.getDetectedIdeDisplayName() ?? '';
} catch {
return '';
}
}
/**
* Gets the sandbox environment information.
* Handles different sandbox types including sandbox-exec and custom sandbox environments.
* For bug reports, removes 'qwen-' or 'qwen-code-' prefixes from sandbox names.
*
* @param stripPrefix - Whether to strip 'qwen-' prefix (used for bug reports)
*/
export function getSandboxEnv(stripPrefix = false): string {
const sandbox = process.env['SANDBOX'];
if (!sandbox || sandbox === 'sandbox-exec') {
if (sandbox === 'sandbox-exec') {
const profile = process.env['SEATBELT_PROFILE'] || 'unknown';
return `sandbox-exec (${profile})`;
}
return 'no sandbox';
}
// For bug reports, remove qwen- prefix
if (stripPrefix) {
return sandbox.replace(/^qwen-(?:code-)?/, '');
}
return sandbox;
}
/**
* Collects comprehensive system information for debugging and reporting.
* This function gathers all system-related details including OS, versions,
* sandbox environment, authentication, and session information.
*
* @param context - Command context containing config and settings
* @returns Promise resolving to SystemInfo object with all collected information
*/
export async function getSystemInfo(
context: CommandContext,
): Promise<SystemInfo> {
const osPlatform = process.platform;
const osArch = process.arch;
const osRelease = os.release();
const nodeVersion = process.version;
const npmVersion = await getNpmVersion();
const sandboxEnv = getSandboxEnv();
const modelVersion = context.services.config?.getModel() || 'Unknown';
const cliVersion = await getCliVersion();
const selectedAuthType =
context.services.settings.merged.security?.auth?.selectedType || '';
const ideClient = await getIdeClientName(context);
const sessionId = context.services.config?.getSessionId() || 'unknown';
return {
cliVersion,
osPlatform,
osArch,
osRelease,
nodeVersion,
npmVersion,
sandboxEnv,
modelVersion,
selectedAuthType,
ideClient,
sessionId,
};
}
/**
* Collects extended system information for bug reports.
* Includes all standard system info plus memory usage and optional base URL.
*
* @param context - Command context containing config and settings
* @returns Promise resolving to ExtendedSystemInfo object
*/
export async function getExtendedSystemInfo(
context: CommandContext,
): Promise<ExtendedSystemInfo> {
const baseInfo = await getSystemInfo(context);
const memoryUsage = formatMemoryUsage(process.memoryUsage().rss);
// For bug reports, use sandbox name without prefix
const sandboxEnv = getSandboxEnv(true);
// Get base URL if using OpenAI auth
const baseUrl =
baseInfo.selectedAuthType === AuthType.USE_OPENAI
? context.services.config?.getContentGeneratorConfig()?.baseUrl
: undefined;
// Get git commit info
const gitCommit =
GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO)
? GIT_COMMIT_INFO
: undefined;
return {
...baseInfo,
sandboxEnv,
memoryUsage,
baseUrl,
gitCommit,
};
}

View File

@@ -1,117 +0,0 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import type { ExtendedSystemInfo } from './systemInfo.js';
/**
* Field configuration for system information display
*/
export interface SystemInfoField {
label: string;
key: keyof ExtendedSystemInfo;
}
/**
* Unified field configuration for system information display.
* This ensures consistent labeling between /about and /bug commands.
*/
export function getSystemInfoFields(
info: ExtendedSystemInfo,
): SystemInfoField[] {
const allFields: SystemInfoField[] = [
{
label: 'CLI Version',
key: 'cliVersion',
},
{
label: 'Git Commit',
key: 'gitCommit',
},
{
label: 'Model',
key: 'modelVersion',
},
{
label: 'Sandbox',
key: 'sandboxEnv',
},
{
label: 'OS Platform',
key: 'osPlatform',
},
{
label: 'OS Arch',
key: 'osArch',
},
{
label: 'OS Release',
key: 'osRelease',
},
{
label: 'Node.js Version',
key: 'nodeVersion',
},
{
label: 'NPM Version',
key: 'npmVersion',
},
{
label: 'Session ID',
key: 'sessionId',
},
{
label: 'Auth Method',
key: 'selectedAuthType',
},
{
label: 'Base URL',
key: 'baseUrl',
},
{
label: 'Memory Usage',
key: 'memoryUsage',
},
{
label: 'IDE Client',
key: 'ideClient',
},
];
// Filter out optional fields that are not present
return allFields.filter((field) => {
const value = info[field.key];
// Optional fields: only show if they exist and are non-empty
if (
field.key === 'baseUrl' ||
field.key === 'gitCommit' ||
field.key === 'ideClient'
) {
return Boolean(value);
}
return true;
});
}
/**
* Get the value for a field from system info
*/
export function getFieldValue(
field: SystemInfoField,
info: ExtendedSystemInfo,
): string {
const value = info[field.key];
if (value === undefined || value === null) {
return '';
}
// Special formatting for selectedAuthType
if (field.key === 'selectedAuthType') {
return String(value).startsWith('oauth') ? 'OAuth' : String(value);
}
return String(value);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-core",
"version": "0.1.4",
"version": "0.1.5",
"description": "Qwen Code Core",
"repository": {
"type": "git",

View File

@@ -60,7 +60,10 @@ function verifyVSCode(
if (ide.name !== IDE_DEFINITIONS.vscode.name) {
return ide;
}
if (ideProcessInfo.command.toLowerCase().includes('code')) {
if (
ideProcessInfo.command &&
ideProcessInfo.command.toLowerCase().includes('code')
) {
return IDE_DEFINITIONS.vscode;
}
return IDE_DEFINITIONS.vscodefork;

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.1.4",
"version": "0.1.5",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",

View File

@@ -2,7 +2,7 @@
"name": "qwen-code-vscode-ide-companion",
"displayName": "Qwen Code Companion",
"description": "Enable Qwen Code with direct access to your VS Code workspace.",
"version": "0.1.4",
"version": "0.1.5",
"publisher": "qwenlm",
"icon": "assets/icon.png",
"repository": {