Implement clean request state management system

MAJOR IMPROVEMENT: Replace confusing 'human-agent-request' event with clean 'request-state-change' SSE system

 NEW: request-state-change events for both web and VS Code interfaces
 CLEAN: Separate message display (via ChatManager) from input control (via request state)
 CONSISTENT: Both interfaces get same state updates via SSE
 ELIMINATED: Duplicate message display issues
 PROPER: Input enabling/disabling and 'waiting for response' indicators

States: 'waiting_for_response' → 'completed'
Benefits: Clean separation of concerns, no more mixed event types causing confusion

Ready for testing: VS Code input should enable when AI asks questions, disable when complete
This commit is contained in:
B Harper
2025-10-24 13:40:34 +11:00
parent cac8d40b66
commit 100508581e
4 changed files with 2663 additions and 424 deletions
+2596 -368
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -22,3 +22,7 @@
2025-10-24T02:22:43.114Z - RESPONSE ENDPOINT CALLED - RequestID: 5-1761272315744
2025-10-24T02:23:14.737Z - RESPONSE ENDPOINT CALLED - RequestID: 6-1761272566830
2025-10-24T02:24:18.799Z - RESPONSE ENDPOINT CALLED - RequestID: 3-1761272641413
2025-10-24T02:29:47.107Z - RESPONSE ENDPOINT CALLED - RequestID: 5-1761272965017
2025-10-24T02:33:40.664Z - RESPONSE ENDPOINT CALLED - RequestID: 3-1761273090722
2025-10-24T02:34:46.816Z - RESPONSE ENDPOINT CALLED - RequestID: 3-1761273266913
2025-10-24T02:39:08.715Z - RESPONSE ENDPOINT CALLED - RequestID: 4-1761273289660
+27 -19
View File
@@ -138,9 +138,9 @@ export class McpServer extends EventEmitter {
}
private setupEventForwarding(): void {
this.on('human-agent-request', (data) => {
this.debugLogger.log('SSE', 'Forwarding human-agent-request to SSE connections');
this.broadcastToSSE('human-agent-request', data);
this.on('request-state-change', (data) => {
this.debugLogger.log('SSE', 'Forwarding request-state-change to SSE connections');
this.broadcastToSSE('request-state-change', data);
});
}
@@ -1261,14 +1261,12 @@ export class McpServer extends EventEmitter {
addMessageToUI(data.sessionId, data.message.sender, data.message.content, data.message.source);
} 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);
} else if (data.type === 'request-state-change' && data.data) {
// Handle request state changes for input control
console.log('Web interface received request-state-change:', 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);
// No message display here - messages are handled via chat_message SSE
// This event only manages input state and waiting indicators
} else if (data.type === 'session_update') {
// Refresh the page to show new sessions
window.location.reload();
@@ -1406,15 +1404,6 @@ export class McpServer extends EventEmitter {
const displayMessage = params.context ? `${params.context}\n\n${params.message}` : params.message;
this.debugLogger.log('TOOL', 'Displaying message in chat UI:', displayMessage);
// Emit event to show message in chat UI immediately
this.emit('human-agent-request', {
requestId,
message: params.message,
context: params.context,
priority: params.priority || 'normal',
timestamp: new Date().toISOString()
});
// Wait for human response
return new Promise((resolve) => {
// Set up timeout
@@ -1451,6 +1440,16 @@ export class McpServer extends EventEmitter {
this.debugLogger.log('CHAT', `Stored AI message in ChatManager for session ${sessionToUse}: ${aiMessage.content.substring(0, 50)}...`);
this.broadcastMessageToClients(sessionToUse, aiMessage);
// Emit request state to enable input controls and show waiting indicator
this.emit('request-state-change', {
requestId,
sessionId: sessionToUse,
state: 'waiting_for_response',
message: params.message,
context: params.context,
timestamp: new Date().toISOString()
});
this.chatManager.addPendingRequest(sessionToUse, requestId, params);
this.requestResolvers.set(requestId, {
resolve: (response: string) => {
@@ -1458,6 +1457,15 @@ export class McpServer extends EventEmitter {
const responseTime = Date.now() - startTime;
this.debugLogger.log('TOOL', `Request ${requestId} completed with response:`, response);
// Emit request completed state to disable input controls and hide waiting indicator
this.emit('request-state-change', {
requestId,
sessionId: sessionToUse,
state: 'completed',
response: response,
timestamp: new Date().toISOString()
});
// Don't store human response as assistant message
// The 'response' here is the human's answer to AI's question
// The AI will generate its own response separately after receiving this
+36 -37
View File
@@ -1051,8 +1051,8 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
const data = JSON.parse(event.data);
console.log('SSE event received:', data);
if (data.type === 'human-agent-request') {
handleHumanAgentRequest(data.data);
if (data.type === 'request-state-change') {
handleRequestStateChange(data.data);
} else if (data.type === 'chat_message') {
handleIncomingChatMessage(data);
// Removed web_user_message auto-trigger - no longer needed
@@ -1076,39 +1076,18 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
// Global variable to store current request ID for responses
let currentPendingRequestId = null;
function handleHumanAgentRequest(data) {
console.log('Handling human agent request:', data);
function handleRequestStateChange(data) {
console.log('Handling request state change:', data);
// Store the request ID for sending response
currentPendingRequestId = data.requestId;
// Add the AI message to chat
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 ai-message';
const displayMessage = data.context ? \`\${data.context}\\n\\n\${data.message}\` : data.message;
messageDiv.innerHTML = \`
<div class="message-header">
<strong>AI Agent</strong>
<span class="timestamp">\${new Date(data.timestamp).toLocaleTimeString()}</span>
</div>
<div class="message-content">\${displayMessage.replace(/\\n/g, '<br>')}</div>
\`;
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
if (data.state === 'waiting_for_response') {
// Store the request ID for sending response
currentPendingRequestId = data.requestId;
// Enable input controls for response
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
if (messageInput && sendButton) {
messageInput.disabled = false;
sendButton.disabled = false;
@@ -1116,12 +1095,14 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
}
// Add waiting indicator if not present
const existingWaiting = messagesContainer.querySelector('.waiting-indicator');
if (!existingWaiting) {
const waitingDiv = document.createElement('div');
waitingDiv.className = 'waiting-indicator';
waitingDiv.textContent = '⏳ Waiting for your response...';
messagesContainer.appendChild(waitingDiv);
if (messagesContainer) {
const existingWaiting = messagesContainer.querySelector('.waiting-indicator');
if (!existingWaiting) {
const waitingDiv = document.createElement('div');
waitingDiv.className = 'waiting-indicator';
waitingDiv.textContent = '⏳ Waiting for your response...';
messagesContainer.appendChild(waitingDiv);
}
}
// Play notification
@@ -1132,6 +1113,24 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider {
setTimeout(() => {
document.body.classList.remove('flashing');
}, 2000);
} else if (data.state === 'completed') {
// Clear pending request
currentPendingRequestId = null;
// Disable input controls
if (messageInput && sendButton) {
messageInput.disabled = true;
sendButton.disabled = true;
}
// Remove waiting indicator
if (messagesContainer) {
const waitingIndicator = messagesContainer.querySelector('.waiting-indicator');
if (waitingIndicator) {
waitingIndicator.remove();
}
}
}
}