UNPKG

termcode

Version:

Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative

309 lines (308 loc) 10.8 kB
import * as readline from "node:readline"; import { EventEmitter } from "node:events"; import { log } from "../util/logging.js"; /** * Real-time message steering system inspired by Claude Code * Allows sending messages to Claude while it's working to provide guidance */ export class RealtimeInputManager extends EventEmitter { isAIWorking = false; pendingMessages = []; messageQueue = []; currentTaskId; inputBuffer = ""; rl; nextMessageId = 1; constructor() { super(); this.setupKeyboardHandlers(); } /** * Initialize with readline interface */ initialize(rl) { this.rl = rl; this.setupRealtimeInput(); log.info("Real-time input manager initialized"); } /** * Signal that AI is starting to work */ startAITask(taskId, description) { this.isAIWorking = true; this.currentTaskId = taskId; this.pendingMessages = []; this.messageQueue = []; // Show real-time input instructions log.raw(""); log.raw(log.colors.dim("💡 While AI is working:")); log.raw(log.colors.dim(` ${log.colors.cyan("Enter")} - Queue message for AI`)); log.raw(log.colors.dim(` ${log.colors.cyan("Ctrl+C")} - Stop AI task`)); log.raw(log.colors.dim(` ${log.colors.cyan("Ctrl+G")} - Send urgent guidance`)); log.raw(""); this.emit('task-started', { taskId, description }); } /** * Signal that AI has finished working */ finishAITask(taskId) { if (taskId && taskId !== this.currentTaskId) return; this.isAIWorking = false; this.currentTaskId = undefined; // Process any pending messages if (this.pendingMessages.length > 0) { log.info(`Processing ${this.pendingMessages.length} queued messages...`); this.emit('messages-queued', this.pendingMessages); this.pendingMessages = []; } this.emit('task-finished', { taskId }); } /** * Add a message to be sent to AI */ queueMessage(content, priority = 'medium', type = 'steering') { const message = { id: `msg-${this.nextMessageId++}`, content: content.trim(), timestamp: Date.now(), priority, type }; if (this.isAIWorking) { // Add to pending messages for immediate processing this.pendingMessages.push(message); this.pendingMessages.sort((a, b) => { const priorityOrder = { urgent: 4, high: 3, medium: 2, low: 1 }; return priorityOrder[b.priority] - priorityOrder[a.priority]; }); log.info(`Queued ${priority} priority message for AI (${this.pendingMessages.length} total)`); // For urgent messages, interrupt AI immediately if (priority === 'urgent') { this.emit('urgent-message', message); } else { this.emit('message-queued', message); } } else { // Store for next AI task this.messageQueue.push(message); log.info(`Message stored for next AI task`); } return message.id; } /** * Get pending messages for AI */ getPendingMessages() { return [...this.pendingMessages]; } /** * Get queued messages for next task */ getQueuedMessages() { return [...this.messageQueue]; } /** * Clear specific message */ clearMessage(messageId) { const pendingIndex = this.pendingMessages.findIndex(m => m.id === messageId); if (pendingIndex !== -1) { this.pendingMessages.splice(pendingIndex, 1); return true; } const queueIndex = this.messageQueue.findIndex(m => m.id === messageId); if (queueIndex !== -1) { this.messageQueue.splice(queueIndex, 1); return true; } return false; } /** * Clear all messages */ clearAllMessages() { const totalCleared = this.pendingMessages.length + this.messageQueue.length; this.pendingMessages = []; this.messageQueue = []; return totalCleared; } /** * Process messages and convert to AI context */ formatMessagesForAI() { const messages = [...this.pendingMessages, ...this.messageQueue]; if (messages.length === 0) return ""; const messagesByType = messages.reduce((acc, msg) => { if (!acc[msg.type]) acc[msg.type] = []; acc[msg.type].push(msg); return acc; }, {}); let formatted = "\n\n## Real-time User Guidance:\n"; // Process by type and priority const typeOrder = ['stop', 'steering', 'clarification', 'guidance']; for (const type of typeOrder) { if (messagesByType[type]) { formatted += `\n### ${type.charAt(0).toUpperCase() + type.slice(1)}:\n`; messagesByType[type] .sort((a, b) => b.timestamp - a.timestamp) // Most recent first .forEach((msg, i) => { const timestamp = new Date(msg.timestamp).toLocaleTimeString(); formatted += `${i + 1}. [${timestamp}] ${msg.content}\n`; }); } } formatted += "\n*Please acknowledge and incorporate this guidance into your current work.*\n"; return formatted; } /** * Setup real-time input handling */ setupRealtimeInput() { if (!this.rl) return; // Handle Enter key for queuing messages const originalWrite = this.rl.write; this.rl.write = (data) => { if (this.isAIWorking && data === '\r') { this.handleRealtimeInput(); return true; } return originalWrite.call(this.rl, data); }; // Handle special key combinations this.rl.on('SIGINT', () => { if (this.isAIWorking) { this.queueMessage("Please stop the current task.", 'urgent', 'stop'); log.warn("Stop signal sent to AI"); } }); } /** * Setup keyboard handlers for special keys */ setupKeyboardHandlers() { if (process.stdin.isTTY) { process.stdin.setRawMode(true); process.stdin.on('data', (data) => { const key = data.toString(); // Handle Ctrl+G for urgent guidance if (key === '\x07' && this.isAIWorking) { // Ctrl+G this.handleUrgentInput(); return; } // Handle Ctrl+M for medium priority message if (key === '\x0D' && this.isAIWorking) { // Ctrl+M (Enter) this.handleRealtimeInput(); return; } }); } } /** * Handle real-time input while AI is working */ async handleRealtimeInput() { if (!this.rl || !this.isAIWorking) return; try { // Temporarily switch to non-raw mode for input if (process.stdin.isTTY) { process.stdin.setRawMode(false); } // Create temporary interface for input const tempRl = readline.createInterface({ input: process.stdin, output: process.stdout }); const message = await new Promise((resolve) => { tempRl.question(log.colors.cyan("💬 Message for AI: "), (answer) => { tempRl.close(); resolve(answer); }); }); if (message.trim()) { this.queueMessage(message, 'medium', 'steering'); } // Restore raw mode if (process.stdin.isTTY) { process.stdin.setRawMode(true); } } catch (error) { log.warn("Error handling real-time input:", error); } } /** * Handle urgent input with priority */ async handleUrgentInput() { if (!this.rl || !this.isAIWorking) return; try { if (process.stdin.isTTY) { process.stdin.setRawMode(false); } const tempRl = readline.createInterface({ input: process.stdin, output: process.stdout }); const message = await new Promise((resolve) => { tempRl.question(log.colors.red("🚨 Urgent guidance: "), (answer) => { tempRl.close(); resolve(answer); }); }); if (message.trim()) { this.queueMessage(message, 'urgent', 'guidance'); } if (process.stdin.isTTY) { process.stdin.setRawMode(true); } } catch (error) { log.warn("Error handling urgent input:", error); } } /** * Get statistics about message handling */ getStats() { const allMessages = [...this.pendingMessages, ...this.messageQueue]; const messagesByPriority = allMessages.reduce((acc, msg) => { acc[msg.priority] = (acc[msg.priority] || 0) + 1; return acc; }, {}); const messagesByType = allMessages.reduce((acc, msg) => { acc[msg.type] = (acc[msg.type] || 0) + 1; return acc; }, {}); return { isAIWorking: this.isAIWorking, pendingMessages: this.pendingMessages.length, queuedMessages: this.messageQueue.length, currentTask: this.currentTaskId, messagesByPriority, messagesByType }; } /** * Create a message template for common steering scenarios */ createSteeringTemplate(scenario) { const templates = { focus: "Please focus more on [specific aspect] and less on [other aspect]", clarify: "I need clarification on [specific point]. Please explain [what you need explained]", stop: "Please stop working on [current approach] and instead [new direction]", continue: "Great work! Please continue with [current approach] but also consider [additional aspect]", alternative: "Try a different approach: [describe alternative approach]" }; return templates[scenario]; } } // Export singleton instance export const realtimeInputManager = new RealtimeInputManager();