feat(vscode-ide-companion): split module & notes in english

This commit is contained in:
yiliang114
2025-11-25 00:32:51 +08:00
parent 3cf22c065f
commit f503eb2520
42 changed files with 4189 additions and 3063 deletions

View File

@@ -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}` },
});
}
}
}

View File

@@ -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;
}
}

View File

@@ -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}`);
}
}
}

View File

@@ -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}`);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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}` },
});
}
}
}

View File

@@ -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}` },
});
}
}
}