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
JavaScript
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();