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