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

106 lines
3.5 KiB
JavaScript

'use strict';
const readline = require('readline');
const { visibleWidth, truncatePlain } = require('../utils/text_width');
function truncateNoEllipsis(text, maxCols) {
const out = truncatePlain(text, maxCols);
if (out.endsWith('...') && visibleWidth(text) > maxCols) {
return out.slice(0, -3);
}
return out;
}
function createStatusBar({ getTokens, maxTokens, getMaxTokens }) {
let mode = 'input';
let enabled = true;
let scrollApplied = false;
let lastRows = null;
const render = () => {
if (!enabled || !process.stdout.isTTY) return;
const cols = process.stdout.columns || 80;
const rows = process.stdout.rows || 24;
if (rows < 3) return;
const left = mode === 'running' ? '按下Esc停止' : '输入/查看所有指令';
const total = typeof getTokens === 'function' ? getTokens() : 0;
const formatCount = (value) => {
const num = Number(value) || 0;
if (num < 1000) return String(num);
const k = Math.round((num / 1000) * 10) / 10;
return `${k % 1 === 0 ? k.toFixed(0) : k.toFixed(1)}k`;
};
const maxValue = typeof getMaxTokens === 'function' ? getMaxTokens() : maxTokens;
const maxText = typeof maxValue === 'number' ? formatCount(maxValue) : (maxValue || '?');
const right = `当前上下文 ${formatCount(total)}/${maxText}`;
const rightTail = ` ${formatCount(total)}/${maxText}`;
const leftWidth = visibleWidth(left);
const rightWidth = visibleWidth(right);
const usableCols = Math.max(1, cols - 1);
let safeLine = '';
const availableLeft = usableCols - rightWidth - 1;
if (availableLeft >= 1) {
const displayLeft = truncateNoEllipsis(left, availableLeft);
const gap = usableCols - visibleWidth(displayLeft) - rightWidth;
safeLine = `${displayLeft}${' '.repeat(Math.max(0, gap))}${right}`;
} else {
safeLine = truncateNoEllipsis(right, usableCols);
}
process.stdout.write('\x1b7'); // save cursor
if (!scrollApplied || lastRows !== rows) {
process.stdout.write('\x1b[r'); // reset scroll region
process.stdout.write(`\x1b[1;${rows - 2}r`); // reserve last 2 lines
scrollApplied = true;
lastRows = rows;
}
// keep one blank spacer line above status line
readline.cursorTo(process.stdout, 0, rows - 2);
readline.clearLine(process.stdout, 0);
// render status line on last row
readline.cursorTo(process.stdout, 0, rows - 1);
readline.clearLine(process.stdout, 0);
process.stdout.write('\x1b[?7l'); // disable auto-wrap
process.stdout.write(safeLine);
process.stdout.write('\x1b[?7h'); // re-enable auto-wrap
process.stdout.write('\x1b8'); // restore cursor
};
const setMode = (nextMode) => {
if (mode === nextMode) return;
mode = nextMode;
render();
};
const setEnabled = (nextEnabled) => {
enabled = nextEnabled;
if (enabled) render();
};
const destroy = () => {
if (!process.stdout.isTTY) return;
if (scrollApplied) {
process.stdout.write('\x1b[r'); // reset scroll region
scrollApplied = false;
}
const rows = process.stdout.rows || 24;
if (rows >= 2) {
readline.cursorTo(process.stdout, 0, rows - 2);
readline.clearLine(process.stdout, 0);
}
if (rows >= 1) {
readline.cursorTo(process.stdout, 0, rows - 1);
readline.clearLine(process.stdout, 0);
}
};
if (process.stdout.isTTY) {
process.stdout.on('resize', () => render());
}
return { render, setMode, setEnabled, destroy };
}
module.exports = { createStatusBar };