termcode
Version:
Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative
220 lines (219 loc) • 7.87 kB
JavaScript
import { EventEmitter } from "node:events";
import { getProvider } from "../providers/index.js";
import { log } from "../util/logging.js";
export class StreamProcessor extends EventEmitter {
buffer = "";
isStreaming = false;
cancelled = false;
startTime = 0;
tokensGenerated = 0;
updateTimer;
currentLine = "";
config;
constructor(config = {}) {
super();
this.config = {
showCursor: true,
colorizeCode: true,
showTokenCount: true,
bufferSize: 10,
updateInterval: 50,
...config
};
}
async streamChat(provider, model, messages, options = {}) {
if (this.isStreaming) {
throw new Error("Stream already active");
}
this.isStreaming = true;
this.cancelled = false;
this.buffer = "";
this.tokensGenerated = 0;
this.startTime = Date.now();
this.currentLine = "";
this.emit("start");
try {
// Check if provider supports streaming
const providerInstance = getProvider(provider);
if (providerInstance.streamChat) {
return await this.handleNativeStreaming(providerInstance, model, messages, options);
}
else {
return await this.handlePolledStreaming(providerInstance, model, messages, options);
}
}
catch (error) {
this.isStreaming = false;
this.emit("error", error);
throw error;
}
}
async handleNativeStreaming(provider, model, messages, options) {
return new Promise((resolve, reject) => {
const stream = provider.streamChat(messages, { model, ...options });
stream.on('data', (chunk) => {
if (this.cancelled) {
stream.destroy();
return;
}
this.processChunk(chunk);
});
stream.on('end', () => {
this.finishStreaming();
resolve(this.buffer);
});
stream.on('error', (error) => {
this.isStreaming = false;
this.clearUpdateTimer();
reject(error);
});
this.startUpdateTimer();
});
}
async handlePolledStreaming(provider, model, messages, options) {
// Fallback: simulate streaming by breaking up the response
const fullResponse = await provider.chat(messages, { model, ...options });
return new Promise((resolve) => {
const words = fullResponse.split(' ');
let wordIndex = 0;
const simulateStream = () => {
if (this.cancelled || wordIndex >= words.length) {
this.finishStreaming();
resolve(this.buffer);
return;
}
const word = words[wordIndex];
this.processChunk(word + ' ');
wordIndex++;
// Random delay to simulate realistic streaming
const delay = Math.random() * 100 + 20;
setTimeout(simulateStream, delay);
};
this.startUpdateTimer();
simulateStream();
});
}
processChunk(chunk) {
this.buffer += chunk;
this.tokensGenerated += this.estimateTokens(chunk);
// Add to current line for display
this.currentLine += chunk;
// If we have a newline, process the complete line
if (chunk.includes('\n')) {
const lines = this.currentLine.split('\n');
this.currentLine = lines.pop() || "";
for (const line of lines.slice(0, -1)) {
this.displayLine(line);
}
}
this.emit("chunk", chunk);
this.emit("stats", this.getStats());
}
displayLine(line) {
if (this.config.colorizeCode) {
// Simple syntax highlighting
line = this.highlightSyntax(line);
}
// Clear current line and write new content
process.stdout.write('\r\x1b[K');
process.stdout.write(line + '\n');
}
startUpdateTimer() {
this.updateTimer = setInterval(() => {
this.updateDisplay();
}, this.config.updateInterval);
}
clearUpdateTimer() {
if (this.updateTimer) {
clearInterval(this.updateTimer);
this.updateTimer = undefined;
}
}
updateDisplay() {
if (!this.isStreaming)
return;
const stats = this.getStats();
const cursor = this.config.showCursor ? (Date.now() % 1000 > 500 ? '▊' : ' ') : '';
// Update current line with cursor
process.stdout.write('\r\x1b[K');
process.stdout.write(this.currentLine + cursor);
// Show stats in a separate line if enabled
if (this.config.showTokenCount) {
const statsLine = `\n${log.colors.dim(`Tokens: ${stats.tokensGenerated} | Speed: ${stats.tokensPerSecond.toFixed(1)}/s | Time: ${(stats.timeElapsed / 1000).toFixed(1)}s`)}`;
process.stdout.write(statsLine);
process.stdout.write('\x1b[1A'); // Move cursor back up
}
}
finishStreaming() {
this.isStreaming = false;
this.clearUpdateTimer();
// Display final line
if (this.currentLine) {
this.displayLine(this.currentLine);
}
// Clear cursor and show final stats
process.stdout.write('\r\x1b[K');
const stats = this.getStats();
this.emit("complete", {
content: this.buffer,
stats
});
if (this.config.showTokenCount) {
const finalStats = `\n${log.colors.green(`✓ Generated ${stats.tokensGenerated} tokens in ${(stats.timeElapsed / 1000).toFixed(2)}s (${stats.tokensPerSecond.toFixed(1)} tokens/s)`)}`;
process.stdout.write(finalStats + '\n');
}
}
highlightSyntax(line) {
// Simple syntax highlighting for common languages
return line
.replace(/\b(function|const|let|var|class|interface|type)\b/g, log.colors.magenta('$1'))
.replace(/\b(if|else|for|while|return|import|export)\b/g, log.colors.blue('$1'))
.replace(/(['"`])((?:(?!\1)[^\\]|\\.)*)(\1)/g, log.colors.green('$1$2$3'))
.replace(/\/\/.*$/g, log.colors.dim('$&'))
.replace(/\/\*[\s\S]*?\*\//g, log.colors.dim('$&'));
}
estimateTokens(text) {
// Simple token estimation
return Math.ceil(text.length / 4);
}
getStats() {
const timeElapsed = Date.now() - this.startTime;
const tokensPerSecond = timeElapsed > 0 ? (this.tokensGenerated / timeElapsed) * 1000 : 0;
return {
tokensGenerated: this.tokensGenerated,
timeElapsed,
tokensPerSecond
};
}
cancel() {
if (!this.isStreaming)
return;
this.cancelled = true;
this.isStreaming = false;
this.clearUpdateTimer();
process.stdout.write('\r\x1b[K');
log.warn("Generation cancelled by user");
this.emit("cancelled", {
partialContent: this.buffer,
stats: this.getStats()
});
}
isActive() {
return this.isStreaming;
}
}
// Global stream processor instance
let globalStreamProcessor = null;
export function getStreamProcessor() {
if (!globalStreamProcessor) {
globalStreamProcessor = new StreamProcessor();
}
return globalStreamProcessor;
}
export function cancelCurrentStream() {
if (globalStreamProcessor && globalStreamProcessor.isActive()) {
globalStreamProcessor.cancel();
return true;
}
return false;
}