mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-22 09:47:47 +00:00
feat(vscode-ide-companion): split module & notes in english
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { BaseMessageHandler } from './BaseMessageHandler.js';
|
||||
|
||||
/**
|
||||
* Auth message handler
|
||||
* Handles all authentication-related messages
|
||||
*/
|
||||
export class AuthMessageHandler extends BaseMessageHandler {
|
||||
private loginHandler: (() => Promise<void>) | null = null;
|
||||
|
||||
canHandle(messageType: string): boolean {
|
||||
return ['login'].includes(messageType);
|
||||
}
|
||||
|
||||
async handle(message: { type: string; data?: unknown }): Promise<void> {
|
||||
switch (message.type) {
|
||||
case 'login':
|
||||
await this.handleLogin();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
'[AuthMessageHandler] Unknown message type:',
|
||||
message.type,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set login handler
|
||||
*/
|
||||
setLoginHandler(handler: () => Promise<void>): void {
|
||||
this.loginHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle login request
|
||||
*/
|
||||
private async handleLogin(): Promise<void> {
|
||||
try {
|
||||
console.log('[AuthMessageHandler] Login requested');
|
||||
|
||||
if (this.loginHandler) {
|
||||
await this.loginHandler();
|
||||
} else {
|
||||
vscode.window.showInformationMessage(
|
||||
'Please wait while we connect to Qwen Code...',
|
||||
);
|
||||
|
||||
// Fallback: trigger WebViewProvider's forceReLogin
|
||||
await vscode.commands.executeCommand('qwenCode.login');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AuthMessageHandler] Login failed:', error);
|
||||
this.sendToWebView({
|
||||
type: 'loginError',
|
||||
data: { message: `Login failed: ${error}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { QwenAgentManager } from '../agents/qwenAgentManager.js';
|
||||
import type { ConversationStore } from '../storage/conversationStore.js';
|
||||
|
||||
/**
|
||||
* Base message handler interface
|
||||
* All sub-handlers should implement this interface
|
||||
*/
|
||||
export interface IMessageHandler {
|
||||
/**
|
||||
* Handle message
|
||||
* @param message - Message object
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
handle(message: { type: string; data?: unknown }): Promise<void>;
|
||||
|
||||
/**
|
||||
* Check if this handler can handle the message type
|
||||
* @param messageType - Message type
|
||||
* @returns boolean
|
||||
*/
|
||||
canHandle(messageType: string): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base message handler class
|
||||
* Provides common dependency injection and helper methods
|
||||
*/
|
||||
export abstract class BaseMessageHandler implements IMessageHandler {
|
||||
constructor(
|
||||
protected agentManager: QwenAgentManager,
|
||||
protected conversationStore: ConversationStore,
|
||||
protected currentConversationId: string | null,
|
||||
protected sendToWebView: (message: unknown) => void,
|
||||
) {}
|
||||
|
||||
abstract handle(message: { type: string; data?: unknown }): Promise<void>;
|
||||
abstract canHandle(messageType: string): boolean;
|
||||
|
||||
/**
|
||||
* Update current conversation ID
|
||||
*/
|
||||
setCurrentConversationId(id: string | null): void {
|
||||
this.currentConversationId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current conversation ID
|
||||
*/
|
||||
getCurrentConversationId(): string | null {
|
||||
return this.currentConversationId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { BaseMessageHandler } from './BaseMessageHandler.js';
|
||||
import { getFileName } from '../../utils/webviewUtils.js';
|
||||
|
||||
/**
|
||||
* Editor message handler
|
||||
* Handles all editor state-related messages
|
||||
*/
|
||||
export class EditorMessageHandler extends BaseMessageHandler {
|
||||
canHandle(messageType: string): boolean {
|
||||
return ['getActiveEditor', 'focusActiveEditor'].includes(messageType);
|
||||
}
|
||||
|
||||
async handle(message: { type: string; data?: unknown }): Promise<void> {
|
||||
switch (message.type) {
|
||||
case 'getActiveEditor':
|
||||
await this.handleGetActiveEditor();
|
||||
break;
|
||||
|
||||
case 'focusActiveEditor':
|
||||
await this.handleFocusActiveEditor();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
'[EditorMessageHandler] Unknown message type:',
|
||||
message.type,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current active editor info
|
||||
*/
|
||||
private async handleGetActiveEditor(): Promise<void> {
|
||||
try {
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
|
||||
if (activeEditor) {
|
||||
const filePath = activeEditor.document.uri.fsPath;
|
||||
const fileName = getFileName(filePath);
|
||||
|
||||
let selectionInfo = null;
|
||||
if (!activeEditor.selection.isEmpty) {
|
||||
const selection = activeEditor.selection;
|
||||
selectionInfo = {
|
||||
startLine: selection.start.line + 1,
|
||||
endLine: selection.end.line + 1,
|
||||
};
|
||||
}
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'activeEditorChanged',
|
||||
data: { fileName, filePath, selection: selectionInfo },
|
||||
});
|
||||
} else {
|
||||
this.sendToWebView({
|
||||
type: 'activeEditorChanged',
|
||||
data: { fileName: null, filePath: null, selection: null },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[EditorMessageHandler] Failed to get active editor:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus on active editor
|
||||
*/
|
||||
private async handleFocusActiveEditor(): Promise<void> {
|
||||
try {
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
|
||||
if (activeEditor) {
|
||||
await vscode.window.showTextDocument(activeEditor.document, {
|
||||
viewColumn: activeEditor.viewColumn,
|
||||
preserveFocus: false,
|
||||
});
|
||||
} else {
|
||||
// If no active editor, show file picker
|
||||
const uri = await vscode.window.showOpenDialog({
|
||||
defaultUri: vscode.workspace.workspaceFolders?.[0]?.uri,
|
||||
canSelectMany: false,
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
openLabel: 'Open',
|
||||
});
|
||||
|
||||
if (uri && uri.length > 0) {
|
||||
await vscode.window.showTextDocument(uri[0]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[EditorMessageHandler] Failed to focus active editor:',
|
||||
error,
|
||||
);
|
||||
vscode.window.showErrorMessage(`Failed to focus editor: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { BaseMessageHandler } from './BaseMessageHandler.js';
|
||||
import { getFileName } from '../../utils/webviewUtils.js';
|
||||
|
||||
/**
|
||||
* File message handler
|
||||
* Handles all file-related messages
|
||||
*/
|
||||
export class FileMessageHandler extends BaseMessageHandler {
|
||||
canHandle(messageType: string): boolean {
|
||||
return [
|
||||
'attachFile',
|
||||
'showContextPicker',
|
||||
'getWorkspaceFiles',
|
||||
'openFile',
|
||||
'openDiff',
|
||||
].includes(messageType);
|
||||
}
|
||||
|
||||
async handle(message: { type: string; data?: unknown }): Promise<void> {
|
||||
const data = message.data as Record<string, unknown> | undefined;
|
||||
|
||||
switch (message.type) {
|
||||
case 'attachFile':
|
||||
await this.handleAttachFile();
|
||||
break;
|
||||
|
||||
case 'showContextPicker':
|
||||
await this.handleShowContextPicker();
|
||||
break;
|
||||
|
||||
case 'getWorkspaceFiles':
|
||||
await this.handleGetWorkspaceFiles(data?.query as string | undefined);
|
||||
break;
|
||||
|
||||
case 'openFile':
|
||||
await this.handleOpenFile(data?.path as string | undefined);
|
||||
break;
|
||||
|
||||
case 'openDiff':
|
||||
await this.handleOpenDiff(data);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
'[FileMessageHandler] Unknown message type:',
|
||||
message.type,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle attach file request
|
||||
*/
|
||||
private async handleAttachFile(): Promise<void> {
|
||||
try {
|
||||
const uris = await vscode.window.showOpenDialog({
|
||||
canSelectMany: false,
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
openLabel: 'Attach',
|
||||
});
|
||||
|
||||
if (uris && uris.length > 0) {
|
||||
const uri = uris[0];
|
||||
const fileName = getFileName(uri.fsPath);
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'fileAttached',
|
||||
data: {
|
||||
id: `file-${Date.now()}`,
|
||||
type: 'file',
|
||||
name: fileName,
|
||||
value: uri.fsPath,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[FileMessageHandler] Failed to attach file:', error);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: `Failed to attach file: ${error}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle show context picker request
|
||||
*/
|
||||
private async handleShowContextPicker(): Promise<void> {
|
||||
try {
|
||||
const items: vscode.QuickPickItem[] = [];
|
||||
|
||||
// Add current file
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
if (activeEditor) {
|
||||
const fileName = getFileName(activeEditor.document.uri.fsPath);
|
||||
items.push({
|
||||
label: `$(file) ${fileName}`,
|
||||
description: 'Current file',
|
||||
detail: activeEditor.document.uri.fsPath,
|
||||
});
|
||||
}
|
||||
|
||||
// Add file picker option
|
||||
items.push({
|
||||
label: '$(file) File...',
|
||||
description: 'Choose a file to attach',
|
||||
});
|
||||
|
||||
// Add workspace files option
|
||||
items.push({
|
||||
label: '$(search) Search files...',
|
||||
description: 'Search workspace files',
|
||||
});
|
||||
|
||||
const selected = await vscode.window.showQuickPick(items, {
|
||||
placeHolder: 'Attach context',
|
||||
matchOnDescription: true,
|
||||
matchOnDetail: true,
|
||||
});
|
||||
|
||||
if (selected) {
|
||||
if (selected.label.includes('Current file') && activeEditor) {
|
||||
const fileName = getFileName(activeEditor.document.uri.fsPath);
|
||||
this.sendToWebView({
|
||||
type: 'fileAttached',
|
||||
data: {
|
||||
id: `file-${Date.now()}`,
|
||||
type: 'file',
|
||||
name: fileName,
|
||||
value: activeEditor.document.uri.fsPath,
|
||||
},
|
||||
});
|
||||
} else if (selected.label.includes('File...')) {
|
||||
await this.handleAttachFile();
|
||||
} else if (selected.label.includes('Search files')) {
|
||||
const uri = await vscode.window.showOpenDialog({
|
||||
defaultUri: vscode.workspace.workspaceFolders?.[0]?.uri,
|
||||
canSelectMany: false,
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
openLabel: 'Attach',
|
||||
});
|
||||
|
||||
if (uri && uri.length > 0) {
|
||||
const fileName = getFileName(uri[0].fsPath);
|
||||
this.sendToWebView({
|
||||
type: 'fileAttached',
|
||||
data: {
|
||||
id: `file-${Date.now()}`,
|
||||
type: 'file',
|
||||
name: fileName,
|
||||
value: uri[0].fsPath,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[FileMessageHandler] Failed to show context picker:',
|
||||
error,
|
||||
);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: `Failed to show context picker: ${error}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workspace files
|
||||
*/
|
||||
private async handleGetWorkspaceFiles(query?: string): Promise<void> {
|
||||
try {
|
||||
const files: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
path: string;
|
||||
}> = [];
|
||||
const addedPaths = new Set<string>();
|
||||
|
||||
const addFile = (uri: vscode.Uri, isCurrentFile = false) => {
|
||||
if (addedPaths.has(uri.fsPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = getFileName(uri.fsPath);
|
||||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
|
||||
const relativePath = workspaceFolder
|
||||
? vscode.workspace.asRelativePath(uri, false)
|
||||
: uri.fsPath;
|
||||
|
||||
// Filter by query if provided
|
||||
if (
|
||||
query &&
|
||||
!fileName.toLowerCase().includes(query.toLowerCase()) &&
|
||||
!relativePath.toLowerCase().includes(query.toLowerCase())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
files.push({
|
||||
id: isCurrentFile ? 'current-file' : uri.fsPath,
|
||||
label: fileName,
|
||||
description: relativePath,
|
||||
path: uri.fsPath,
|
||||
});
|
||||
addedPaths.add(uri.fsPath);
|
||||
};
|
||||
|
||||
// Search or show recent files
|
||||
if (query) {
|
||||
const uris = await vscode.workspace.findFiles(
|
||||
`**/*${query}*`,
|
||||
'**/node_modules/**',
|
||||
50,
|
||||
);
|
||||
|
||||
for (const uri of uris) {
|
||||
addFile(uri);
|
||||
}
|
||||
} else {
|
||||
// Add current active file first
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
if (activeEditor) {
|
||||
addFile(activeEditor.document.uri, true);
|
||||
}
|
||||
|
||||
// Add all open tabs
|
||||
const tabGroups = vscode.window.tabGroups.all;
|
||||
for (const tabGroup of tabGroups) {
|
||||
for (const tab of tabGroup.tabs) {
|
||||
const input = tab.input as { uri?: vscode.Uri } | undefined;
|
||||
if (input && input.uri instanceof vscode.Uri) {
|
||||
addFile(input.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not enough files, add some workspace files
|
||||
if (files.length < 10) {
|
||||
const recentUris = await vscode.workspace.findFiles(
|
||||
'**/*',
|
||||
'**/node_modules/**',
|
||||
20,
|
||||
);
|
||||
|
||||
for (const uri of recentUris) {
|
||||
if (files.length >= 20) {
|
||||
break;
|
||||
}
|
||||
addFile(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'workspaceFiles',
|
||||
data: { files },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[FileMessageHandler] Failed to get workspace files:',
|
||||
error,
|
||||
);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: `Failed to get workspace files: ${error}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open file
|
||||
*/
|
||||
private async handleOpenFile(path?: string): Promise<void> {
|
||||
if (!path) {
|
||||
console.warn('[FileMessageHandler] No path provided for openFile');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(path);
|
||||
await vscode.window.showTextDocument(uri, {
|
||||
preview: false,
|
||||
preserveFocus: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[FileMessageHandler] Failed to open file:', error);
|
||||
vscode.window.showErrorMessage(`Failed to open file: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open diff view
|
||||
*/
|
||||
private async handleOpenDiff(
|
||||
data: Record<string, unknown> | undefined,
|
||||
): Promise<void> {
|
||||
if (!data) {
|
||||
console.warn('[FileMessageHandler] No data provided for openDiff');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await vscode.commands.executeCommand('qwenCode.showDiff', {
|
||||
path: (data.path as string) || '',
|
||||
oldText: (data.oldText as string) || '',
|
||||
newText: (data.newText as string) || '',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[FileMessageHandler] Failed to open diff:', error);
|
||||
vscode.window.showErrorMessage(`Failed to open diff: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { IMessageHandler } from './BaseMessageHandler.js';
|
||||
import type { QwenAgentManager } from '../../agents/qwenAgentManager.js';
|
||||
import type { ConversationStore } from '../../storage/conversationStore.js';
|
||||
import { SessionMessageHandler } from './SessionMessageHandler.js';
|
||||
import { FileMessageHandler } from './FileMessageHandler.js';
|
||||
import { EditorMessageHandler } from './EditorMessageHandler.js';
|
||||
import { AuthMessageHandler } from './AuthMessageHandler.js';
|
||||
import { SettingsMessageHandler } from './SettingsMessageHandler.js';
|
||||
|
||||
/**
|
||||
* Message Router
|
||||
* Routes messages to appropriate handlers
|
||||
*/
|
||||
export class MessageRouter {
|
||||
private handlers: IMessageHandler[] = [];
|
||||
private sessionHandler: SessionMessageHandler;
|
||||
private authHandler: AuthMessageHandler;
|
||||
private currentConversationId: string | null = null;
|
||||
private permissionHandler:
|
||||
| ((message: { type: string; data: { optionId: string } }) => void)
|
||||
| null = null;
|
||||
|
||||
constructor(
|
||||
agentManager: QwenAgentManager,
|
||||
conversationStore: ConversationStore,
|
||||
currentConversationId: string | null,
|
||||
sendToWebView: (message: unknown) => void,
|
||||
) {
|
||||
this.currentConversationId = currentConversationId;
|
||||
|
||||
// Initialize all handlers
|
||||
this.sessionHandler = new SessionMessageHandler(
|
||||
agentManager,
|
||||
conversationStore,
|
||||
currentConversationId,
|
||||
sendToWebView,
|
||||
);
|
||||
|
||||
const fileHandler = new FileMessageHandler(
|
||||
agentManager,
|
||||
conversationStore,
|
||||
currentConversationId,
|
||||
sendToWebView,
|
||||
);
|
||||
|
||||
const editorHandler = new EditorMessageHandler(
|
||||
agentManager,
|
||||
conversationStore,
|
||||
currentConversationId,
|
||||
sendToWebView,
|
||||
);
|
||||
|
||||
this.authHandler = new AuthMessageHandler(
|
||||
agentManager,
|
||||
conversationStore,
|
||||
currentConversationId,
|
||||
sendToWebView,
|
||||
);
|
||||
|
||||
const settingsHandler = new SettingsMessageHandler(
|
||||
agentManager,
|
||||
conversationStore,
|
||||
currentConversationId,
|
||||
sendToWebView,
|
||||
);
|
||||
|
||||
// Register handlers in order of priority
|
||||
this.handlers = [
|
||||
this.sessionHandler,
|
||||
fileHandler,
|
||||
editorHandler,
|
||||
this.authHandler,
|
||||
settingsHandler,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Route message to appropriate handler
|
||||
*/
|
||||
async route(message: { type: string; data?: unknown }): Promise<void> {
|
||||
console.log('[MessageRouter] Routing message:', message.type);
|
||||
|
||||
// Handle permission response specially
|
||||
if (message.type === 'permissionResponse') {
|
||||
if (this.permissionHandler) {
|
||||
this.permissionHandler(
|
||||
message as { type: string; data: { optionId: string } },
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Find appropriate handler
|
||||
const handler = this.handlers.find((h) => h.canHandle(message.type));
|
||||
|
||||
if (handler) {
|
||||
try {
|
||||
await handler.handle(message);
|
||||
} catch (error) {
|
||||
console.error('[MessageRouter] Handler error:', error);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
'[MessageRouter] No handler found for message type:',
|
||||
message.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current conversation ID
|
||||
*/
|
||||
setCurrentConversationId(id: string | null): void {
|
||||
this.currentConversationId = id;
|
||||
// Update all handlers
|
||||
this.handlers.forEach((handler) => {
|
||||
if ('setCurrentConversationId' in handler) {
|
||||
(
|
||||
handler as { setCurrentConversationId: (id: string | null) => void }
|
||||
).setCurrentConversationId(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current conversation ID
|
||||
*/
|
||||
getCurrentConversationId(): string | null {
|
||||
return this.currentConversationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set permission handler
|
||||
*/
|
||||
setPermissionHandler(
|
||||
handler: (message: { type: string; data: { optionId: string } }) => void,
|
||||
): void {
|
||||
this.permissionHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set login handler
|
||||
*/
|
||||
setLoginHandler(handler: () => Promise<void>): void {
|
||||
this.authHandler.setLoginHandler(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append stream content
|
||||
*/
|
||||
appendStreamContent(chunk: string): void {
|
||||
this.sessionHandler.appendStreamContent(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if saving checkpoint
|
||||
*/
|
||||
getIsSavingCheckpoint(): boolean {
|
||||
return this.sessionHandler.getIsSavingCheckpoint();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { BaseMessageHandler } from './BaseMessageHandler.js';
|
||||
import type { ChatMessage } from '../../agents/qwenAgentManager.js';
|
||||
|
||||
/**
|
||||
* Session message handler
|
||||
* Handles all session-related messages
|
||||
*/
|
||||
export class SessionMessageHandler extends BaseMessageHandler {
|
||||
private currentStreamContent = '';
|
||||
private isSavingCheckpoint = false;
|
||||
|
||||
canHandle(messageType: string): boolean {
|
||||
return [
|
||||
'sendMessage',
|
||||
'newQwenSession',
|
||||
'switchQwenSession',
|
||||
'getQwenSessions',
|
||||
'saveSession',
|
||||
'resumeSession',
|
||||
].includes(messageType);
|
||||
}
|
||||
|
||||
async handle(message: { type: string; data?: unknown }): Promise<void> {
|
||||
const data = message.data as Record<string, unknown> | undefined;
|
||||
|
||||
switch (message.type) {
|
||||
case 'sendMessage':
|
||||
await this.handleSendMessage(
|
||||
(data?.text as string) || '',
|
||||
data?.context as
|
||||
| Array<{
|
||||
type: string;
|
||||
name: string;
|
||||
value: string;
|
||||
startLine?: number;
|
||||
endLine?: number;
|
||||
}>
|
||||
| undefined,
|
||||
data?.fileContext as
|
||||
| {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
startLine?: number;
|
||||
endLine?: number;
|
||||
}
|
||||
| undefined,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'newQwenSession':
|
||||
await this.handleNewQwenSession();
|
||||
break;
|
||||
|
||||
case 'switchQwenSession':
|
||||
await this.handleSwitchQwenSession((data?.sessionId as string) || '');
|
||||
break;
|
||||
|
||||
case 'getQwenSessions':
|
||||
await this.handleGetQwenSessions();
|
||||
break;
|
||||
|
||||
case 'saveSession':
|
||||
await this.handleSaveSession((data?.tag as string) || '');
|
||||
break;
|
||||
|
||||
case 'resumeSession':
|
||||
await this.handleResumeSession((data?.sessionId as string) || '');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
'[SessionMessageHandler] Unknown message type:',
|
||||
message.type,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current stream content
|
||||
*/
|
||||
getCurrentStreamContent(): string {
|
||||
return this.currentStreamContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append stream content
|
||||
*/
|
||||
appendStreamContent(chunk: string): void {
|
||||
this.currentStreamContent += chunk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset stream content
|
||||
*/
|
||||
resetStreamContent(): void {
|
||||
this.currentStreamContent = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if saving checkpoint
|
||||
*/
|
||||
getIsSavingCheckpoint(): boolean {
|
||||
return this.isSavingCheckpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle send message request
|
||||
*/
|
||||
private async handleSendMessage(
|
||||
text: string,
|
||||
context?: Array<{
|
||||
type: string;
|
||||
name: string;
|
||||
value: string;
|
||||
startLine?: number;
|
||||
endLine?: number;
|
||||
}>,
|
||||
fileContext?: {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
startLine?: number;
|
||||
endLine?: number;
|
||||
},
|
||||
): Promise<void> {
|
||||
console.log('[SessionMessageHandler] handleSendMessage called with:', text);
|
||||
|
||||
// Format message with file context if present
|
||||
let formattedText = text;
|
||||
if (context && context.length > 0) {
|
||||
const contextParts = context
|
||||
.map((ctx) => {
|
||||
if (ctx.startLine && ctx.endLine) {
|
||||
return `${ctx.value}#${ctx.startLine}${ctx.startLine !== ctx.endLine ? `-${ctx.endLine}` : ''}`;
|
||||
}
|
||||
return ctx.value;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
formattedText = `${contextParts}\n\n${text}`;
|
||||
}
|
||||
|
||||
// Ensure we have an active conversation
|
||||
if (!this.currentConversationId) {
|
||||
console.log(
|
||||
'[SessionMessageHandler] No active conversation, creating one...',
|
||||
);
|
||||
try {
|
||||
const newConv = await this.conversationStore.createConversation();
|
||||
this.currentConversationId = newConv.id;
|
||||
this.sendToWebView({
|
||||
type: 'conversationLoaded',
|
||||
data: newConv,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMsg = `Failed to create conversation: ${error}`;
|
||||
console.error('[SessionMessageHandler]', errorMsg);
|
||||
vscode.window.showErrorMessage(errorMsg);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: errorMsg },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.currentConversationId) {
|
||||
const errorMsg =
|
||||
'Failed to create conversation. Please restart the extension.';
|
||||
console.error('[SessionMessageHandler]', errorMsg);
|
||||
vscode.window.showErrorMessage(errorMsg);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: errorMsg },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is the first message
|
||||
let isFirstMessage = false;
|
||||
try {
|
||||
const conversation = await this.conversationStore.getConversation(
|
||||
this.currentConversationId,
|
||||
);
|
||||
isFirstMessage = !conversation || conversation.messages.length === 0;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[SessionMessageHandler] Failed to check conversation:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
// Generate title for first message
|
||||
if (isFirstMessage) {
|
||||
const title = text.substring(0, 50) + (text.length > 50 ? '...' : '');
|
||||
this.sendToWebView({
|
||||
type: 'sessionTitleUpdated',
|
||||
data: {
|
||||
sessionId: this.currentConversationId,
|
||||
title,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Save user message
|
||||
const userMessage: ChatMessage = {
|
||||
role: 'user',
|
||||
content: text,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
await this.conversationStore.addMessage(
|
||||
this.currentConversationId,
|
||||
userMessage,
|
||||
);
|
||||
|
||||
// Send to WebView
|
||||
this.sendToWebView({
|
||||
type: 'message',
|
||||
data: { ...userMessage, fileContext },
|
||||
});
|
||||
|
||||
// Check if agent is connected
|
||||
if (!this.agentManager.isConnected) {
|
||||
console.warn('[SessionMessageHandler] Agent not connected');
|
||||
|
||||
const result = await vscode.window.showWarningMessage(
|
||||
'You need to login first to use Qwen Code.',
|
||||
'Login Now',
|
||||
);
|
||||
|
||||
if (result === 'Login Now') {
|
||||
vscode.commands.executeCommand('qwenCode.login');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Send to agent
|
||||
try {
|
||||
this.resetStreamContent();
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'streamStart',
|
||||
data: { timestamp: Date.now() },
|
||||
});
|
||||
|
||||
await this.agentManager.sendMessage(formattedText);
|
||||
|
||||
// Save assistant message
|
||||
if (this.currentStreamContent && this.currentConversationId) {
|
||||
const assistantMessage: ChatMessage = {
|
||||
role: 'assistant',
|
||||
content: this.currentStreamContent,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
await this.conversationStore.addMessage(
|
||||
this.currentConversationId,
|
||||
assistantMessage,
|
||||
);
|
||||
}
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'streamEnd',
|
||||
data: { timestamp: Date.now() },
|
||||
});
|
||||
|
||||
// Auto-save checkpoint
|
||||
if (this.currentConversationId) {
|
||||
try {
|
||||
const conversation = await this.conversationStore.getConversation(
|
||||
this.currentConversationId,
|
||||
);
|
||||
|
||||
const messages = conversation?.messages || [];
|
||||
|
||||
this.isSavingCheckpoint = true;
|
||||
|
||||
const result = await this.agentManager.saveCheckpoint(
|
||||
messages,
|
||||
this.currentConversationId,
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
this.isSavingCheckpoint = false;
|
||||
}, 2000);
|
||||
|
||||
if (result.success) {
|
||||
console.log(
|
||||
'[SessionMessageHandler] Checkpoint saved:',
|
||||
result.tag,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[SessionMessageHandler] Checkpoint save failed:',
|
||||
error,
|
||||
);
|
||||
this.isSavingCheckpoint = false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SessionMessageHandler] Error sending message:', error);
|
||||
|
||||
const errorMsg = String(error);
|
||||
if (errorMsg.includes('No active ACP session')) {
|
||||
const result = await vscode.window.showWarningMessage(
|
||||
'You need to login first to use Qwen Code.',
|
||||
'Login Now',
|
||||
);
|
||||
|
||||
if (result === 'Login Now') {
|
||||
vscode.commands.executeCommand('qwenCode.login');
|
||||
}
|
||||
} else {
|
||||
vscode.window.showErrorMessage(`Error sending message: ${error}`);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: errorMsg },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle new Qwen session request
|
||||
*/
|
||||
private async handleNewQwenSession(): Promise<void> {
|
||||
try {
|
||||
console.log('[SessionMessageHandler] Creating new Qwen session...');
|
||||
|
||||
// Save current session before creating new one
|
||||
if (this.currentConversationId && this.agentManager.isConnected) {
|
||||
try {
|
||||
const conversation = await this.conversationStore.getConversation(
|
||||
this.currentConversationId,
|
||||
);
|
||||
const messages = conversation?.messages || [];
|
||||
|
||||
await this.agentManager.saveCheckpoint(
|
||||
messages,
|
||||
this.currentConversationId,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[SessionMessageHandler] Failed to auto-save:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||
|
||||
await this.agentManager.createNewSession(workingDir);
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'conversationCleared',
|
||||
data: {},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[SessionMessageHandler] Failed to create new session:',
|
||||
error,
|
||||
);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: `Failed to create new session: ${error}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle switch Qwen session request
|
||||
*/
|
||||
private async handleSwitchQwenSession(sessionId: string): Promise<void> {
|
||||
try {
|
||||
console.log('[SessionMessageHandler] Switching to session:', sessionId);
|
||||
|
||||
// Save current session before switching
|
||||
if (
|
||||
this.currentConversationId &&
|
||||
this.currentConversationId !== sessionId &&
|
||||
this.agentManager.isConnected
|
||||
) {
|
||||
try {
|
||||
const conversation = await this.conversationStore.getConversation(
|
||||
this.currentConversationId,
|
||||
);
|
||||
const messages = conversation?.messages || [];
|
||||
|
||||
await this.agentManager.saveCheckpoint(
|
||||
messages,
|
||||
this.currentConversationId,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[SessionMessageHandler] Failed to auto-save:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get session details
|
||||
let sessionDetails = null;
|
||||
try {
|
||||
const allSessions = await this.agentManager.getSessionList();
|
||||
sessionDetails = allSessions.find(
|
||||
(s: { id?: string; sessionId?: string }) =>
|
||||
s.id === sessionId || s.sessionId === sessionId,
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'[SessionMessageHandler] Could not get session details:',
|
||||
err,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||
|
||||
// Try to load session via ACP
|
||||
try {
|
||||
const loadResponse =
|
||||
await this.agentManager.loadSessionViaAcp(sessionId);
|
||||
console.log(
|
||||
'[SessionMessageHandler] session/load succeeded:',
|
||||
loadResponse,
|
||||
);
|
||||
|
||||
this.currentConversationId = sessionId;
|
||||
|
||||
const messages = await this.agentManager.getSessionMessages(sessionId);
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'qwenSessionSwitched',
|
||||
data: { sessionId, messages, session: sessionDetails },
|
||||
});
|
||||
} catch (_loadError) {
|
||||
console.warn(
|
||||
'[SessionMessageHandler] session/load failed, using fallback',
|
||||
);
|
||||
|
||||
// Fallback: create new session
|
||||
const messages = await this.agentManager.getSessionMessages(sessionId);
|
||||
|
||||
try {
|
||||
const newAcpSessionId =
|
||||
await this.agentManager.createNewSession(workingDir);
|
||||
|
||||
this.currentConversationId = newAcpSessionId;
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'qwenSessionSwitched',
|
||||
data: { sessionId, messages, session: sessionDetails },
|
||||
});
|
||||
|
||||
vscode.window.showWarningMessage(
|
||||
'Session restored from local cache. Some context may be incomplete.',
|
||||
);
|
||||
} catch (createError) {
|
||||
console.error(
|
||||
'[SessionMessageHandler] Failed to create session:',
|
||||
createError,
|
||||
);
|
||||
throw createError;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SessionMessageHandler] Failed to switch session:', error);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: `Failed to switch session: ${error}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle get Qwen sessions request
|
||||
*/
|
||||
private async handleGetQwenSessions(): Promise<void> {
|
||||
try {
|
||||
const sessions = await this.agentManager.getSessionList();
|
||||
this.sendToWebView({
|
||||
type: 'qwenSessionList',
|
||||
data: { sessions },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[SessionMessageHandler] Failed to get sessions:', error);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: `Failed to get sessions: ${error}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle save session request
|
||||
*/
|
||||
private async handleSaveSession(tag: string): Promise<void> {
|
||||
try {
|
||||
if (!this.currentConversationId) {
|
||||
throw new Error('No active conversation to save');
|
||||
}
|
||||
|
||||
const conversation = await this.conversationStore.getConversation(
|
||||
this.currentConversationId,
|
||||
);
|
||||
const messages = conversation?.messages || [];
|
||||
|
||||
// Try ACP save first
|
||||
try {
|
||||
const response = await this.agentManager.saveSessionViaAcp(
|
||||
this.currentConversationId,
|
||||
tag,
|
||||
);
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'saveSessionResponse',
|
||||
data: response,
|
||||
});
|
||||
} catch (_acpError) {
|
||||
// Fallback to direct save
|
||||
const response = await this.agentManager.saveSessionDirect(
|
||||
messages,
|
||||
tag,
|
||||
);
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'saveSessionResponse',
|
||||
data: response,
|
||||
});
|
||||
}
|
||||
|
||||
await this.handleGetQwenSessions();
|
||||
} catch (error) {
|
||||
console.error('[SessionMessageHandler] Failed to save session:', error);
|
||||
this.sendToWebView({
|
||||
type: 'saveSessionResponse',
|
||||
data: {
|
||||
success: false,
|
||||
message: `Failed to save session: ${error}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle resume session request
|
||||
*/
|
||||
private async handleResumeSession(sessionId: string): Promise<void> {
|
||||
try {
|
||||
// Try ACP load first
|
||||
try {
|
||||
await this.agentManager.loadSessionViaAcp(sessionId);
|
||||
|
||||
this.currentConversationId = sessionId;
|
||||
|
||||
const messages = await this.agentManager.getSessionMessages(sessionId);
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'qwenSessionSwitched',
|
||||
data: { sessionId, messages },
|
||||
});
|
||||
} catch (_acpError) {
|
||||
// Fallback to direct load
|
||||
const messages = await this.agentManager.loadSessionDirect(sessionId);
|
||||
|
||||
if (messages) {
|
||||
this.currentConversationId = sessionId;
|
||||
|
||||
this.sendToWebView({
|
||||
type: 'qwenSessionSwitched',
|
||||
data: { sessionId, messages },
|
||||
});
|
||||
} else {
|
||||
throw new Error('Failed to load session');
|
||||
}
|
||||
}
|
||||
|
||||
await this.handleGetQwenSessions();
|
||||
} catch (error) {
|
||||
console.error('[SessionMessageHandler] Failed to resume session:', error);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: `Failed to resume session: ${error}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { BaseMessageHandler } from './BaseMessageHandler.js';
|
||||
|
||||
/**
|
||||
* Settings message handler
|
||||
* Handles all settings-related messages
|
||||
*/
|
||||
export class SettingsMessageHandler extends BaseMessageHandler {
|
||||
canHandle(messageType: string): boolean {
|
||||
return ['openSettings', 'recheckCli'].includes(messageType);
|
||||
}
|
||||
|
||||
async handle(message: { type: string; data?: unknown }): Promise<void> {
|
||||
switch (message.type) {
|
||||
case 'openSettings':
|
||||
await this.handleOpenSettings();
|
||||
break;
|
||||
|
||||
case 'recheckCli':
|
||||
await this.handleRecheckCli();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
'[SettingsMessageHandler] Unknown message type:',
|
||||
message.type,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open settings page
|
||||
*/
|
||||
private async handleOpenSettings(): Promise<void> {
|
||||
try {
|
||||
await vscode.commands.executeCommand(
|
||||
'workbench.action.openSettings',
|
||||
'qwenCode',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[SettingsMessageHandler] Failed to open settings:', error);
|
||||
vscode.window.showErrorMessage(`Failed to open settings: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recheck CLI
|
||||
*/
|
||||
private async handleRecheckCli(): Promise<void> {
|
||||
try {
|
||||
await vscode.commands.executeCommand('qwenCode.recheckCli');
|
||||
this.sendToWebView({
|
||||
type: 'cliRechecked',
|
||||
data: { success: true },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[SettingsMessageHandler] Failed to recheck CLI:', error);
|
||||
this.sendToWebView({
|
||||
type: 'error',
|
||||
data: { message: `Failed to recheck CLI: ${error}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user