-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils.js
More file actions
161 lines (139 loc) · 5.08 KB
/
utils.js
File metadata and controls
161 lines (139 loc) · 5.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// Utility functions for the AI Agent system
/**
* Estimate token count for text
* Rough approximation: ~4 characters per token
* @param {string} text - The text to estimate tokens for
* @returns {number} Estimated number of tokens
*/
function estimateTokens(text) {
if (!text) return 0;
return Math.ceil(text.length / 4);
}
/**
* Calculate the visual width of text in terminal
* Accounts for emojis and wide characters that take up 2 terminal cells
* Also strips ANSI escape codes before measuring
* @param {string} str - The text to measure
* @returns {number} Visual width in terminal cells
*/
function getVisualWidth(str) {
if (!str) return 0;
// Remove ANSI escape codes
const ansiRegex = /\x1b\[[0-9;]*m/g;
const cleanStr = str.replace(ansiRegex, '');
let width = 0;
for (const char of cleanStr) {
const code = char.codePointAt(0);
// Check for emoji ranges and wide characters
// Most emojis are in these ranges and take up 2 cells
if (
(code >= 0x1F300 && code <= 0x1F9FF) || // Emoticons, symbols, pictographs
(code >= 0x1F600 && code <= 0x1F64F) || // Emoticons
(code >= 0x1F680 && code <= 0x1F6FF) || // Transport and map symbols
(code >= 0x2600 && code <= 0x26FF) || // Miscellaneous symbols
(code >= 0x2700 && code <= 0x27BF) || // Dingbats
(code >= 0xFE00 && code <= 0xFE0F) || // Variation selectors
(code >= 0x1F900 && code <= 0x1F9FF) || // Supplemental Symbols and Pictographs
(code >= 0x1F1E6 && code <= 0x1F1FF) // Regional indicator symbols (flags)
) {
width += 2; // Emojis take 2 cells
} else if (code >= 0x10000) {
width += 2; // Other supplementary characters
} else {
width += 1; // Normal ASCII and basic characters
}
}
return width;
}
/**
* Pad text to a specific visual width
* Uses getVisualWidth to correctly handle emojis and ANSI codes
* @param {string} text - The text to pad
* @param {number} width - Target visual width
* @returns {string} Padded text
*/
function padToVisualWidth(text, width) {
const visualLength = getVisualWidth(text);
const padding = Math.max(0, width - visualLength);
return text + ' '.repeat(padding);
}
/**
* Wrap text to fit within a specific width
* Preserves ANSI escape codes and handles emojis correctly
* @param {string} text - The text to wrap
* @param {number} width - Maximum visual width per line
* @returns {string[]} Array of wrapped lines
*/
function wrapText(text, width) {
if (!text) return [''];
if (width <= 0) return [text];
const lines = [];
const ansiRegex = /\x1b\[[0-9;]*m/g;
// Split by newlines first to preserve explicit line breaks
const paragraphs = text.split('\n');
for (const paragraph of paragraphs) {
if (getVisualWidth(paragraph) <= width) {
lines.push(paragraph);
continue;
}
// Extract ANSI codes and their positions
let cleanText = '';
let ansiCodes = [];
let lastIndex = 0;
let match;
const regex = new RegExp(ansiRegex);
while ((match = regex.exec(paragraph)) !== null) {
cleanText += paragraph.slice(lastIndex, match.index);
ansiCodes.push({ pos: cleanText.length, code: match[0] });
lastIndex = match.index + match[0].length;
}
cleanText += paragraph.slice(lastIndex);
// Wrap the clean text
let currentLine = '';
let currentWidth = 0;
let charIndex = 0;
// Track active ANSI codes to carry them across lines
let activeAnsi = '';
for (const char of cleanText) {
// Check for ANSI codes at this position
const codesAtPos = ansiCodes.filter(a => a.pos === charIndex);
for (const { code } of codesAtPos) {
currentLine += code;
// Track active formatting
if (code.includes('[0m')) {
activeAnsi = ''; // Reset
} else {
activeAnsi += code;
}
}
const charWidth = getVisualWidth(char);
// If adding this char would exceed width, start new line
if (currentWidth + charWidth > width && currentLine) {
// Reset formatting at end of line if needed
if (activeAnsi) {
currentLine += '\x1b[0m';
}
lines.push(currentLine);
currentLine = activeAnsi; // Carry formatting to next line
currentWidth = 0;
}
currentLine += char;
currentWidth += charWidth;
charIndex++;
}
// Add any remaining text
if (currentLine) {
if (activeAnsi) {
currentLine += '\x1b[0m';
}
lines.push(currentLine);
}
}
return lines.length > 0 ? lines : [''];
}
module.exports = {
estimateTokens,
getVisualWidth,
padToVisualWidth,
wrapText
};