capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
247 lines • 10.3 kB
JavaScript
import chalk from 'chalk';
import { TextProcessor } from './text-processor.js';
import { uiStateManager } from './ui-state-manager.js';
export class ConversationRenderer {
conversationBuffer = [];
addItem(item) {
this.conversationBuffer.push(item);
}
updateLastAssistantMessage(content) {
const lastIndex = this.conversationBuffer.length - 1;
if (lastIndex >= 0 && this.conversationBuffer[lastIndex].type === 'assistant') {
this.conversationBuffer[lastIndex].content = content;
return true;
}
return false;
}
getBuffer() {
return this.conversationBuffer;
}
clearBuffer() {
this.conversationBuffer.length = 0;
}
processBufferedOutput() {
const bufferedOutput = uiStateManager.flushBufferedOutput();
for (const output of bufferedOutput) {
const item = uiStateManager.formatBufferedOutput(output);
if (item) {
this.addItem(item);
}
}
}
addBufferedOutput(output) {
const item = uiStateManager.formatBufferedOutput(output);
if (item) {
this.addItem(item);
}
}
getLastAssistantIndex() {
for (let i = this.conversationBuffer.length - 1; i >= 0; i--) {
if (this.conversationBuffer[i].type === 'assistant') {
return i;
}
}
return -1;
}
formatItem(item) {
switch (item.type) {
case 'user':
return chalk.blue('> ') + item.content;
case 'assistant':
return chalk.green('◆ ') + item.content;
case 'tool-call':
return chalk.white(`⏺ ${item.metadata?.toolName}(${item.content})`);
case 'tool-result':
if (item.metadata?.toolName === 'Edit' && item.metadata?.success && item.metadata?.diffData) {
return this.formatFileEditDiff(item);
}
return item.metadata?.success
? chalk.gray(` ⎿ ✓ ${item.content}`)
: chalk.red(` ⎿ ✗ ${item.content}`);
case 'system':
return item.metadata?.isWelcomeHeader ? item.content : chalk.dim(item.content);
case 'error':
return chalk.red('✗ ') + item.content;
default:
return item.content;
}
}
getDisplayLines(terminalWidth) {
const displayLines = [];
this.conversationBuffer.forEach((item, index) => {
if (index > 0) {
const prevItem = this.conversationBuffer[index - 1];
if (this.shouldAddSpacing(prevItem, item)) {
displayLines.push('');
}
}
const content = String(item.content || '');
const contentLines = content.split('\n');
let isFirstLine = true;
for (const contentLine of contentLines) {
let linePrefix = '';
let indent = '';
if (item.type === 'assistant') {
linePrefix = isFirstLine ? chalk.green('◆ ') : ' ';
indent = ' ';
}
else if (item.type === 'system' && item.metadata?.isWelcomeHeader) {
linePrefix = '';
indent = '';
}
else {
linePrefix = isFirstLine ? this.formatItem({ ...item, content: '' }) : ' ';
indent = ' ';
}
const wrappedLines = TextProcessor.wrapText(contentLine, terminalWidth - TextProcessor.getVisibleLength(linePrefix));
for (let j = 0; j < wrappedLines.length; j++) {
const wrappedPrefix = j === 0 ? linePrefix : indent;
displayLines.push(wrappedPrefix + wrappedLines[j]);
}
isFirstLine = false;
}
});
return displayLines;
}
shouldAddSpacing(prevItem, currentItem) {
if (currentItem.type === 'user' || currentItem.type === 'assistant') {
return true;
}
if (currentItem.type === 'tool-call' && prevItem.type !== 'tool-call') {
return true;
}
return false;
}
formatMessageContent(content) {
let formatted = content;
formatted = formatted.replace(/^(#{2,3})\s+(.+)$/gm, (_match, _hashes, title) => chalk.bold(title));
formatted = formatted.replace(/^(\s*)-\s+/gm, (_match, spaces) => `${spaces}• `);
formatted = formatted.replace(/^(\s*)(\d+)\.\s+/gm, (_match, spaces, num) => `${spaces}${num}. `);
formatted = formatted.replace(/\*\*([^*]+)\*\*/g, (_match, text) => chalk.bold(text));
formatted = formatted.replace(/`([^`]+)`/g, (_match, code) => chalk.yellow(code));
const parts = formatted.split(/```(\w+)?\n?/);
let finalFormatted = '';
for (let index = 0; index < parts.length; index++) {
const part = parts[index];
if (index % 2 === 0) {
finalFormatted += part;
}
else if (/^\w+$/.test(part)) {
finalFormatted += chalk.gray('```' + part) + '\n';
}
else {
finalFormatted += chalk.yellow(part.trim()) + '\n' + chalk.gray('```');
}
}
return finalFormatted;
}
getConversationPreview(maxLength = 50) {
const firstUserMessage = this.conversationBuffer.find(item => item.type === 'user');
if (!firstUserMessage) {
return 'New Chat';
}
const preview = firstUserMessage.content
.replace(/\n/g, ' ')
.substring(0, maxLength);
return preview + (firstUserMessage.content.length > maxLength ? '...' : '');
}
getStats() {
const stats = {
messageCount: this.conversationBuffer.length,
userMessageCount: 0,
assistantMessageCount: 0,
toolCallCount: 0,
errorCount: 0
};
this.conversationBuffer.forEach(item => {
switch (item.type) {
case 'user':
stats.userMessageCount++;
break;
case 'assistant':
stats.assistantMessageCount++;
break;
case 'tool-call':
stats.toolCallCount++;
break;
case 'error':
stats.errorCount++;
break;
}
});
return stats;
}
formatFileEditDiff(item) {
const diff = item.metadata?.diffData;
if (!diff) {
return chalk.gray(` ⎿ ✓ ${item.content}`);
}
const lines = [];
lines.push(chalk.gray(` ⎿ `) + chalk.green(`Updated ${diff.filePath} with ${diff.additions} addition${diff.additions !== 1 ? 's' : ''} and ${diff.deletions} deletion${diff.deletions !== 1 ? 's' : ''}`));
const oldLines = diff.oldText.split('\n');
const newLines = diff.newText.split('\n');
const startLine = Math.max(1, diff.lineNumber - 2);
for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) {
const lineNum = startLine + i;
const oldLine = oldLines[i];
const newLine = newLines[i];
if (oldLine === newLine && oldLine !== undefined) {
lines.push(chalk.gray(` ${lineNum.toString().padStart(3)} `) + chalk.gray(oldLine));
}
else {
if (oldLine !== undefined && newLine === undefined) {
lines.push(chalk.red(` ${lineNum.toString().padStart(3)} - ${oldLine}`));
}
else if (oldLine === undefined && newLine !== undefined) {
lines.push(chalk.green(` ${lineNum.toString().padStart(3)} + ${newLine}`));
}
else if (oldLine !== undefined && newLine !== undefined) {
lines.push(chalk.red(` ${lineNum.toString().padStart(3)} - ${oldLine}`));
lines.push(chalk.green(` ${lineNum.toString().padStart(3)} + ${newLine}`));
}
}
}
return lines.join('\n');
}
exportToMarkdown() {
let markdown = '# Conversation Export\n\n';
markdown += `_Exported on ${new Date().toLocaleString()}_\n\n`;
markdown += '---\n\n';
this.conversationBuffer.forEach((item, index) => {
const timestamp = item.timestamp.toLocaleTimeString();
switch (item.type) {
case 'user':
markdown += `### User [${timestamp}]\n\n${item.content}\n\n`;
break;
case 'assistant':
markdown += `### Assistant [${timestamp}]`;
if (item.metadata?.model) {
markdown += ` (${item.metadata.model})`;
}
markdown += `\n\n${item.content}\n\n`;
break;
case 'tool-call':
markdown += `#### Tool Call: ${item.metadata?.toolName} [${timestamp}]\n`;
markdown += `\`\`\`\n${item.content}\n\`\`\`\n\n`;
break;
case 'tool-result':
markdown += `#### Tool Result [${timestamp}]\n`;
markdown += `Status: ${item.metadata?.success ? '✓ Success' : '✗ Failed'}\n`;
markdown += `\`\`\`\n${item.content}\n\`\`\`\n\n`;
break;
case 'system':
markdown += `_System: ${item.content}_\n\n`;
break;
case 'error':
markdown += `**Error [${timestamp}]:** ${item.content}\n\n`;
break;
}
if (index < this.conversationBuffer.length - 1) {
markdown += '---\n\n';
}
});
return markdown;
}
}
export const conversationRenderer = new ConversationRenderer();
//# sourceMappingURL=conversation-renderer.js.map