'use strict'; const readline = require('readline'); const { gray } = require('../utils/colors'); const FRAMES = ['⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; class Spinner { constructor(indent = '', leadingLines = 0) { this.timer = null; this.index = 0; this.textProvider = () => ''; this.active = false; this.indent = indent; this.leadingLines = Math.max(0, Number(leadingLines) || 0); this.leadingWritten = false; this.thinkingLineReady = false; } setIndent(indent) { this.indent = indent || ''; } start(textProvider) { this.textProvider = textProvider || (() => ''); this.active = true; this.thinkingLineReady = false; if (this.leadingLines > 0 && !this.leadingWritten) { process.stdout.write('\n'.repeat(this.leadingLines)); this.leadingWritten = true; } this.render(); this.timer = setInterval(() => this.render(), 120); } ensureThinkingLine() { if (this.thinkingLineReady) return; process.stdout.write('\n'); this.thinkingLineReady = true; readline.moveCursor(process.stdout, 0, -1); readline.cursorTo(process.stdout, 0); } render() { readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0); const frame = FRAMES[this.index % FRAMES.length]; this.index += 1; const provided = this.textProvider(frame); let suffix = ''; let thinking = ''; let colorThinking = false; let hasThinkingLine = false; if (typeof provided === 'string') { suffix = provided; } else if (provided && typeof provided === 'object') { suffix = provided.label || ''; thinking = provided.thinking || ''; colorThinking = !!provided.colorThinking; hasThinkingLine = Object.prototype.hasOwnProperty.call(provided, 'thinking'); } const linePrefix = `${this.indent}${frame}${suffix}`; process.stdout.write(linePrefix); if (hasThinkingLine) { this.ensureThinkingLine(); const width = process.stdout.columns || 80; let visibleThinking = thinking; const maxThinking = Math.max(0, width - this.indent.length - 1); if (visibleThinking && maxThinking > 0 && visibleThinking.length > maxThinking) { visibleThinking = visibleThinking.slice(0, maxThinking); } if (visibleThinking && maxThinking === 0) visibleThinking = ''; const line = visibleThinking ? (colorThinking ? gray(visibleThinking) : visibleThinking) : ''; const leadSpaces = (suffix.match(/^\s*/) || [''])[0].length; const alignCol = this.indent.length + frame.length + leadSpaces; readline.moveCursor(process.stdout, 0, 1); readline.cursorTo(process.stdout, alignCol); readline.clearLine(process.stdout, 0); process.stdout.write(line); readline.moveCursor(process.stdout, 0, -1); readline.cursorTo(process.stdout, 0); } } stop(finalText) { if (this.timer) clearInterval(this.timer); this.timer = null; this.active = false; readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0); const provided = this.textProvider(''); let thinking = ''; let colorThinking = false; let hasThinkingLine = false; if (provided && typeof provided === 'object' && Object.prototype.hasOwnProperty.call(provided, 'thinking')) { thinking = provided.thinking || ''; colorThinking = !!provided.colorThinking; hasThinkingLine = true; } const linePrefix = `${this.indent}${finalText}`; process.stdout.write(linePrefix); if (hasThinkingLine) { this.ensureThinkingLine(); const width = process.stdout.columns || 80; let visibleThinking = thinking; const maxThinking = Math.max(0, width - this.indent.length - 1); if (visibleThinking && maxThinking > 0 && visibleThinking.length > maxThinking) { visibleThinking = visibleThinking.slice(0, maxThinking); } if (visibleThinking && maxThinking === 0) visibleThinking = ''; const line = visibleThinking ? (colorThinking ? gray(visibleThinking) : visibleThinking) : ''; let alignCol = this.indent.length; const firstNonSpace = finalText.search(/\S/); if (firstNonSpace >= 0) { const afterFirst = finalText.slice(firstNonSpace + 1); const spaceAfter = (afterFirst.match(/^\s*/) || [''])[0].length; alignCol = this.indent.length + firstNonSpace + 1 + spaceAfter; } readline.moveCursor(process.stdout, 0, 1); readline.cursorTo(process.stdout, alignCol); readline.clearLine(process.stdout, 0); process.stdout.write(line); process.stdout.write('\n'); } else { process.stdout.write('\n'); } this.thinkingLineReady = false; } stopSilent() { if (this.timer) clearInterval(this.timer); this.timer = null; this.active = false; this.thinkingLineReady = false; readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0); } } function truncateThinking(text, max = 50) { const sanitized = String(text || '').replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim(); if (sanitized.length <= max) return sanitized; return sanitized.slice(0, max) + '...'; } function formatThinkingLine(text) { if (!text) return ''; return gray(truncateThinking(text)); } module.exports = { Spinner, formatThinkingLine, truncateThinking };