mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-01-19 07:16:19 +00:00
Compare commits
3 Commits
mingholy/f
...
fix/vscode
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e80e80387 | ||
|
|
bde056b62e | ||
|
|
97497457a8 |
@@ -18,7 +18,6 @@ import { copyCommand } from '../ui/commands/copyCommand.js';
|
||||
import { docsCommand } from '../ui/commands/docsCommand.js';
|
||||
import { directoryCommand } from '../ui/commands/directoryCommand.js';
|
||||
import { editorCommand } from '../ui/commands/editorCommand.js';
|
||||
import { exportCommand } from '../ui/commands/exportCommand.js';
|
||||
import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
|
||||
import { helpCommand } from '../ui/commands/helpCommand.js';
|
||||
import { ideCommand } from '../ui/commands/ideCommand.js';
|
||||
@@ -68,7 +67,6 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
docsCommand,
|
||||
directoryCommand,
|
||||
editorCommand,
|
||||
exportCommand,
|
||||
extensionsCommand,
|
||||
helpCommand,
|
||||
await ideCommand(),
|
||||
|
||||
@@ -1,379 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { exportCommand } from './exportCommand.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import type { ChatRecord } from '@qwen-code/qwen-code-core';
|
||||
import type { Part, Content } from '@google/genai';
|
||||
import {
|
||||
transformToMarkdown,
|
||||
loadHtmlTemplate,
|
||||
prepareExportData,
|
||||
injectDataIntoHtmlTemplate,
|
||||
generateExportFilename,
|
||||
} from '../utils/exportUtils.js';
|
||||
|
||||
const mockSessionServiceMocks = vi.hoisted(() => ({
|
||||
loadLastSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@qwen-code/qwen-code-core', () => {
|
||||
class SessionService {
|
||||
constructor(_cwd: string) {}
|
||||
async loadLastSession() {
|
||||
return mockSessionServiceMocks.loadLastSession();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
SessionService,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../utils/exportUtils.js', () => ({
|
||||
transformToMarkdown: vi.fn(),
|
||||
loadHtmlTemplate: vi.fn(),
|
||||
prepareExportData: vi.fn(),
|
||||
injectDataIntoHtmlTemplate: vi.fn(),
|
||||
generateExportFilename: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
writeFile: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('exportCommand', () => {
|
||||
const mockSessionData = {
|
||||
conversation: {
|
||||
sessionId: 'test-session-id',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
messages: [
|
||||
{
|
||||
type: 'user',
|
||||
message: {
|
||||
parts: [{ text: 'Hello' }] as Part[],
|
||||
} as Content,
|
||||
},
|
||||
] as ChatRecord[],
|
||||
},
|
||||
};
|
||||
|
||||
let mockContext: ReturnType<typeof createMockCommandContext>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockSessionServiceMocks.loadLastSession.mockResolvedValue(mockSessionData);
|
||||
|
||||
mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getWorkingDir: vi.fn().mockReturnValue('/test/dir'),
|
||||
getProjectRoot: vi.fn().mockReturnValue('/test/project'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
vi.mocked(transformToMarkdown).mockReturnValue('# Test Markdown');
|
||||
vi.mocked(loadHtmlTemplate).mockResolvedValue(
|
||||
'<html><script id="chat-data" type="application/json">// DATA_PLACEHOLDER</script></html>',
|
||||
);
|
||||
vi.mocked(prepareExportData).mockReturnValue({
|
||||
sessionId: 'test-session-id',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
messages: mockSessionData.conversation.messages,
|
||||
});
|
||||
vi.mocked(injectDataIntoHtmlTemplate).mockReturnValue(
|
||||
'<html><script id="chat-data" type="application/json">{"data": "test"}</script></html>',
|
||||
);
|
||||
vi.mocked(generateExportFilename).mockImplementation(
|
||||
(ext: string) => `export-2025-01-01T00-00-00-000Z.${ext}`,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('command structure', () => {
|
||||
it('should have correct name and description', () => {
|
||||
expect(exportCommand.name).toBe('export');
|
||||
expect(exportCommand.description).toBe(
|
||||
'Export current session message history to a file',
|
||||
);
|
||||
});
|
||||
|
||||
it('should have md and html subcommands', () => {
|
||||
expect(exportCommand.subCommands).toHaveLength(2);
|
||||
expect(exportCommand.subCommands?.map((c) => c.name)).toEqual([
|
||||
'md',
|
||||
'html',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportMarkdownAction', () => {
|
||||
it('should export session to markdown file', async () => {
|
||||
const mdCommand = exportCommand.subCommands?.find((c) => c.name === 'md');
|
||||
if (!mdCommand?.action) {
|
||||
throw new Error('md command not found');
|
||||
}
|
||||
|
||||
const result = await mdCommand.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: expect.stringContaining('export-2025-01-01T00-00-00-000Z.md'),
|
||||
});
|
||||
|
||||
expect(mockSessionServiceMocks.loadLastSession).toHaveBeenCalled();
|
||||
expect(transformToMarkdown).toHaveBeenCalledWith(
|
||||
mockSessionData.conversation.messages,
|
||||
'test-session-id',
|
||||
'2025-01-01T00:00:00Z',
|
||||
);
|
||||
expect(generateExportFilename).toHaveBeenCalledWith('md');
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
expect.stringContaining('export-2025-01-01T00-00-00-000Z.md'),
|
||||
'# Test Markdown',
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error when config is not available', async () => {
|
||||
const contextWithoutConfig = createMockCommandContext({
|
||||
services: {
|
||||
config: null,
|
||||
},
|
||||
});
|
||||
|
||||
const mdCommand = exportCommand.subCommands?.find((c) => c.name === 'md');
|
||||
if (!mdCommand?.action) {
|
||||
throw new Error('md command not found');
|
||||
}
|
||||
const result = await mdCommand.action(contextWithoutConfig, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Configuration not available.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when working directory cannot be determined', async () => {
|
||||
const contextWithoutCwd = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getWorkingDir: vi.fn().mockReturnValue(null),
|
||||
getProjectRoot: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mdCommand = exportCommand.subCommands?.find((c) => c.name === 'md');
|
||||
if (!mdCommand || !mdCommand.action) {
|
||||
throw new Error('md command not found');
|
||||
}
|
||||
const result = await mdCommand.action(contextWithoutCwd, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Could not determine current working directory.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when no session is found', async () => {
|
||||
mockSessionServiceMocks.loadLastSession.mockResolvedValue(undefined);
|
||||
|
||||
const mdCommand = exportCommand.subCommands?.find((c) => c.name === 'md');
|
||||
if (!mdCommand?.action) {
|
||||
throw new Error('md command not found');
|
||||
}
|
||||
const result = await mdCommand.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'No active session found to export.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors during export', async () => {
|
||||
const error = new Error('File write failed');
|
||||
vi.mocked(fs.writeFile).mockRejectedValue(error);
|
||||
|
||||
const mdCommand = exportCommand.subCommands?.find((c) => c.name === 'md');
|
||||
if (!mdCommand?.action) {
|
||||
throw new Error('md command not found');
|
||||
}
|
||||
const result = await mdCommand.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Failed to export session: File write failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use project root when working dir is not available', async () => {
|
||||
const contextWithProjectRoot = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getWorkingDir: vi.fn().mockReturnValue(null),
|
||||
getProjectRoot: vi.fn().mockReturnValue('/test/project'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mdCommand = exportCommand.subCommands?.find((c) => c.name === 'md');
|
||||
if (!mdCommand?.action) {
|
||||
throw new Error('md command not found');
|
||||
}
|
||||
await mdCommand.action(contextWithProjectRoot, '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportHtmlAction', () => {
|
||||
it('should export session to HTML file', async () => {
|
||||
const htmlCommand = exportCommand.subCommands?.find(
|
||||
(c) => c.name === 'html',
|
||||
);
|
||||
if (!htmlCommand?.action) {
|
||||
throw new Error('html command not found');
|
||||
}
|
||||
|
||||
const result = await htmlCommand.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: expect.stringContaining(
|
||||
'export-2025-01-01T00-00-00-000Z.html',
|
||||
),
|
||||
});
|
||||
|
||||
expect(mockSessionServiceMocks.loadLastSession).toHaveBeenCalled();
|
||||
expect(loadHtmlTemplate).toHaveBeenCalled();
|
||||
expect(prepareExportData).toHaveBeenCalledWith(
|
||||
mockSessionData.conversation,
|
||||
);
|
||||
expect(injectDataIntoHtmlTemplate).toHaveBeenCalled();
|
||||
expect(generateExportFilename).toHaveBeenCalledWith('html');
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
expect.stringContaining('export-2025-01-01T00-00-00-000Z.html'),
|
||||
expect.stringContaining('{"data": "test"}'),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error when config is not available', async () => {
|
||||
const contextWithoutConfig = createMockCommandContext({
|
||||
services: {
|
||||
config: null,
|
||||
},
|
||||
});
|
||||
|
||||
const htmlCommand = exportCommand.subCommands?.find(
|
||||
(c) => c.name === 'html',
|
||||
);
|
||||
if (!htmlCommand?.action) {
|
||||
throw new Error('html command not found');
|
||||
}
|
||||
const result = await htmlCommand.action(contextWithoutConfig, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Configuration not available.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when working directory cannot be determined', async () => {
|
||||
const contextWithoutCwd = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getWorkingDir: vi.fn().mockReturnValue(null),
|
||||
getProjectRoot: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const htmlCommand = exportCommand.subCommands?.find(
|
||||
(c) => c.name === 'html',
|
||||
);
|
||||
if (!htmlCommand || !htmlCommand.action) {
|
||||
throw new Error('html command not found');
|
||||
}
|
||||
const result = await htmlCommand.action(contextWithoutCwd, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Could not determine current working directory.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when no session is found', async () => {
|
||||
mockSessionServiceMocks.loadLastSession.mockResolvedValue(undefined);
|
||||
|
||||
const htmlCommand = exportCommand.subCommands?.find(
|
||||
(c) => c.name === 'html',
|
||||
);
|
||||
if (!htmlCommand?.action) {
|
||||
throw new Error('html command not found');
|
||||
}
|
||||
const result = await htmlCommand.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'No active session found to export.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors during HTML template loading', async () => {
|
||||
const error = new Error('Failed to fetch template');
|
||||
vi.mocked(loadHtmlTemplate).mockRejectedValue(error);
|
||||
|
||||
const htmlCommand = exportCommand.subCommands?.find(
|
||||
(c) => c.name === 'html',
|
||||
);
|
||||
if (!htmlCommand?.action) {
|
||||
throw new Error('html command not found');
|
||||
}
|
||||
const result = await htmlCommand.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Failed to export session: Failed to fetch template',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors during file write', async () => {
|
||||
const error = new Error('File write failed');
|
||||
vi.mocked(fs.writeFile).mockRejectedValue(error);
|
||||
|
||||
const htmlCommand = exportCommand.subCommands?.find(
|
||||
(c) => c.name === 'html',
|
||||
);
|
||||
if (!htmlCommand?.action) {
|
||||
throw new Error('html command not found');
|
||||
}
|
||||
const result = await htmlCommand.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Failed to export session: File write failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,177 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
type MessageActionReturn,
|
||||
CommandKind,
|
||||
} from './types.js';
|
||||
import { SessionService } from '@qwen-code/qwen-code-core';
|
||||
import {
|
||||
transformToMarkdown,
|
||||
loadHtmlTemplate,
|
||||
prepareExportData,
|
||||
injectDataIntoHtmlTemplate,
|
||||
generateExportFilename,
|
||||
} from '../utils/exportUtils.js';
|
||||
|
||||
/**
|
||||
* Action for the 'md' subcommand - exports session to markdown.
|
||||
*/
|
||||
async function exportMarkdownAction(
|
||||
context: CommandContext,
|
||||
): Promise<MessageActionReturn> {
|
||||
const { services } = context;
|
||||
const { config } = services;
|
||||
|
||||
if (!config) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Configuration not available.',
|
||||
};
|
||||
}
|
||||
|
||||
const cwd = config.getWorkingDir() || config.getProjectRoot();
|
||||
if (!cwd) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Could not determine current working directory.',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Load the current session
|
||||
const sessionService = new SessionService(cwd);
|
||||
const sessionData = await sessionService.loadLastSession();
|
||||
|
||||
if (!sessionData) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'No active session found to export.',
|
||||
};
|
||||
}
|
||||
|
||||
const { conversation } = sessionData;
|
||||
|
||||
const markdown = transformToMarkdown(
|
||||
conversation.messages,
|
||||
conversation.sessionId,
|
||||
conversation.startTime,
|
||||
);
|
||||
|
||||
const filename = generateExportFilename('md');
|
||||
const filepath = path.join(cwd, filename);
|
||||
|
||||
// Write to file
|
||||
await fs.writeFile(filepath, markdown, 'utf-8');
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Session exported to markdown: ${filename}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to export session: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action for the 'html' subcommand - exports session to HTML.
|
||||
*/
|
||||
async function exportHtmlAction(
|
||||
context: CommandContext,
|
||||
): Promise<MessageActionReturn> {
|
||||
const { services } = context;
|
||||
const { config } = services;
|
||||
|
||||
if (!config) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Configuration not available.',
|
||||
};
|
||||
}
|
||||
|
||||
const cwd = config.getWorkingDir() || config.getProjectRoot();
|
||||
if (!cwd) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Could not determine current working directory.',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Load the current session
|
||||
const sessionService = new SessionService(cwd);
|
||||
const sessionData = await sessionService.loadLastSession();
|
||||
|
||||
if (!sessionData) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'No active session found to export.',
|
||||
};
|
||||
}
|
||||
|
||||
const { conversation } = sessionData;
|
||||
|
||||
const template = await loadHtmlTemplate();
|
||||
const exportData = prepareExportData(conversation);
|
||||
const html = injectDataIntoHtmlTemplate(template, exportData);
|
||||
|
||||
const filename = generateExportFilename('html');
|
||||
const filepath = path.join(cwd, filename);
|
||||
|
||||
// Write to file
|
||||
await fs.writeFile(filepath, html, 'utf-8');
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Session exported to HTML: ${filename}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to export session: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main export command with subcommands.
|
||||
*/
|
||||
export const exportCommand: SlashCommand = {
|
||||
name: 'export',
|
||||
description: 'Export current session message history to a file',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [
|
||||
{
|
||||
name: 'md',
|
||||
description: 'Export session to markdown format',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: exportMarkdownAction,
|
||||
},
|
||||
{
|
||||
name: 'html',
|
||||
description: 'Export session to HTML format',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: exportHtmlAction,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,404 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
extractTextFromContent,
|
||||
transformToMarkdown,
|
||||
loadHtmlTemplate,
|
||||
prepareExportData,
|
||||
injectDataIntoHtmlTemplate,
|
||||
generateExportFilename,
|
||||
} from './exportUtils.js';
|
||||
import type { ChatRecord } from '@qwen-code/qwen-code-core';
|
||||
import type { Part, Content } from '@google/genai';
|
||||
|
||||
describe('exportUtils', () => {
|
||||
describe('extractTextFromContent', () => {
|
||||
it('should return empty string for undefined content', () => {
|
||||
expect(extractTextFromContent(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string for content without parts', () => {
|
||||
expect(extractTextFromContent({} as Content)).toBe('');
|
||||
});
|
||||
|
||||
it('should extract text from text parts', () => {
|
||||
const content: Content = {
|
||||
parts: [{ text: 'Hello' }, { text: 'World' }] as Part[],
|
||||
};
|
||||
expect(extractTextFromContent(content)).toBe('Hello\nWorld');
|
||||
});
|
||||
|
||||
it('should format function call parts', () => {
|
||||
const content: Content = {
|
||||
parts: [
|
||||
{
|
||||
functionCall: {
|
||||
name: 'testFunction',
|
||||
args: { param1: 'value1' },
|
||||
},
|
||||
},
|
||||
] as Part[],
|
||||
};
|
||||
const result = extractTextFromContent(content);
|
||||
expect(result).toContain('[Function Call: testFunction]');
|
||||
expect(result).toContain('"param1": "value1"');
|
||||
});
|
||||
|
||||
it('should format function response parts', () => {
|
||||
const content: Content = {
|
||||
parts: [
|
||||
{
|
||||
functionResponse: {
|
||||
name: 'testFunction',
|
||||
response: { result: 'success' },
|
||||
},
|
||||
},
|
||||
] as Part[],
|
||||
};
|
||||
const result = extractTextFromContent(content);
|
||||
expect(result).toContain('[Function Response: testFunction]');
|
||||
expect(result).toContain('"result": "success"');
|
||||
});
|
||||
|
||||
it('should handle mixed part types', () => {
|
||||
const content: Content = {
|
||||
parts: [
|
||||
{ text: 'Start' },
|
||||
{
|
||||
functionCall: {
|
||||
name: 'call',
|
||||
args: {},
|
||||
},
|
||||
},
|
||||
{ text: 'End' },
|
||||
] as Part[],
|
||||
};
|
||||
const result = extractTextFromContent(content);
|
||||
expect(result).toContain('Start');
|
||||
expect(result).toContain('[Function Call: call]');
|
||||
expect(result).toContain('End');
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformToMarkdown', () => {
|
||||
const mockMessages: ChatRecord[] = [
|
||||
{
|
||||
uuid: 'uuid-1',
|
||||
parentUuid: null,
|
||||
sessionId: 'test-session-id',
|
||||
timestamp: '2025-01-01T00:00:00Z',
|
||||
type: 'user',
|
||||
cwd: '/test',
|
||||
version: '1.0.0',
|
||||
message: {
|
||||
parts: [{ text: 'Hello, how are you?' }] as Part[],
|
||||
} as Content,
|
||||
},
|
||||
{
|
||||
uuid: 'uuid-2',
|
||||
parentUuid: 'uuid-1',
|
||||
sessionId: 'test-session-id',
|
||||
timestamp: '2025-01-01T00:00:01Z',
|
||||
type: 'assistant',
|
||||
cwd: '/test',
|
||||
version: '1.0.0',
|
||||
message: {
|
||||
parts: [{ text: 'I am doing well, thank you!' }] as Part[],
|
||||
} as Content,
|
||||
},
|
||||
];
|
||||
|
||||
it('should transform messages to markdown format', () => {
|
||||
const result = transformToMarkdown(
|
||||
mockMessages,
|
||||
'test-session-id',
|
||||
'2025-01-01T00:00:00Z',
|
||||
);
|
||||
|
||||
expect(result).toContain('# Chat Session Export');
|
||||
expect(result).toContain('**Session ID**: test-session-id');
|
||||
expect(result).toContain('**Start Time**: 2025-01-01T00:00:00Z');
|
||||
expect(result).toContain('## User');
|
||||
expect(result).toContain('Hello, how are you?');
|
||||
expect(result).toContain('## Assistant');
|
||||
expect(result).toContain('I am doing well, thank you!');
|
||||
});
|
||||
|
||||
it('should include exported timestamp', () => {
|
||||
const before = new Date().toISOString();
|
||||
const result = transformToMarkdown(
|
||||
mockMessages,
|
||||
'test-session-id',
|
||||
'2025-01-01T00:00:00Z',
|
||||
);
|
||||
const after = new Date().toISOString();
|
||||
|
||||
expect(result).toContain('**Exported**:');
|
||||
const exportedMatch = result.match(/\*\*Exported\*\*: (.+)/);
|
||||
expect(exportedMatch).toBeTruthy();
|
||||
if (exportedMatch) {
|
||||
const exportedTime = exportedMatch[1].trim();
|
||||
expect(exportedTime >= before).toBe(true);
|
||||
expect(exportedTime <= after).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should format tool_result messages', () => {
|
||||
const messages: ChatRecord[] = [
|
||||
{
|
||||
uuid: 'uuid-3',
|
||||
parentUuid: 'uuid-2',
|
||||
sessionId: 'test-session-id',
|
||||
timestamp: '2025-01-01T00:00:02Z',
|
||||
type: 'tool_result',
|
||||
cwd: '/test',
|
||||
version: '1.0.0',
|
||||
toolCallResult: {
|
||||
resultDisplay: 'Tool output',
|
||||
},
|
||||
message: {
|
||||
parts: [{ text: 'Additional info' }] as Part[],
|
||||
} as Content,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformToMarkdown(
|
||||
messages,
|
||||
'test-session-id',
|
||||
'2025-01-01T00:00:00Z',
|
||||
);
|
||||
|
||||
expect(result).toContain('## Tool Result');
|
||||
expect(result).toContain('```');
|
||||
expect(result).toContain('Tool output');
|
||||
expect(result).toContain('Additional info');
|
||||
});
|
||||
|
||||
it('should format tool_result with JSON resultDisplay', () => {
|
||||
const messages: ChatRecord[] = [
|
||||
{
|
||||
uuid: 'uuid-4',
|
||||
parentUuid: 'uuid-3',
|
||||
sessionId: 'test-session-id',
|
||||
timestamp: '2025-01-01T00:00:03Z',
|
||||
type: 'tool_result',
|
||||
cwd: '/test',
|
||||
version: '1.0.0',
|
||||
toolCallResult: {
|
||||
resultDisplay: '{"key": "value"}',
|
||||
},
|
||||
message: {} as Content,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformToMarkdown(
|
||||
messages,
|
||||
'test-session-id',
|
||||
'2025-01-01T00:00:00Z',
|
||||
);
|
||||
|
||||
expect(result).toContain('## Tool Result');
|
||||
expect(result).toContain('```');
|
||||
expect(result).toContain('"key": "value"');
|
||||
});
|
||||
|
||||
it('should handle chat compression system messages', () => {
|
||||
const messages: ChatRecord[] = [
|
||||
{
|
||||
uuid: 'uuid-5',
|
||||
parentUuid: null,
|
||||
sessionId: 'test-session-id',
|
||||
timestamp: '2025-01-01T00:00:04Z',
|
||||
type: 'system',
|
||||
subtype: 'chat_compression',
|
||||
cwd: '/test',
|
||||
version: '1.0.0',
|
||||
message: {} as Content,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformToMarkdown(
|
||||
messages,
|
||||
'test-session-id',
|
||||
'2025-01-01T00:00:00Z',
|
||||
);
|
||||
|
||||
expect(result).toContain('_[Chat history compressed]_');
|
||||
});
|
||||
|
||||
it('should skip system messages without subtype', () => {
|
||||
const messages: ChatRecord[] = [
|
||||
{
|
||||
uuid: 'uuid-6',
|
||||
parentUuid: null,
|
||||
sessionId: 'test-session-id',
|
||||
timestamp: '2025-01-01T00:00:05Z',
|
||||
type: 'system',
|
||||
cwd: '/test',
|
||||
version: '1.0.0',
|
||||
message: {} as Content,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformToMarkdown(
|
||||
messages,
|
||||
'test-session-id',
|
||||
'2025-01-01T00:00:00Z',
|
||||
);
|
||||
|
||||
expect(result).not.toContain('## System');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadHtmlTemplate', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', vi.fn());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('should load HTML template from URL', async () => {
|
||||
const mockTemplate = '<html><body>Test Template</body></html>';
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
text: vi.fn().mockResolvedValue(mockTemplate),
|
||||
};
|
||||
vi.mocked(fetch).mockResolvedValue(mockResponse as unknown as Response);
|
||||
|
||||
const result = await loadHtmlTemplate();
|
||||
|
||||
expect(result).toBe(mockTemplate);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
'https://raw.githubusercontent.com/QwenLM/qwen-code/main/template_portable.html',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when fetch fails', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
};
|
||||
vi.mocked(fetch).mockResolvedValue(mockResponse as unknown as Response);
|
||||
|
||||
await expect(loadHtmlTemplate()).rejects.toThrow(
|
||||
'Failed to fetch HTML template: 404 Not Found',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when network request fails', async () => {
|
||||
const networkError = new Error('Network error');
|
||||
vi.mocked(fetch).mockRejectedValue(networkError);
|
||||
|
||||
await expect(loadHtmlTemplate()).rejects.toThrow(
|
||||
'Failed to load HTML template',
|
||||
);
|
||||
await expect(loadHtmlTemplate()).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareExportData', () => {
|
||||
it('should prepare export data from conversation', () => {
|
||||
const conversation = {
|
||||
sessionId: 'test-session-id',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
messages: [
|
||||
{
|
||||
type: 'user',
|
||||
message: {
|
||||
parts: [{ text: 'Hello' }] as Part[],
|
||||
} as Content,
|
||||
},
|
||||
] as ChatRecord[],
|
||||
};
|
||||
|
||||
const result = prepareExportData(conversation);
|
||||
|
||||
expect(result).toEqual({
|
||||
sessionId: 'test-session-id',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
messages: conversation.messages,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectDataIntoHtmlTemplate', () => {
|
||||
it('should inject JSON data into HTML template', () => {
|
||||
const template = `
|
||||
<html>
|
||||
<body>
|
||||
<script id="chat-data" type="application/json">
|
||||
// DATA_PLACEHOLDER: Your JSONL data will be injected here
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const data = {
|
||||
sessionId: 'test-session-id',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
messages: [] as ChatRecord[],
|
||||
};
|
||||
|
||||
const result = injectDataIntoHtmlTemplate(template, data);
|
||||
|
||||
expect(result).toContain(
|
||||
'<script id="chat-data" type="application/json">',
|
||||
);
|
||||
expect(result).toContain('"sessionId": "test-session-id"');
|
||||
expect(result).toContain('"startTime": "2025-01-01T00:00:00Z"');
|
||||
expect(result).not.toContain('DATA_PLACEHOLDER');
|
||||
});
|
||||
|
||||
it('should handle template with whitespace around placeholder', () => {
|
||||
const template = `<script id="chat-data" type="application/json">\n// DATA_PLACEHOLDER: Your JSONL data will be injected here\n</script>`;
|
||||
|
||||
const data = {
|
||||
sessionId: 'test',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
messages: [] as ChatRecord[],
|
||||
};
|
||||
|
||||
const result = injectDataIntoHtmlTemplate(template, data);
|
||||
|
||||
expect(result).toContain('"sessionId": "test"');
|
||||
expect(result).not.toContain('DATA_PLACEHOLDER');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateExportFilename', () => {
|
||||
it('should generate filename with timestamp and extension', () => {
|
||||
const filename = generateExportFilename('md');
|
||||
|
||||
expect(filename).toMatch(
|
||||
/^export-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.md$/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should use provided extension', () => {
|
||||
const filename1 = generateExportFilename('html');
|
||||
const filename2 = generateExportFilename('json');
|
||||
|
||||
expect(filename1).toMatch(/\.html$/);
|
||||
expect(filename2).toMatch(/\.json$/);
|
||||
});
|
||||
|
||||
it('should replace colons and dots in timestamp', () => {
|
||||
const filename = generateExportFilename('md');
|
||||
|
||||
expect(filename).not.toContain(':');
|
||||
// The filename should contain a dot only for the extension
|
||||
expect(filename.split('.').length).toBe(2);
|
||||
// Check that timestamp part (before extension) doesn't contain dots
|
||||
const timestampPart = filename.split('.')[0];
|
||||
expect(timestampPart).not.toContain('.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,167 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Part, Content } from '@google/genai';
|
||||
import type { ChatRecord } from '@qwen-code/qwen-code-core';
|
||||
|
||||
const HTML_TEMPLATE_URL =
|
||||
'https://raw.githubusercontent.com/QwenLM/qwen-code/main/template_portable.html';
|
||||
|
||||
/**
|
||||
* Extracts text content from a Content object's parts.
|
||||
*/
|
||||
export function extractTextFromContent(content: Content | undefined): string {
|
||||
if (!content?.parts) return '';
|
||||
|
||||
const textParts: string[] = [];
|
||||
for (const part of content.parts as Part[]) {
|
||||
if ('text' in part) {
|
||||
const textPart = part as { text: string };
|
||||
textParts.push(textPart.text);
|
||||
} else if ('functionCall' in part) {
|
||||
const fnPart = part as { functionCall: { name: string; args: unknown } };
|
||||
textParts.push(
|
||||
`[Function Call: ${fnPart.functionCall.name}]\n${JSON.stringify(fnPart.functionCall.args, null, 2)}`,
|
||||
);
|
||||
} else if ('functionResponse' in part) {
|
||||
const fnResPart = part as {
|
||||
functionResponse: { name: string; response: unknown };
|
||||
};
|
||||
textParts.push(
|
||||
`[Function Response: ${fnResPart.functionResponse.name}]\n${JSON.stringify(fnResPart.functionResponse.response, null, 2)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return textParts.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms ChatRecord messages to markdown format.
|
||||
*/
|
||||
export function transformToMarkdown(
|
||||
messages: ChatRecord[],
|
||||
sessionId: string,
|
||||
startTime: string,
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
// Add header with metadata
|
||||
lines.push('# Chat Session Export\n');
|
||||
lines.push(`**Session ID**: ${sessionId}\n`);
|
||||
lines.push(`**Start Time**: ${startTime}\n`);
|
||||
lines.push(`**Exported**: ${new Date().toISOString()}\n`);
|
||||
lines.push('---\n');
|
||||
|
||||
// Process each message
|
||||
for (const record of messages) {
|
||||
if (record.type === 'user') {
|
||||
lines.push('## User\n');
|
||||
const text = extractTextFromContent(record.message);
|
||||
lines.push(`${text}\n`);
|
||||
} else if (record.type === 'assistant') {
|
||||
lines.push('## Assistant\n');
|
||||
const text = extractTextFromContent(record.message);
|
||||
lines.push(`${text}\n`);
|
||||
} else if (record.type === 'tool_result') {
|
||||
lines.push('## Tool Result\n');
|
||||
if (record.toolCallResult) {
|
||||
const resultDisplay = record.toolCallResult.resultDisplay;
|
||||
if (resultDisplay) {
|
||||
lines.push('```\n');
|
||||
lines.push(
|
||||
typeof resultDisplay === 'string'
|
||||
? resultDisplay
|
||||
: JSON.stringify(resultDisplay, null, 2),
|
||||
);
|
||||
lines.push('\n```\n');
|
||||
}
|
||||
}
|
||||
const text = extractTextFromContent(record.message);
|
||||
if (text) {
|
||||
lines.push(`${text}\n`);
|
||||
}
|
||||
} else if (record.type === 'system') {
|
||||
// Skip system messages or format them minimally
|
||||
if (record.subtype === 'chat_compression') {
|
||||
lines.push('_[Chat history compressed]_\n');
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('\n');
|
||||
}
|
||||
|
||||
return lines.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the HTML template from a remote URL via fetch.
|
||||
* Throws an error if the fetch fails.
|
||||
*/
|
||||
export async function loadHtmlTemplate(): Promise<string> {
|
||||
try {
|
||||
const response = await fetch(HTML_TEMPLATE_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch HTML template: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
const template = await response.text();
|
||||
return template;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to load HTML template from ${HTML_TEMPLATE_URL}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares export data from conversation.
|
||||
*/
|
||||
export function prepareExportData(conversation: {
|
||||
sessionId: string;
|
||||
startTime: string;
|
||||
messages: ChatRecord[];
|
||||
}): {
|
||||
sessionId: string;
|
||||
startTime: string;
|
||||
messages: ChatRecord[];
|
||||
} {
|
||||
return {
|
||||
sessionId: conversation.sessionId,
|
||||
startTime: conversation.startTime,
|
||||
messages: conversation.messages,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects JSON data into the HTML template.
|
||||
*/
|
||||
export function injectDataIntoHtmlTemplate(
|
||||
template: string,
|
||||
data: {
|
||||
sessionId: string;
|
||||
startTime: string;
|
||||
messages: ChatRecord[];
|
||||
},
|
||||
): string {
|
||||
const jsonData = JSON.stringify(data, null, 2);
|
||||
const html = template.replace(
|
||||
/<script id="chat-data" type="application\/json">\s*\/\/ DATA_PLACEHOLDER:.*?\s*<\/script>/s,
|
||||
`<script id="chat-data" type="application/json">\n${jsonData}\n </script>`,
|
||||
);
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a filename with timestamp for export files.
|
||||
*/
|
||||
export function generateExportFilename(extension: string): string {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
return `export-${timestamp}.${extension}`;
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
# Qwen Code Companion
|
||||
|
||||
Seamlessly integrate [Qwen Code](https://github.com/QwenLM/qwen-code) into Visual Studio Code with native IDE features and an intuitive interface. This extension bundles everything you need to get started immediately.
|
||||
[](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion)
|
||||
[](https://open-vsx.org/extension/qwenlm/qwen-code-vscode-ide-companion)
|
||||
[](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion)
|
||||
|
||||
Seamlessly integrate [Qwen Code](https://github.com/QwenLM/qwen-code) into Visual Studio Code with native IDE features and an intuitive chat interface. This extension bundles everything you need — no additional installation required.
|
||||
|
||||
## Demo
|
||||
|
||||
@@ -11,7 +16,7 @@ Seamlessly integrate [Qwen Code](https://github.com/QwenLM/qwen-code) into Visua
|
||||
|
||||
## Features
|
||||
|
||||
- **Native IDE experience**: Dedicated Qwen Code sidebar panel accessed via the Qwen icon
|
||||
- **Native IDE experience**: Dedicated Qwen Code Chat panel accessed via the Qwen icon in the editor title bar
|
||||
- **Native diffing**: Review, edit, and accept changes in VS Code's diff view
|
||||
- **Auto-accept edits mode**: Automatically apply Qwen's changes as they're made
|
||||
- **File management**: @-mention files or attach files and images using the system file picker
|
||||
@@ -20,73 +25,46 @@ Seamlessly integrate [Qwen Code](https://github.com/QwenLM/qwen-code) into Visua
|
||||
|
||||
## Requirements
|
||||
|
||||
- Visual Studio Code 1.85.0 or newer
|
||||
- Visual Studio Code 1.85.0 or newer (also works with Cursor, Windsurf, and other VS Code-based editors)
|
||||
|
||||
## Installation
|
||||
## Quick Start
|
||||
|
||||
1. Install from the VS Code Marketplace: https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion
|
||||
1. **Install** from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion) or [Open VSX Registry](https://open-vsx.org/extension/qwenlm/qwen-code-vscode-ide-companion)
|
||||
|
||||
2. Two ways to use
|
||||
- Chat panel: Click the Qwen icon in the Activity Bar, or run `Qwen Code: Open` from the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`).
|
||||
- Terminal session (classic): Run `Qwen Code: Run` to launch a session in the integrated terminal (bundled CLI).
|
||||
2. **Open the Chat panel** using one of these methods:
|
||||
- Click the **Qwen icon** in the top-right corner of the editor
|
||||
- Run `Qwen Code: Open` from the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`)
|
||||
|
||||
## Development and Debugging
|
||||
3. **Start chatting** — Ask Qwen to help with coding tasks, explain code, fix bugs, or write new features
|
||||
|
||||
To debug and develop this extension locally:
|
||||
## Commands
|
||||
|
||||
1. **Clone the repository**
|
||||
| Command | Description |
|
||||
| -------------------------------- | ------------------------------------------------------ |
|
||||
| `Qwen Code: Open` | Open the Qwen Code Chat panel |
|
||||
| `Qwen Code: Run` | Launch a classic terminal session with the bundled CLI |
|
||||
| `Qwen Code: Accept Current Diff` | Accept the currently displayed diff |
|
||||
| `Qwen Code: Close Diff Editor` | Close/reject the current diff |
|
||||
|
||||
```bash
|
||||
git clone https://github.com/QwenLM/qwen-code.git
|
||||
cd qwen-code
|
||||
```
|
||||
## Feedback & Issues
|
||||
|
||||
2. **Install dependencies**
|
||||
- 🐛 [Report bugs](https://github.com/QwenLM/qwen-code/issues/new?template=bug_report.yml&labels=bug,vscode-ide-companion)
|
||||
- 💡 [Request features](https://github.com/QwenLM/qwen-code/issues/new?template=feature_request.yml&labels=enhancement,vscode-ide-companion)
|
||||
- 📖 [Documentation](https://qwenlm.github.io/qwen-code-docs/)
|
||||
- 📋 [Changelog](https://github.com/QwenLM/qwen-code/releases)
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# or if using pnpm
|
||||
pnpm install
|
||||
```
|
||||
## Contributing
|
||||
|
||||
3. **Start debugging**
|
||||
We welcome contributions! See our [Contributing Guide](https://github.com/QwenLM/qwen-code/blob/main/CONTRIBUTING.md) for details on:
|
||||
|
||||
```bash
|
||||
code . # Open the project root in VS Code
|
||||
```
|
||||
- Open the `packages/vscode-ide-companion/src/extension.ts` file
|
||||
- Open Debug panel (`Ctrl+Shift+D` or `Cmd+Shift+D`)
|
||||
- Select **"Launch Companion VS Code Extension"** from the debug dropdown
|
||||
- Press `F5` to launch Extension Development Host
|
||||
|
||||
4. **Make changes and reload**
|
||||
- Edit the source code in the original VS Code window
|
||||
- To see your changes, reload the Extension Development Host window by:
|
||||
- Pressing `Ctrl+R` (Windows/Linux) or `Cmd+R` (macOS)
|
||||
- Or clicking the "Reload" button in the debug toolbar
|
||||
|
||||
5. **View logs and debug output**
|
||||
- Open the Debug Console in the original VS Code window to see extension logs
|
||||
- In the Extension Development Host window, open Developer Tools with `Help > Toggle Developer Tools` to see webview logs
|
||||
|
||||
## Build for Production
|
||||
|
||||
To build the extension for distribution:
|
||||
|
||||
```bash
|
||||
npm run compile
|
||||
# or
|
||||
pnpm run compile
|
||||
```
|
||||
|
||||
To package the extension as a VSIX file:
|
||||
|
||||
```bash
|
||||
npx vsce package
|
||||
# or
|
||||
pnpm vsce package
|
||||
```
|
||||
- Setting up the development environment
|
||||
- Building and debugging the extension locally
|
||||
- Submitting pull requests
|
||||
|
||||
## Terms of Service and Privacy Notice
|
||||
|
||||
By installing this extension, you agree to the [Terms of Service](https://github.com/QwenLM/qwen-code/blob/main/docs/tos-privacy.md).
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://github.com/QwenLM/qwen-code/blob/main/LICENSE)
|
||||
|
||||
@@ -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.7.0",
|
||||
"version": "0.7.1",
|
||||
"publisher": "qwenlm",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
@@ -314,34 +314,32 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
'cli.js',
|
||||
).fsPath;
|
||||
const execPath = process.execPath;
|
||||
const lowerExecPath = execPath.toLowerCase();
|
||||
const needsElectronRunAsNode =
|
||||
lowerExecPath.includes('code') ||
|
||||
lowerExecPath.includes('electron');
|
||||
|
||||
let qwenCmd: string;
|
||||
const terminalOptions: vscode.TerminalOptions = {
|
||||
name: `Qwen Code (${selectedFolder.name})`,
|
||||
cwd: selectedFolder.uri.fsPath,
|
||||
location,
|
||||
};
|
||||
|
||||
let qwenCmd: string;
|
||||
|
||||
if (isWindows) {
|
||||
// Use system Node via cmd.exe; avoid PowerShell parsing issues
|
||||
// On Windows, try multiple strategies to find a Node.js runtime:
|
||||
// 1. Check if VSCode ships a standalone node.exe alongside Code.exe
|
||||
// 2. Check VSCode's internal Node.js in resources directory
|
||||
// 3. Fall back to using Code.exe with ELECTRON_RUN_AS_NODE=1
|
||||
const quoteCmd = (s: string) => `"${s.replace(/"/g, '""')}"`;
|
||||
const cliQuoted = quoteCmd(cliEntry);
|
||||
// TODO: @yiliang114, temporarily run through node, and later hope to decouple from the local node
|
||||
qwenCmd = `node ${cliQuoted}`;
|
||||
terminalOptions.shellPath = process.env.ComSpec;
|
||||
} else {
|
||||
// macOS/Linux: All VSCode-like IDEs (VSCode, Cursor, Windsurf, etc.)
|
||||
// are Electron-based, so we always need ELECTRON_RUN_AS_NODE=1
|
||||
// to run Node.js scripts using the IDE's bundled runtime.
|
||||
const quotePosix = (s: string) => `"${s.replace(/"/g, '\\"')}"`;
|
||||
const baseCmd = `${quotePosix(execPath)} ${quotePosix(cliEntry)}`;
|
||||
if (needsElectronRunAsNode) {
|
||||
// macOS Electron helper needs ELECTRON_RUN_AS_NODE=1;
|
||||
qwenCmd = `ELECTRON_RUN_AS_NODE=1 ${baseCmd}`;
|
||||
} else {
|
||||
qwenCmd = baseCmd;
|
||||
}
|
||||
qwenCmd = `ELECTRON_RUN_AS_NODE=1 ${baseCmd}`;
|
||||
}
|
||||
|
||||
const terminal = vscode.window.createTerminal(terminalOptions);
|
||||
|
||||
Reference in New Issue
Block a user