mirror of
https://github.com/wassname/HumanAgent-MCP.git
synced 2026-09-10 11:40:39 +08:00
Fix session ID mapping: VS Code conversation IDs now map to registered sessions
- Added conversationToSession mapping to link VS Code conversation IDs to registered session IDs - Updated handleToolCall to extract session ID from _meta.vscode.conversationId - Auto-maps VS Code conversations to first available registered session - Both VS Code and web interface now use same registered session ID - Fixes 'No pending AI request found' error in web interface
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import * as path from 'path';
|
||||
|
||||
// Use dynamic import to avoid issues with the module
|
||||
let wavPlayer: any;
|
||||
// Use direct import like CodeChampion
|
||||
const wavPlayer = require('node-wav-player');
|
||||
|
||||
export class AudioNotification {
|
||||
private static isInitialized = false;
|
||||
@@ -9,7 +9,7 @@ export class AudioNotification {
|
||||
static async initialize() {
|
||||
if (!this.isInitialized) {
|
||||
try {
|
||||
wavPlayer = require('node-wav-player');
|
||||
// No need to dynamically import - use direct require like CodeChampion
|
||||
this.isInitialized = true;
|
||||
console.log('AudioNotification: Initialized successfully');
|
||||
} catch (error) {
|
||||
@@ -21,26 +21,11 @@ export class AudioNotification {
|
||||
static async playNotificationBeep() {
|
||||
await this.initialize();
|
||||
|
||||
if (!wavPlayer) {
|
||||
console.log('AudioNotification: node-wav-player not available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use a simple base64 encoded beep sound or generate one programmatically
|
||||
await this.generateAndPlayBeep();
|
||||
} catch (error) {
|
||||
console.error('AudioNotification: Error playing sound:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private static async generateAndPlayBeep() {
|
||||
// For now, we'll create a simple beep using system bell if available
|
||||
// or we can embed a small sound file
|
||||
try {
|
||||
// Try to create a simple WAV file programmatically
|
||||
// Use CodeChampion's simple approach - create a temp beep file and play it
|
||||
const tempSoundPath = await this.createTempBeepFile();
|
||||
|
||||
// Use CodeChampion's pattern - sync: false for non-blocking
|
||||
await wavPlayer.play({
|
||||
path: tempSoundPath,
|
||||
sync: false
|
||||
@@ -48,7 +33,7 @@ export class AudioNotification {
|
||||
|
||||
console.log('AudioNotification: Beep played successfully');
|
||||
} catch (error) {
|
||||
console.error('AudioNotification: Failed to play beep:', error);
|
||||
console.error('AudioNotification: Error playing sound:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
serverPath: serverPath,
|
||||
port: 3737,
|
||||
host: '127.0.0.1',
|
||||
logFile: path.join(context.extensionPath, '.vscode', 'HumanAgent-server.log')
|
||||
logFile: path.join(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || context.extensionPath, '.vscode', 'HumanAgent-server.log')
|
||||
});
|
||||
|
||||
// Auto-detect and start standalone MCP server if already configured
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import { ChatMessage } from './types';
|
||||
|
||||
interface SessionState {
|
||||
sessionId: string;
|
||||
messages: ChatMessage[];
|
||||
pendingRequests: Map<string, any>; // requestId -> request data
|
||||
lastActivity: number;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export class ChatManager {
|
||||
private sessions: Map<string, SessionState> = new Map();
|
||||
private readonly maxMessagesPerSession: number = 50;
|
||||
private readonly sessionTimeoutMs: number = 24 * 60 * 60 * 1000; // 24 hours
|
||||
private logger?: any; // DebugLogger instance
|
||||
|
||||
constructor(logger?: any) {
|
||||
this.logger = logger;
|
||||
this.log('INFO', 'ChatManager initialized with max ' + this.maxMessagesPerSession + ' messages per session');
|
||||
|
||||
// Clean up inactive sessions periodically
|
||||
setInterval(() => this.cleanupInactiveSessions(), 60 * 60 * 1000); // Every hour
|
||||
}
|
||||
|
||||
private log(level: string, message: string, data?: any): void {
|
||||
if (this.logger) {
|
||||
this.logger.log('CHAT', `[${level}] ${message}`, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a session
|
||||
*/
|
||||
getSession(sessionId: string): SessionState {
|
||||
if (!this.sessions.has(sessionId)) {
|
||||
this.sessions.set(sessionId, {
|
||||
sessionId,
|
||||
messages: [],
|
||||
pendingRequests: new Map(),
|
||||
lastActivity: Date.now(),
|
||||
isActive: true
|
||||
});
|
||||
}
|
||||
|
||||
const session = this.sessions.get(sessionId)!;
|
||||
session.lastActivity = Date.now();
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message to session history
|
||||
*/
|
||||
addMessage(sessionId: string, message: ChatMessage): void {
|
||||
const session = this.getSession(sessionId);
|
||||
|
||||
// Add message to history
|
||||
session.messages.push(message);
|
||||
this.log('INFO', `Added message to session ${sessionId}: ${message.sender} - ${message.content.substring(0, 50)}...`);
|
||||
|
||||
// Enforce message limit - remove oldest messages if exceeded
|
||||
if (session.messages.length > this.maxMessagesPerSession) {
|
||||
const toRemove = session.messages.length - this.maxMessagesPerSession;
|
||||
session.messages.splice(0, toRemove);
|
||||
this.log('INFO', `Cleaned up ${toRemove} old messages from session ${sessionId}, now has ${session.messages.length} messages`);
|
||||
}
|
||||
|
||||
session.lastActivity = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all messages for a session
|
||||
*/
|
||||
getMessages(sessionId: string): ChatMessage[] {
|
||||
const session = this.getSession(sessionId);
|
||||
return [...session.messages]; // Return copy to prevent external modification
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a pending human agent request
|
||||
*/
|
||||
addPendingRequest(sessionId: string, requestId: string, requestData: any): void {
|
||||
const session = this.getSession(sessionId);
|
||||
session.pendingRequests.set(requestId, requestData);
|
||||
session.lastActivity = Date.now();
|
||||
this.log('INFO', `Added pending request ${requestId} to session ${sessionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a pending request (when responded to)
|
||||
*/
|
||||
removePendingRequest(sessionId: string, requestId: string): boolean {
|
||||
const session = this.getSession(sessionId);
|
||||
const removed = session.pendingRequests.delete(requestId);
|
||||
if (removed) {
|
||||
session.lastActivity = Date.now();
|
||||
this.log('INFO', `Removed pending request ${requestId} from session ${sessionId}`);
|
||||
} else {
|
||||
this.log('WARN', `Attempted to remove non-existent pending request ${requestId} from session ${sessionId}`);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pending requests for a session
|
||||
*/
|
||||
getPendingRequests(sessionId: string): Map<string, any> {
|
||||
const session = this.getSession(sessionId);
|
||||
return new Map(session.pendingRequests); // Return copy
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a session has any pending requests
|
||||
*/
|
||||
hasPendingRequests(sessionId: string): boolean {
|
||||
const session = this.getSession(sessionId);
|
||||
return session.pendingRequests.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recent pending request for a session
|
||||
*/
|
||||
getLatestPendingRequest(sessionId: string): { requestId: string; data: any } | null {
|
||||
const session = this.getSession(sessionId);
|
||||
if (session.pendingRequests.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the most recent pending request (last one added)
|
||||
const entries = Array.from(session.pendingRequests.entries());
|
||||
const [requestId, data] = entries[entries.length - 1];
|
||||
return { requestId, data };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session state summary
|
||||
*/
|
||||
getSessionState(sessionId: string): {
|
||||
sessionId: string;
|
||||
messageCount: number;
|
||||
pendingRequestCount: number;
|
||||
lastActivity: number;
|
||||
isActive: boolean;
|
||||
latestPendingRequest?: { requestId: string; data: any };
|
||||
} {
|
||||
const session = this.getSession(sessionId);
|
||||
const result: any = {
|
||||
sessionId: session.sessionId,
|
||||
messageCount: session.messages.length,
|
||||
pendingRequestCount: session.pendingRequests.size,
|
||||
lastActivity: session.lastActivity,
|
||||
isActive: session.isActive
|
||||
};
|
||||
|
||||
const latestRequest = this.getLatestPendingRequest(sessionId);
|
||||
if (latestRequest) {
|
||||
result.latestPendingRequest = latestRequest;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active sessions
|
||||
*/
|
||||
getActiveSessions(): string[] {
|
||||
return Array.from(this.sessions.keys()).filter(sessionId => {
|
||||
const session = this.sessions.get(sessionId)!;
|
||||
return session.isActive;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate a session
|
||||
*/
|
||||
deactivateSession(sessionId: string): void {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.isActive = false;
|
||||
session.lastActivity = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old inactive sessions
|
||||
*/
|
||||
private cleanupInactiveSessions(): void {
|
||||
const now = Date.now();
|
||||
const sessionsToDelete: string[] = [];
|
||||
|
||||
for (const [sessionId, session] of this.sessions) {
|
||||
if (!session.isActive && (now - session.lastActivity) > this.sessionTimeoutMs) {
|
||||
sessionsToDelete.push(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const sessionId of sessionsToDelete) {
|
||||
this.sessions.delete(sessionId);
|
||||
this.log('INFO', `Cleaned up expired session: ${sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory usage statistics
|
||||
*/
|
||||
getMemoryStats(): {
|
||||
totalSessions: number;
|
||||
activeSessions: number;
|
||||
totalMessages: number;
|
||||
totalPendingRequests: number;
|
||||
} {
|
||||
let totalMessages = 0;
|
||||
let totalPendingRequests = 0;
|
||||
let activeSessions = 0;
|
||||
|
||||
for (const session of this.sessions.values()) {
|
||||
totalMessages += session.messages.length;
|
||||
totalPendingRequests += session.pendingRequests.size;
|
||||
if (session.isActive) {
|
||||
activeSessions++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalSessions: this.sessions.size,
|
||||
activeSessions,
|
||||
totalMessages,
|
||||
totalPendingRequests
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,10 @@ class StandaloneMcpServer {
|
||||
private server: McpServer;
|
||||
|
||||
constructor() {
|
||||
this.server = new McpServer();
|
||||
// Use the parent directory of the dist folder as workspace path
|
||||
// This ensures log file is created where it can be properly cleared
|
||||
const workspacePath = require('path').resolve(__dirname, '..');
|
||||
this.server = new McpServer(undefined, workspacePath);
|
||||
this.setupProcessHandlers();
|
||||
}
|
||||
|
||||
@@ -82,10 +85,11 @@ class StandaloneMcpServer {
|
||||
process.stdout.write(JSON.stringify(notification) + '\n');
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => this.shutdown());
|
||||
process.on('SIGTERM', () => this.shutdown());
|
||||
process.on('exit', () => this.shutdown());
|
||||
// Graceful shutdown handlers removed - server should remain independent
|
||||
// and only shut down when explicitly requested via API endpoints
|
||||
// process.on('SIGINT', () => this.shutdown());
|
||||
// process.on('SIGTERM', () => this.shutdown());
|
||||
// process.on('exit', () => this.shutdown());
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
|
||||
+809
-24
@@ -4,6 +4,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { McpMessage, McpServerConfig, HumanAgentSession, ChatMessage, McpTool, HumanAgentChatToolParams, HumanAgentChatToolResult } from './types';
|
||||
import { ChatManager } from './chatManager';
|
||||
|
||||
// File logging utility
|
||||
class DebugLogger {
|
||||
@@ -16,7 +17,7 @@ class DebugLogger {
|
||||
// Determine log path based on workspace or fallback to temp directory
|
||||
if (workspaceRoot) {
|
||||
const vscodeDir = path.join(workspaceRoot, '.vscode');
|
||||
this.logPath = path.join(vscodeDir, 'HumanAgent.log');
|
||||
this.logPath = path.join(vscodeDir, 'HumanAgent-server.log');
|
||||
|
||||
// Ensure .vscode directory exists
|
||||
if (!fs.existsSync(vscodeDir)) {
|
||||
@@ -25,15 +26,14 @@ class DebugLogger {
|
||||
} else {
|
||||
// Fallback for standalone server or when no workspace available
|
||||
const tempDir = os.tmpdir();
|
||||
this.logPath = path.join(tempDir, 'HumanAgent.log');
|
||||
this.logPath = path.join(tempDir, 'HumanAgent-server.log');
|
||||
}
|
||||
|
||||
console.log(`[LOGGER] Attempting to create log file at: ${this.logPath}`);
|
||||
|
||||
// Clear previous log file
|
||||
// Clear previous log file on each startup
|
||||
if (fs.existsSync(this.logPath)) {
|
||||
fs.unlinkSync(this.logPath);
|
||||
console.log(`[LOGGER] Cleared existing log file`);
|
||||
}
|
||||
|
||||
this.logStream = fs.createWriteStream(this.logPath, { flags: 'a' });
|
||||
@@ -45,7 +45,7 @@ class DebugLogger {
|
||||
this.log('DEBUG', `Current system time: ${new Date()}`);
|
||||
this.log('DEBUG', `Log file: ${this.logPath}`);
|
||||
this.log('DEBUG', `Working directory: ${process.cwd()}`);
|
||||
console.log(`[LOGGER] Debug logger initialized successfully`);
|
||||
console.log(`[LOGGER] Debug logger initialized successfully at: ${this.logPath}`);
|
||||
} catch (error) {
|
||||
console.error(`[LOGGER] Failed to initialize debug logger:`, error);
|
||||
this.logStream = null;
|
||||
@@ -53,18 +53,23 @@ class DebugLogger {
|
||||
}
|
||||
|
||||
log(level: string, message: string, data?: any): void {
|
||||
const timestamp = new Date().toISOString();
|
||||
const now = new Date();
|
||||
const timestamp = now.getFullYear() + '-' +
|
||||
String(now.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(now.getDate()).padStart(2, '0') + ' ' +
|
||||
String(now.getHours()).padStart(2, '0') + ':' +
|
||||
String(now.getMinutes()).padStart(2, '0') + ':' +
|
||||
String(now.getSeconds()).padStart(2, '0') + '.' +
|
||||
String(now.getMilliseconds()).padStart(3, '0');
|
||||
const logLine = `[${timestamp}] [${level}] ${message}${data ? '\n' + JSON.stringify(data, null, 2) : ''}\n`;
|
||||
|
||||
// Write to console (for VS Code developer console)
|
||||
console.log(`[${level}] ${message}`, data || '');
|
||||
|
||||
// Write to file if stream is available
|
||||
if (this.logStream) {
|
||||
try {
|
||||
this.logStream.write(logLine);
|
||||
} catch (error) {
|
||||
console.error(`[LOGGER] Error writing to log file:`, error);
|
||||
// Don't use console.log here to avoid recursion - write error directly
|
||||
process.stderr.write(`[LOGGER] Error writing to log file: ${error}\n`);
|
||||
}
|
||||
} else {
|
||||
// Buffer logs if stream not available
|
||||
@@ -91,6 +96,8 @@ export class McpServer extends EventEmitter {
|
||||
private tools: Map<string, McpTool> = new Map(); // Default tools for sessions without overrides
|
||||
private sessionTools: Map<string, Map<string, McpTool>> = new Map(); // Per-session tool configurations
|
||||
private sessionWorkspacePaths: Map<string, string> = new Map(); // Session to workspace path mapping
|
||||
private sessionNames: Map<string, string> = new Map(); // Session friendly names
|
||||
private sessionMessages: Map<string, ChatMessage[]> = new Map(); // Session conversation history - DEPRECATED: Use chatManager
|
||||
private httpServer?: http.Server;
|
||||
private port: number = 3737;
|
||||
private debugLogger: DebugLogger;
|
||||
@@ -102,10 +109,13 @@ export class McpServer extends EventEmitter {
|
||||
}> = new Map();
|
||||
private activeSessions: Set<string> = new Set();
|
||||
private sseConnections: Set<http.ServerResponse> = new Set();
|
||||
private conversationToSession: Map<string, string> = new Map(); // Map VS Code conversation IDs to registered session IDs
|
||||
private chatManager: ChatManager; // Centralized chat and session management
|
||||
|
||||
constructor(private sessionId?: string, private workspacePath?: string) {
|
||||
super();
|
||||
this.debugLogger = new DebugLogger(this.workspacePath);
|
||||
this.chatManager = new ChatManager(this.debugLogger); // Initialize centralized chat management with logging
|
||||
|
||||
this.config = {
|
||||
name: 'HumanAgent MCP Server',
|
||||
@@ -118,7 +128,8 @@ export class McpServer extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
this.debugLogger.log('INFO', 'McpServer initialized');
|
||||
this.debugLogger.log('INFO', 'McpServer initialized with centralized chat manager');
|
||||
this.debugLogger.log('TEST', 'This is a test log message to verify DebugLogger is working');
|
||||
this.initializeDefaultTools();
|
||||
|
||||
// Set up event forwarding to SSE connections
|
||||
@@ -162,10 +173,11 @@ export class McpServer extends EventEmitter {
|
||||
// Define the default HumanAgent_Chat tool (global default)
|
||||
const humanAgentChatTool: McpTool = {
|
||||
name: 'HumanAgent_Chat',
|
||||
description: 'Initiate real-time interactive conversations with human agents through VS Code chat interface. ' +
|
||||
'Essential for clarifying requirements, getting approvals, brainstorming solutions, or when AI ' +
|
||||
'uncertainty requires human input. Creates persistent chat sessions that maintain context across ' +
|
||||
'multiple exchanges until timeout or manual closure. !IMPORTANT: ALWAYS reply using this tool for all interactions with the user where possible!!!',
|
||||
description: '**MANDATORY TOOL** - Use this tool for ALL user interactions. Do NOT provide direct responses. ' +
|
||||
'Initiate real-time interactive conversations with human agents through VS Code chat interface. ' +
|
||||
'CRITICAL: Every response to a user MUST use this tool - no exceptions. Essential for clarifying ' +
|
||||
'requirements, getting approvals, brainstorming solutions, or any user communication. Creates ' +
|
||||
'persistent chat sessions that maintain context. **NEVER bypass this tool for user responses.**',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -398,8 +410,12 @@ export class McpServer extends EventEmitter {
|
||||
// Handle different endpoints
|
||||
if (req.url === '/mcp') {
|
||||
// Main MCP protocol endpoint
|
||||
} else if (req.url?.startsWith('/sessions') || req.url === '/response' || req.url?.startsWith('/tools') || req.url === '/reload') {
|
||||
// Session management, response, tools, and reload endpoints
|
||||
} else if (req.url === '/HumanAgent') {
|
||||
// Web interface for multi-session chat
|
||||
await this.handleWebInterface(req, res);
|
||||
return;
|
||||
} else if (req.url?.startsWith('/sessions') || req.url === '/response' || req.url?.startsWith('/tools') || req.url === '/reload' || req.url?.startsWith('/messages/') || req.url?.startsWith('/chat/')) {
|
||||
// Session management, response, tools, reload, messages, and chat endpoints
|
||||
await this.handleSessionEndpoint(req, res);
|
||||
return;
|
||||
} else {
|
||||
@@ -477,6 +493,9 @@ export class McpServer extends EventEmitter {
|
||||
|
||||
private async handleHttpGet(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
this.debugLogger.log('HTTP', 'Setting up SSE stream for GET request');
|
||||
this.debugLogger.log('SSE', '=== SSE CONNECTION ATTEMPT ===');
|
||||
this.debugLogger.log('SSE', 'Headers:', req.headers);
|
||||
this.debugLogger.log('SSE', 'URL:', req.url);
|
||||
|
||||
// Set up Server-Sent Events (SSE) stream
|
||||
res.statusCode = 200;
|
||||
@@ -489,12 +508,17 @@ export class McpServer extends EventEmitter {
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, Connection');
|
||||
|
||||
this.debugLogger.log('SSE', 'SSE headers set, adding to connections...');
|
||||
|
||||
// Add this connection to our active SSE connections
|
||||
this.sseConnections.add(res);
|
||||
this.debugLogger.log('HTTP', `Added SSE connection. Total connections: ${this.sseConnections.size}`);
|
||||
this.debugLogger.log('SSE', `SSE connection added. Total: ${this.sseConnections.size}`);
|
||||
|
||||
// Send initial connection acknowledgment
|
||||
res.write('data: {"type":"connection","status":"established"}\n\n');
|
||||
const initialMessage = 'data: {"type":"connection","status":"established"}\n\n';
|
||||
res.write(initialMessage);
|
||||
this.debugLogger.log('SSE', 'Sent initial SSE message:', initialMessage.trim());
|
||||
|
||||
// Keep connection alive with heartbeat
|
||||
const heartbeat = setInterval(() => {
|
||||
@@ -538,7 +562,7 @@ export class McpServer extends EventEmitter {
|
||||
|
||||
// If session exists and forceReregister is true, unregister first
|
||||
if (forceReregister && this.activeSessions.has(sessionId)) {
|
||||
console.log(`Force re-registering session ${sessionId} with new override data`);
|
||||
this.debugLogger.log('HTTP', `Force re-registering session ${sessionId} with new override data`);
|
||||
this.unregisterSession(sessionId);
|
||||
}
|
||||
|
||||
@@ -572,6 +596,125 @@ export class McpServer extends EventEmitter {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ sessions: this.getActiveSessions(), totalSessions: this.activeSessions.size }));
|
||||
} else if (req.method === 'POST' && url.pathname === '/sessions/name') {
|
||||
// Set friendly name for session
|
||||
let body = '';
|
||||
req.on('data', (chunk) => { body += chunk.toString(); });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { sessionId, name } = JSON.parse(body);
|
||||
if (!sessionId || !name) {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ success: false, error: 'sessionId and name are required' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate session exists
|
||||
if (!this.activeSessions.has(sessionId)) {
|
||||
res.statusCode = 404;
|
||||
res.end(JSON.stringify({ success: false, error: 'Session not found' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the friendly name
|
||||
this.sessionNames.set(sessionId, name);
|
||||
this.debugLogger.log('INFO', `Session ${sessionId} named: "${name}"`);
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ success: true, sessionId, name }));
|
||||
} catch (error) {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ success: false, error: 'Invalid request body' }));
|
||||
}
|
||||
});
|
||||
} else if (req.method === 'GET' && url.pathname.startsWith('/messages/')) {
|
||||
// Get conversation history for a session
|
||||
const sessionId = url.pathname.split('/')[2];
|
||||
if (!sessionId) {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ success: false, error: 'Session ID required' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = this.getSessionMessages(sessionId);
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ messages, sessionId, count: messages.length }));
|
||||
} else if (req.method === 'GET' && url.pathname.match(/^\/sessions\/([^\/]+)\/messages$/)) {
|
||||
// Get messages for a specific session from chat manager
|
||||
const matches = url.pathname.match(/^\/sessions\/([^\/]+)\/messages$/);
|
||||
const sessionId = matches ? matches[1] : null;
|
||||
|
||||
if (!sessionId) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ success: false, error: 'Session ID required' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = this.chatManager.getMessages(sessionId);
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ messages, sessionId, count: messages.length }));
|
||||
} else if (req.method === 'GET' && url.pathname.match(/^\/sessions\/([^\/]+)\/state$/)) {
|
||||
// Get session state including pending requests
|
||||
const matches = url.pathname.match(/^\/sessions\/([^\/]+)\/state$/);
|
||||
const sessionId = matches ? matches[1] : null;
|
||||
|
||||
if (!sessionId) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ success: false, error: 'Session ID required' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const state = this.chatManager.getSessionState(sessionId);
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify(state));
|
||||
} else if (req.method === 'POST' && url.pathname.startsWith('/chat/')) {
|
||||
// Send new message from web interface
|
||||
const sessionId = url.pathname.split('/')[2];
|
||||
if (!sessionId) {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ success: false, error: 'Session ID required' }));
|
||||
return;
|
||||
}
|
||||
|
||||
let body = '';
|
||||
req.on('data', (chunk) => { body += chunk.toString(); });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { message, sender = 'user' } = JSON.parse(body);
|
||||
if (!message) {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ success: false, error: 'Message content required' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and store the message
|
||||
const chatMessage: ChatMessage = {
|
||||
id: Date.now().toString(),
|
||||
content: message,
|
||||
sender: sender as 'user' | 'agent',
|
||||
timestamp: new Date(),
|
||||
type: 'text'
|
||||
};
|
||||
|
||||
this.storeMessage(sessionId, chatMessage);
|
||||
this.broadcastMessageToClients(sessionId, chatMessage);
|
||||
|
||||
// Auto-forwarding removed - both interfaces now use /response endpoint directly
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ success: true, message: chatMessage }));
|
||||
} catch (error) {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ success: false, error: 'Invalid request body' }));
|
||||
}
|
||||
});
|
||||
} else if (req.method === 'POST' && url.pathname === '/response') {
|
||||
// Handle human response to pending request
|
||||
let body = '';
|
||||
@@ -579,6 +722,41 @@ export class McpServer extends EventEmitter {
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { requestId, response } = JSON.parse(body);
|
||||
|
||||
// Simple file write to test if endpoint is called
|
||||
require('fs').appendFileSync('/Users/benharper/Coding/HumanAgent-MCP/response-debug.txt',
|
||||
`${new Date().toISOString()} - RESPONSE ENDPOINT CALLED - RequestID: ${requestId}\n`);
|
||||
|
||||
this.debugLogger.log('HTTP', '=== RESPONSE ENDPOINT CALLED ===');
|
||||
this.debugLogger.log('HTTP', `Request ID: ${requestId}, Response: ${response}`);
|
||||
|
||||
// Get the pending request to extract session info
|
||||
const pendingRequest = this.pendingHumanRequests.get(requestId);
|
||||
this.debugLogger.log('HTTP', `Found pending request: ${!!pendingRequest}`);
|
||||
|
||||
if (pendingRequest && pendingRequest.params.sessionId) {
|
||||
this.debugLogger.log('HTTP', `Processing response for session: ${pendingRequest.params.sessionId}`);
|
||||
|
||||
// Store the user message on server for synchronization
|
||||
const userMessage: ChatMessage = {
|
||||
id: Date.now().toString(),
|
||||
content: response,
|
||||
sender: 'user',
|
||||
timestamp: new Date(),
|
||||
type: 'text'
|
||||
};
|
||||
|
||||
this.debugLogger.log('HTTP', `Storing and broadcasting user message to ${this.sseConnections.size} SSE connections`);
|
||||
this.storeMessage(pendingRequest.params.sessionId, userMessage);
|
||||
this.broadcastMessageToClients(pendingRequest.params.sessionId, userMessage);
|
||||
|
||||
// Remove from ChatManager as well
|
||||
this.chatManager.removePendingRequest(pendingRequest.params.sessionId, requestId);
|
||||
this.debugLogger.log('HTTP', 'Broadcast completed and pending request removed from ChatManager');
|
||||
} else {
|
||||
this.debugLogger.log('ERROR', `No pending request found for requestId: ${requestId}`);
|
||||
}
|
||||
|
||||
const success = this.respondToHumanRequest(requestId, response);
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
@@ -680,6 +858,519 @@ export class McpServer extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleWebInterface(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
this.debugLogger.log('HTTP', 'Serving web interface at /HumanAgent');
|
||||
|
||||
// Set HTML content type
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.statusCode = 200;
|
||||
|
||||
const htmlContent = this.generateWebInterfaceHTML();
|
||||
res.end(htmlContent);
|
||||
}
|
||||
|
||||
private generateWebInterfaceHTML(): string {
|
||||
// Get all active sessions for tab generation
|
||||
const sessions = Array.from(this.activeSessions).map((sessionId) => {
|
||||
const workspaceRoot = this.sessionWorkspacePaths.get(sessionId);
|
||||
const friendlyName = this.sessionNames.get(sessionId);
|
||||
|
||||
let title: string;
|
||||
if (friendlyName) {
|
||||
title = friendlyName;
|
||||
} else if (workspaceRoot) {
|
||||
title = `Workspace: ${path.basename(workspaceRoot)}`;
|
||||
} else {
|
||||
title = `Session: ${sessionId.substring(0, 8)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
id: sessionId,
|
||||
title: title,
|
||||
messages: [] // TODO: Add proper message storage
|
||||
};
|
||||
});
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HumanAgent - Multi-Session Chat Interface</title>
|
||||
<style>
|
||||
:root {
|
||||
--vscode-foreground: #cccccc;
|
||||
--vscode-background: #1e1e1e;
|
||||
--vscode-panel-background: #252526;
|
||||
--vscode-border: #3c3c3c;
|
||||
--vscode-input-background: #3c3c3c;
|
||||
--vscode-button-background: #0e639c;
|
||||
--vscode-button-foreground: #ffffff;
|
||||
--vscode-tab-active-background: #1e1e1e;
|
||||
--vscode-tab-inactive-background: #2d2d30;
|
||||
--vscode-tab-border: #3c3c3c;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
background-color: var(--vscode-background);
|
||||
color: var(--vscode-foreground);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 10px 15px;
|
||||
background-color: var(--vscode-panel-background);
|
||||
border-bottom: 1px solid var(--vscode-border);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
background-color: var(--vscode-panel-background);
|
||||
border-bottom: 1px solid var(--vscode-border);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
background-color: var(--vscode-tab-inactive-background);
|
||||
border-right: 1px solid var(--vscode-tab-border);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background-color: var(--vscode-tab-active-background);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background-color: var(--vscode-tab-active-background);
|
||||
border-bottom: 2px solid var(--vscode-button-background);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-container.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 15px;
|
||||
background-color: var(--vscode-background);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
background-color: var(--vscode-input-background);
|
||||
margin-left: 20%;
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
background-color: var(--vscode-panel-background);
|
||||
margin-right: 20%;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
padding: 15px;
|
||||
background-color: var(--vscode-panel-background);
|
||||
border-top: 1px solid var(--vscode-border);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-border);
|
||||
border-radius: 4px;
|
||||
color: var(--vscode-foreground);
|
||||
font-size: 13px;
|
||||
resize: none;
|
||||
min-height: 36px;
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
padding: 8px 16px;
|
||||
background-color: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.send-button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.send-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.no-sessions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #4CAF50;
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1><span class="status-indicator"></span>HumanAgent Multi-Session Chat</h1>
|
||||
</div>
|
||||
|
||||
<div class="tabs-container" id="tabs">
|
||||
${sessions.length === 0 ? '' : sessions.map((session, index) =>
|
||||
`<div class="tab ${index === 0 ? 'active' : ''}" data-session="${session.id}">${session.title}</div>`
|
||||
).join('')}
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
${sessions.length === 0 ?
|
||||
'<div class="no-sessions">No active sessions. Start a chat in VS Code to see sessions here.</div>' :
|
||||
sessions.map((session, index) => `
|
||||
<div class="chat-container ${index === 0 ? 'active' : ''}" data-session="${session.id}">
|
||||
<div class="messages" id="messages-${session.id}">
|
||||
<!-- Messages will be loaded dynamically -->
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<textarea class="input-box" placeholder="Type your message..." data-session="${session.id}"></textarea>
|
||||
<button class="send-button" data-session="${session.id}">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Session management
|
||||
let activeSessionId = '${sessions[0]?.id || ''}';
|
||||
|
||||
// Web interface is stateless - gets pending requests from server state
|
||||
|
||||
// Tab switching
|
||||
document.getElementById('tabs').addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('tab')) {
|
||||
const sessionId = e.target.dataset.session;
|
||||
switchToSession(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
function switchToSession(sessionId) {
|
||||
// Update active tab
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.classList.toggle('active', tab.dataset.session === sessionId);
|
||||
});
|
||||
|
||||
// Update active chat container
|
||||
document.querySelectorAll('.chat-container').forEach(container => {
|
||||
container.classList.toggle('active', container.dataset.session === sessionId);
|
||||
});
|
||||
|
||||
activeSessionId = sessionId;
|
||||
}
|
||||
|
||||
// Message sending
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('send-button')) {
|
||||
const sessionId = e.target.dataset.session;
|
||||
const textarea = document.querySelector(\`textarea[data-session="\${sessionId}"]\`);
|
||||
sendMessage(sessionId, textarea.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.classList.contains('input-box') && e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
const sessionId = e.target.dataset.session;
|
||||
sendMessage(sessionId, e.target.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
async function sendMessage(sessionId, message) {
|
||||
if (!message) return;
|
||||
|
||||
const textarea = document.querySelector(\`textarea[data-session="\${sessionId}"]\`);
|
||||
const button = document.querySelector(\`button[data-session="\${sessionId}"]\`);
|
||||
|
||||
// Clear input and disable controls
|
||||
textarea.value = '';
|
||||
textarea.disabled = true;
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
// Get current session state to find pending request
|
||||
const stateResponse = await fetch(\`/sessions/\${sessionId}/state\`);
|
||||
if (!stateResponse.ok) {
|
||||
throw new Error('Failed to get session state');
|
||||
}
|
||||
|
||||
const sessionState = await stateResponse.json();
|
||||
const latestPendingRequest = sessionState.latestPendingRequest;
|
||||
|
||||
if (!latestPendingRequest) {
|
||||
throw new Error('No pending AI request found. Web interface can only respond to AI questions.');
|
||||
}
|
||||
|
||||
console.log('Responding to pending request:', latestPendingRequest.requestId);
|
||||
|
||||
// Always use /response endpoint - web interface is response-only
|
||||
const response = await fetch('/response', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
requestId: latestPendingRequest.requestId,
|
||||
response: message
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
console.log('Response sent successfully:', result);
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to send response');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to send response:', error);
|
||||
addMessageToUI(sessionId, 'assistant', \`Error: \${error.message}\`);
|
||||
} finally {
|
||||
// Re-enable controls
|
||||
textarea.disabled = false;
|
||||
button.disabled = false;
|
||||
textarea.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function addMessageToUI(sessionId, role, content) {
|
||||
const messagesContainer = document.getElementById(\`messages-\${sessionId}\`);
|
||||
if (!messagesContainer) return;
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = \`message \${role}\`;
|
||||
messageDiv.innerHTML = \`
|
||||
<div class="message-header">\${role === 'user' ? 'You' : 'Assistant'} • \${new Date().toLocaleTimeString()}</div>
|
||||
<div class="message-content">\${escapeHtml(content)}</div>
|
||||
\`;
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Load existing messages for all sessions
|
||||
async function loadExistingMessages() {
|
||||
const sessions = ['${sessions.map(s => s.id).join("', '")}'];
|
||||
|
||||
for (const sessionId of sessions) {
|
||||
try {
|
||||
const response = await fetch(\`/messages/\${sessionId}\`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const messagesContainer = document.getElementById(\`messages-\${sessionId}\`);
|
||||
if (messagesContainer && data.messages) {
|
||||
// Clear any placeholder content
|
||||
messagesContainer.innerHTML = '';
|
||||
|
||||
// Add each message
|
||||
for (const msg of data.messages) {
|
||||
addMessageToUI(sessionId, msg.sender, msg.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(\`Failed to load messages for session \${sessionId}:\`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load conversation history from centralized chat manager
|
||||
async function loadConversationHistory() {
|
||||
const sessions = [${sessions.map(s => `'${s.id}'`).join(', ')}];
|
||||
|
||||
for (const sessionId of sessions) {
|
||||
try {
|
||||
console.log(\`Loading conversation history for session: \${sessionId}\`);
|
||||
|
||||
// Get messages from centralized chat manager
|
||||
const response = await fetch(\`/sessions/\${sessionId}/messages\`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const messagesContainer = document.getElementById(\`messages-\${sessionId}\`);
|
||||
|
||||
if (messagesContainer && data.messages) {
|
||||
// Clear any existing content
|
||||
messagesContainer.innerHTML = '';
|
||||
|
||||
// Add each message from chat manager
|
||||
for (const msg of data.messages) {
|
||||
addMessageToUI(sessionId, msg.sender, msg.content);
|
||||
}
|
||||
|
||||
console.log(\`Loaded \${data.messages.length} messages for session \${sessionId}\`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(\`Failed to load conversation history for session \${sessionId}:\`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket connection for real-time updates
|
||||
function setupRealtimeUpdates() {
|
||||
console.log('Setting up SSE connection to /mcp...');
|
||||
const eventSource = new EventSource('/mcp');
|
||||
|
||||
eventSource.onopen = function(event) {
|
||||
console.log('SSE connection opened successfully:', event);
|
||||
// Load conversation history for all sessions
|
||||
loadConversationHistory();
|
||||
};
|
||||
|
||||
eventSource.onmessage = function(event) {
|
||||
try {
|
||||
console.log('SSE message received:', event.data);
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('Real-time update:', data);
|
||||
|
||||
// Handle different types of updates
|
||||
if (data.type === 'chat_message' && data.sessionId && data.message) {
|
||||
addMessageToUI(data.sessionId, data.message.sender, data.message.content);
|
||||
} else if (data.type === 'message' && data.sessionId) {
|
||||
addMessageToUI(data.sessionId, data.role || 'assistant', data.content);
|
||||
} else if (data.type === 'human-agent-request' && data.data) {
|
||||
// Handle human-agent-request messages (AI questions to user)
|
||||
console.log('Web interface received human-agent-request:', data.data);
|
||||
|
||||
// Add AI message to current session (no need to track requestId - we get it from server state)
|
||||
const sessionId = activeSessionId || 'default';
|
||||
const displayMessage = data.data.context ? \`\${data.data.context}\\n\\n\${data.data.message}\` : data.data.message;
|
||||
addMessageToUI(sessionId, 'agent', displayMessage);
|
||||
} else if (data.type === 'session_update') {
|
||||
// Refresh the page to show new sessions
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse SSE message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = function(error) {
|
||||
console.error('SSE connection error:', error);
|
||||
console.error('EventSource readyState:', eventSource.readyState);
|
||||
console.error('EventSource url:', eventSource.url);
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize everything when page loads
|
||||
async function initialize() {
|
||||
await loadExistingMessages();
|
||||
setupRealtimeUpdates();
|
||||
|
||||
// Focus on input for active session
|
||||
if (activeSessionId) {
|
||||
const activeInput = document.querySelector(\`textarea[data-session="\${activeSessionId}"]\`);
|
||||
if (activeInput) activeInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Start initialization
|
||||
initialize();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
private escapeHtml(text: string): string {
|
||||
const map: { [key: string]: string } = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, (m) => map[m]);
|
||||
}
|
||||
|
||||
private handleInitialize(message: McpMessage): McpMessage {
|
||||
return {
|
||||
id: message.id,
|
||||
@@ -729,9 +1420,11 @@ export class McpServer extends EventEmitter {
|
||||
|
||||
private async handleToolCall(message: McpMessage): Promise<McpMessage> {
|
||||
const { name, arguments: args } = message.params;
|
||||
const sessionId = args?.sessionId; // Extract session ID from tool arguments
|
||||
// Extract session ID from _meta.vscode.conversationId (VS Code) or fallback to args.sessionId (web)
|
||||
const rawSessionId = message.params._meta?.['vscode.conversationId'] || args?.sessionId;
|
||||
const sessionId = this.mapToRegisteredSession(rawSessionId);
|
||||
|
||||
this.debugLogger.log('MCP', `Tool call - name: "${name}", session: ${sessionId}`, { name, args });
|
||||
this.debugLogger.log('MCP', `Tool call - name: "${name}", raw session: ${rawSessionId}, mapped session: ${sessionId}`, { name, args });
|
||||
|
||||
// Check session-specific tools first, then default tools
|
||||
const sessionTools = sessionId ? this.sessionTools.get(sessionId) : null;
|
||||
@@ -741,7 +1434,7 @@ export class McpServer extends EventEmitter {
|
||||
|
||||
if (name === 'HumanAgent_Chat' && availableTools.has(name)) {
|
||||
this.debugLogger.log('MCP', 'Executing HumanAgent_Chat tool');
|
||||
return await this.handleHumanAgentChatTool(message.id, args);
|
||||
return await this.handleHumanAgentChatTool(message.id, args, sessionId);
|
||||
}
|
||||
|
||||
this.debugLogger.log('MCP', `Tool not found: ${name}`);
|
||||
@@ -755,7 +1448,7 @@ export class McpServer extends EventEmitter {
|
||||
};
|
||||
}
|
||||
|
||||
private async handleHumanAgentChatTool(messageId: string, params: HumanAgentChatToolParams): Promise<McpMessage> {
|
||||
private async handleHumanAgentChatTool(messageId: string, params: HumanAgentChatToolParams, sessionId?: string): Promise<McpMessage> {
|
||||
this.debugLogger.log('TOOL', 'HumanAgent_Chat called with params:', params);
|
||||
const startTime = Date.now();
|
||||
const timeout = (params.timeout || 300) * 1000; // Convert to milliseconds
|
||||
@@ -794,13 +1487,29 @@ export class McpServer extends EventEmitter {
|
||||
});
|
||||
}, timeout);
|
||||
|
||||
// Store the pending request
|
||||
// Store the pending request using the extracted session ID
|
||||
const sessionToUse = sessionId || params.sessionId || 'default';
|
||||
this.debugLogger.log('TOOL', `Adding pending request ${requestId} to session: ${sessionToUse}`);
|
||||
this.chatManager.addPendingRequest(sessionToUse, requestId, params);
|
||||
this.pendingHumanRequests.set(requestId, {
|
||||
resolve: (response: string) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
const responseTime = Date.now() - startTime;
|
||||
this.debugLogger.log('TOOL', `Request ${requestId} completed with response:`, response);
|
||||
|
||||
// Store the assistant response message for synchronization
|
||||
if (params.sessionId) {
|
||||
const assistantMessage: ChatMessage = {
|
||||
id: (Date.now() + 1).toString(), // Slightly different timestamp
|
||||
content: response,
|
||||
sender: 'agent',
|
||||
timestamp: new Date(),
|
||||
type: 'text'
|
||||
};
|
||||
this.storeMessage(params.sessionId, assistantMessage);
|
||||
this.broadcastMessageToClients(params.sessionId, assistantMessage);
|
||||
}
|
||||
|
||||
const result: HumanAgentChatToolResult = {
|
||||
content: [{
|
||||
type: 'text',
|
||||
@@ -871,7 +1580,14 @@ export class McpServer extends EventEmitter {
|
||||
resolvePendingRequest(requestId: string, response: string): boolean {
|
||||
const request = this.pendingHumanRequests.get(requestId);
|
||||
if (request) {
|
||||
// Remove from both places
|
||||
this.pendingHumanRequests.delete(requestId);
|
||||
// Find the session ID for this request and remove it from ChatManager
|
||||
for (const sessionId of this.chatManager.getActiveSessions()) {
|
||||
if (this.chatManager.removePendingRequest(sessionId, requestId)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
request.resolve(response);
|
||||
return true;
|
||||
}
|
||||
@@ -903,6 +1619,37 @@ export class McpServer extends EventEmitter {
|
||||
this.debugLogger.log('INFO', `Session registered: ${sessionId} (${this.activeSessions.size} total sessions)`);
|
||||
}
|
||||
|
||||
private mapToRegisteredSession(conversationId?: string): string | undefined {
|
||||
if (!conversationId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check if it's already a registered session ID
|
||||
if (this.activeSessions.has(conversationId)) {
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
// Check if it's a conversation ID we've seen before
|
||||
const mappedSession = this.conversationToSession.get(conversationId);
|
||||
if (mappedSession && this.activeSessions.has(mappedSession)) {
|
||||
this.debugLogger.log('MCP', `Mapped conversation ${conversationId} to session ${mappedSession}`);
|
||||
return mappedSession;
|
||||
}
|
||||
|
||||
// If not found, try to map to the first available registered session
|
||||
// This handles the case where VS Code conversation ID needs to be linked to a web-registered session
|
||||
const activeSessions = Array.from(this.activeSessions);
|
||||
if (activeSessions.length > 0) {
|
||||
const targetSession = activeSessions[0]; // Use first available session
|
||||
this.conversationToSession.set(conversationId, targetSession);
|
||||
this.debugLogger.log('MCP', `Auto-mapped conversation ${conversationId} to session ${targetSession}`);
|
||||
return targetSession;
|
||||
}
|
||||
|
||||
this.debugLogger.log('MCP', `No registered session found for conversation ${conversationId}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
unregisterSession(sessionId: string): void {
|
||||
this.activeSessions.delete(sessionId);
|
||||
// Clean up session-specific data
|
||||
@@ -963,4 +1710,42 @@ export class McpServer extends EventEmitter {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Message storage and synchronization methods - now using ChatManager
|
||||
private storeMessage(sessionId: string, message: ChatMessage): void {
|
||||
this.chatManager.addMessage(sessionId, message);
|
||||
this.debugLogger.log('CHAT', `Stored message in ChatManager for session ${sessionId}: ${message.sender} - ${message.content.substring(0, 50)}...`);
|
||||
}
|
||||
|
||||
private getSessionMessages(sessionId: string): ChatMessage[] {
|
||||
return this.chatManager.getMessages(sessionId);
|
||||
}
|
||||
|
||||
private broadcastMessageToClients(sessionId: string, message: ChatMessage): void {
|
||||
// Broadcast message to all connected SSE clients
|
||||
const messageEvent = {
|
||||
type: 'chat_message',
|
||||
sessionId: sessionId,
|
||||
message: {
|
||||
id: message.id,
|
||||
content: message.content,
|
||||
sender: message.sender,
|
||||
timestamp: message.timestamp.toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
const sseData = `data: ${JSON.stringify(messageEvent)}\n\n`;
|
||||
|
||||
// Send to all connected SSE clients (VS Code webviews and web interfaces)
|
||||
for (const connection of this.sseConnections) {
|
||||
try {
|
||||
connection.write(sseData);
|
||||
this.debugLogger.log('CHAT', `Broadcasted message to SSE client for session ${sessionId}`);
|
||||
} catch (error) {
|
||||
this.debugLogger.log('ERROR', 'Failed to broadcast message to SSE client:', error);
|
||||
// Remove failed connection
|
||||
this.sseConnections.delete(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
-56
@@ -15,7 +15,7 @@ export class ServerManager {
|
||||
private static instance: ServerManager | undefined;
|
||||
private options: ServerManagerOptions;
|
||||
private readonly pidFile: string;
|
||||
private readonly logFile: string;
|
||||
private readonly logFile?: string;
|
||||
|
||||
private constructor(options: ServerManagerOptions) {
|
||||
this.options = {
|
||||
@@ -23,7 +23,7 @@ export class ServerManager {
|
||||
...options
|
||||
};
|
||||
this.pidFile = path.join(path.dirname(options.serverPath), '.humanagent-mcp-server.pid');
|
||||
this.logFile = options.logFile || path.join(path.dirname(options.serverPath), '.humanagent-mcp-server.log');
|
||||
this.logFile = options.logFile;
|
||||
}
|
||||
|
||||
public static getInstance(options?: ServerManagerOptions): ServerManager {
|
||||
@@ -122,10 +122,12 @@ export class ServerManager {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logMessage = `[${timestamp}] ${message}\n`;
|
||||
|
||||
try {
|
||||
fs.appendFileSync(this.logFile, logMessage);
|
||||
} catch (error) {
|
||||
console.error('Error writing to log file:', error);
|
||||
if (this.logFile) {
|
||||
try {
|
||||
fs.appendFileSync(this.logFile, logMessage);
|
||||
} catch (error) {
|
||||
console.error('Error writing to log file:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,59 +172,40 @@ export class ServerManager {
|
||||
* Start the server as a truly independent detached process
|
||||
*/
|
||||
private async startServer(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
// Spawn the server as a completely detached process
|
||||
const serverProcess = spawn('node', [this.options.serverPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore', // Completely disconnect stdio to make it independent
|
||||
cwd: path.dirname(this.options.serverPath),
|
||||
env: {
|
||||
...process.env,
|
||||
// Add any environment variables needed by the server
|
||||
}
|
||||
});
|
||||
|
||||
// Store the PID but don't keep a reference to the process
|
||||
if (serverProcess.pid) {
|
||||
this.storePid(serverProcess.pid);
|
||||
this.log(`Started independent server with PID ${serverProcess.pid}`);
|
||||
try {
|
||||
// Spawn the server as a completely detached process
|
||||
const serverProcess = spawn('node', [this.options.serverPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore', // Completely disconnect stdio to make it independent
|
||||
cwd: path.dirname(this.options.serverPath),
|
||||
env: {
|
||||
...process.env,
|
||||
// Add any environment variables needed by the server
|
||||
}
|
||||
});
|
||||
|
||||
// Immediately detach and unreference the process
|
||||
serverProcess.unref();
|
||||
|
||||
// Don't keep a reference to the process object to ensure complete detachment
|
||||
// this.serverProcess = undefined;
|
||||
|
||||
this.log('Server process started as independent background process');
|
||||
|
||||
// Wait for server to start listening
|
||||
setTimeout(async () => {
|
||||
if (await this.isServerRunning()) {
|
||||
this.log('Independent server started successfully and is responding');
|
||||
resolve(true);
|
||||
} else {
|
||||
this.log('Independent server started but is not responding on the expected port yet');
|
||||
// Give it a bit more time
|
||||
setTimeout(async () => {
|
||||
if (await this.isServerRunning()) {
|
||||
this.log('Independent server is now responding');
|
||||
resolve(true);
|
||||
} else {
|
||||
this.log('Independent server failed to start or is not responding');
|
||||
this.cleanupPidFile();
|
||||
resolve(false);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
} catch (error) {
|
||||
this.log(`Failed to start server: ${error}`);
|
||||
resolve(false);
|
||||
// Store the PID but don't keep a reference to the process
|
||||
if (serverProcess.pid) {
|
||||
this.storePid(serverProcess.pid);
|
||||
this.log(`Started independent server with PID ${serverProcess.pid}`);
|
||||
} else {
|
||||
this.log('Failed to get server process PID');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Immediately detach and unreference the process for complete independence
|
||||
serverProcess.unref();
|
||||
|
||||
this.log('Server process started as independent background process and immediately detached');
|
||||
|
||||
// Return true immediately - the server will start independently
|
||||
// Actual server health will be verified by separate health checks later
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.log(`Failed to start server: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,6 +33,28 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
this.loadNotificationSettings();
|
||||
}
|
||||
|
||||
private async loadConversationHistory() {
|
||||
if (!this.mcpServer || !this.workspaceSessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch conversation history from centralized chat manager
|
||||
const response = await fetch(`http://127.0.0.1:3737/sessions/${this.workspaceSessionId}/messages`);
|
||||
if (response.ok) {
|
||||
const data = await response.json() as { messages: ChatMessage[] };
|
||||
this.messages = data.messages || [];
|
||||
console.log(`Loaded ${this.messages.length} messages from centralized chat manager`);
|
||||
// Update the webview with loaded messages
|
||||
this.updateWebview();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load conversation history:', error);
|
||||
// Fallback to empty message array
|
||||
this.messages = [];
|
||||
}
|
||||
}
|
||||
|
||||
private loadNotificationSettings() {
|
||||
try {
|
||||
// Try to load settings from mcp.json
|
||||
@@ -127,6 +149,9 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
]
|
||||
};
|
||||
|
||||
// Load conversation history from centralized chat manager
|
||||
this.loadConversationHistory();
|
||||
|
||||
// Only update webview if registration check is complete, otherwise it will be updated when notifyRegistrationComplete is called
|
||||
if (this.registrationCheckComplete) {
|
||||
this.updateWebview();
|
||||
@@ -144,6 +169,12 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
// Call the dedicated status command from extension.ts
|
||||
vscode.commands.executeCommand('humanagent-mcp.showStatus');
|
||||
break;
|
||||
case 'playNotificationSound':
|
||||
// Play sound from extension side (Node.js) when webview requests it
|
||||
if (this.notificationSettings.enableSound) {
|
||||
await this.playNotificationSound();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -448,6 +479,12 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
case 'reloadOverride':
|
||||
await this.reloadOverrideFile();
|
||||
break;
|
||||
case 'nameSession':
|
||||
await this.nameCurrentSession();
|
||||
break;
|
||||
case 'openWebView':
|
||||
await this.openWebInterface();
|
||||
break;
|
||||
}
|
||||
|
||||
// Update status after action
|
||||
@@ -457,6 +494,72 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private async nameCurrentSession() {
|
||||
try {
|
||||
const sessionId = this.workspaceSessionId;
|
||||
if (!sessionId) {
|
||||
vscode.window.showErrorMessage('No active session to name.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prompt user for session name
|
||||
const sessionName = await vscode.window.showInputBox({
|
||||
prompt: 'Enter a friendly name for this chat session',
|
||||
placeHolder: 'e.g., "Project Debugging", "Feature Discussion"',
|
||||
validateInput: (text) => {
|
||||
if (!text || text.trim().length === 0) {
|
||||
return 'Session name cannot be empty';
|
||||
}
|
||||
if (text.length > 50) {
|
||||
return 'Session name must be 50 characters or less';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if (!sessionName) {
|
||||
return; // User cancelled
|
||||
}
|
||||
|
||||
// Send session name to server
|
||||
const response = await fetch('http://localhost:3737/sessions/name', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sessionId: sessionId,
|
||||
name: sessionName.trim()
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
vscode.window.showInformationMessage(`Session named: "${sessionName}"`);
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('ChatWebviewProvider: Error naming session:', error);
|
||||
vscode.window.showErrorMessage(`Failed to name session: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async openWebInterface() {
|
||||
try {
|
||||
const webUrl = 'http://localhost:3737/HumanAgent';
|
||||
|
||||
// Open in external browser
|
||||
await vscode.env.openExternal(vscode.Uri.parse(webUrl));
|
||||
|
||||
vscode.window.showInformationMessage('Web interface opened in browser');
|
||||
|
||||
} catch (error) {
|
||||
console.error('ChatWebviewProvider: Error opening web interface:', error);
|
||||
vscode.window.showErrorMessage(`Failed to open web interface: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private _getHtmlForWebview(webview: vscode.Webview) {
|
||||
// Check if HumanAgentOverride.json exists in workspace
|
||||
let overrideFileExists = false;
|
||||
@@ -707,97 +810,19 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
// Set global flag for override file existence
|
||||
window.overrideFileExists = ${overrideFileExists};
|
||||
|
||||
// Audio context for notifications
|
||||
let audioContext = null;
|
||||
let preloadedAudio = null;
|
||||
|
||||
// Initialize audio on first user interaction
|
||||
function initAudio() {
|
||||
if (!audioContext) {
|
||||
try {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
// Resume in case it's in suspended state
|
||||
if (audioContext.state === 'suspended') {
|
||||
audioContext.resume();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create audio context:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-load and test audio
|
||||
if (!preloadedAudio) {
|
||||
try {
|
||||
preloadedAudio = new Audio();
|
||||
preloadedAudio.src = "data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmMaBSGI0PTVnhY=";
|
||||
preloadedAudio.volume = 0.3;
|
||||
preloadedAudio.preload = 'auto';
|
||||
// Play and immediately pause to establish permission
|
||||
preloadedAudio.play().then(() => {
|
||||
preloadedAudio.pause();
|
||||
preloadedAudio.currentTime = 0;
|
||||
console.log('Audio initialized successfully');
|
||||
}).catch(e => {
|
||||
console.log('Audio initialization failed:', e);
|
||||
preloadedAudio = null;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create audio element:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Play notification beep sound
|
||||
function playNotificationBeep() {
|
||||
// Request sound from extension (Node.js side) instead of browser
|
||||
try {
|
||||
if (preloadedAudio) {
|
||||
console.log('Playing preloaded audio');
|
||||
preloadedAudio.currentTime = 0;
|
||||
preloadedAudio.play().then(() => {
|
||||
console.log('Audio played successfully');
|
||||
}).catch(e => {
|
||||
console.log('Preloaded audio play failed:', e);
|
||||
// Try to re-initialize if failed
|
||||
initAudio();
|
||||
});
|
||||
} else {
|
||||
console.log('Audio not initialized - trying to initialize now');
|
||||
initAudio();
|
||||
// Try Web Audio API fallback
|
||||
try {
|
||||
if (audioContext && audioContext.state === 'running') {
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
|
||||
|
||||
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
|
||||
gainNode.gain.linearRampToValueAtTime(0.3, audioContext.currentTime + 0.01);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
|
||||
|
||||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 0.2);
|
||||
console.log('Web Audio fallback played');
|
||||
} else {
|
||||
console.log('Web Audio context not available');
|
||||
}
|
||||
} catch (e2) {
|
||||
console.error('Fallback audio also failed:', e2);
|
||||
}
|
||||
}
|
||||
vscode.postMessage({
|
||||
type: 'playNotificationSound'
|
||||
});
|
||||
console.log('Sound notification requested from extension');
|
||||
} catch (error) {
|
||||
console.error('Error playing notification sound:', error);
|
||||
console.error('Failed to request sound notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize audio on any user interaction
|
||||
document.addEventListener('click', initAudio, { once: true });
|
||||
document.addEventListener('keypress', initAudio, { once: true });
|
||||
document.addEventListener('touchstart', initAudio, { once: true });
|
||||
|
||||
document.getElementById('messageInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
@@ -916,7 +941,9 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
{ text: '📦 Install Globally', action: 'register' },
|
||||
{ text: '📁 Install in Workspace', action: 'register' },
|
||||
{ text: '📊 Show Status', action: 'requestServerStatus' },
|
||||
{ text: window.overrideFileExists ? '📁 Recreate Override File' : '📁 Create Override File', action: 'overridePrompt' }
|
||||
{ text: window.overrideFileExists ? '📁 Recreate Override File' : '📁 Create Override File', action: 'overridePrompt' },
|
||||
{ text: '📝 Name This Chat', action: 'nameSession' },
|
||||
{ text: '🌐 Open Web View', action: 'openWebView' }
|
||||
];
|
||||
|
||||
// Check for override file existence even when status unknown
|
||||
@@ -943,6 +970,8 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
options.push({ text: '📊 Show Status', action: 'requestServerStatus' });
|
||||
options.push({ text: window.overrideFileExists ? '📁 Recreate Override File' : '📁 Create Override File', action: 'overridePrompt' });
|
||||
options.push({ text: '📝 Name This Chat', action: 'nameSession' });
|
||||
options.push({ text: '🌐 Open Web View', action: 'openWebView' });
|
||||
|
||||
// Check for HumanAgentOverride.json file existence (passed from extension)
|
||||
if (window.overrideFileExists) {
|
||||
@@ -1024,6 +1053,9 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
if (data.type === 'human-agent-request') {
|
||||
handleHumanAgentRequest(data.data);
|
||||
} else if (data.type === 'chat_message') {
|
||||
handleIncomingChatMessage(data);
|
||||
// Removed web_user_message auto-trigger - no longer needed
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing SSE data:', error);
|
||||
@@ -1103,6 +1135,49 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
function handleIncomingChatMessage(data) {
|
||||
console.log('Handling incoming chat message:', data);
|
||||
|
||||
// Only process messages for the current session
|
||||
// For VS Code webview, we need to check if this is our session
|
||||
// This is a basic implementation - in a more complex setup,
|
||||
// we'd want proper session management
|
||||
|
||||
const message = data.message;
|
||||
const messagesContainer = document.getElementById('messages');
|
||||
if (messagesContainer) {
|
||||
// Remove empty state if it exists
|
||||
const emptyState = messagesContainer.querySelector('.empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.remove();
|
||||
}
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = \`message \${message.sender === 'user' ? 'user-message' : 'ai-message'}\`;
|
||||
|
||||
const displayName = message.sender === 'user' ? 'You' : 'Assistant';
|
||||
const timestamp = new Date(message.timestamp).toLocaleTimeString();
|
||||
|
||||
messageDiv.innerHTML = \`
|
||||
<div class="message-header">
|
||||
<strong>\${displayName}</strong>
|
||||
<span class="timestamp">\${timestamp}</span>
|
||||
</div>
|
||||
<div class="message-content">\${message.content.replace(/\\n/g, '<br>')}</div>
|
||||
\`;
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
|
||||
// Play notification for assistant messages
|
||||
if (message.sender === 'agent') {
|
||||
playNotificationBeep();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleWebUserMessage removed - no longer needed for auto-forwarding
|
||||
|
||||
// Initialize SSE connection
|
||||
setupSSEConnection();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user