agent-Specialization/easyagent/src/ui/spinner.js

152 lines
5.3 KiB
JavaScript

'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 };