cowork-cli 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/configs/config.json +1 -1
- package/src/configs/sys.txt +9 -2
- package/src/utils/logger.js +2 -2
- package/src/utils/outputFormatter.js +439 -27
- package/src/utils/ui.js +1 -1
package/README.md
CHANGED
|
@@ -53,7 +53,8 @@ cwk "Explain the data flow in the engine/ models"
|
|
|
53
53
|
|
|
54
54
|
## ✨ Features that Matter
|
|
55
55
|
|
|
56
|
-
- **Zero-Whitespace UI**: High-density terminal output designed for professionals. No fluff,
|
|
56
|
+
- **Zero-Whitespace UI**: High-density terminal output designed for professionals. No fluff, just clean, ansi-balanced structured layouts.
|
|
57
|
+
- **Rich Terminal Markdown Rendering**: Custom lightweight layout engine supporting responsive headers, bullets, blockquotes, code blocks with clean borders, horizontal rules, and tables, with full CJK visual character column matching.
|
|
57
58
|
- **Interactive Feedback**: The AI can request clarifications via the `askUser` tool or trigger an interactive `[ Yes ] No` toggle using `askConfirm`.
|
|
58
59
|
- **Smart Discovery**: Built-in `searchText`, `findFile`, and `projectTree` tools that respect your `.gitignore`.
|
|
59
60
|
- **Web Research**: Dynamically search the web (`webSearch`) and read documentation (`webFetch`) directly from the CLI.
|
package/package.json
CHANGED
package/src/configs/config.json
CHANGED
package/src/configs/sys.txt
CHANGED
|
@@ -8,8 +8,15 @@ You are Cowork (cwk), an interactive CLI tool acting as a Full Engineering Read-
|
|
|
8
8
|
|
|
9
9
|
# CONSTRAINTS (Terminal Environment)
|
|
10
10
|
- NO conversational filler, pleasantries, preambles, or postambles.
|
|
11
|
-
-
|
|
12
|
-
|
|
11
|
+
- Use ONLY the following supported Markdown syntax elements:
|
|
12
|
+
* Headers: # (H1), ## (H2), ### (H3)
|
|
13
|
+
* Lists: starting with -, *, + (bullets) or 1. (numbers)
|
|
14
|
+
* Blockquotes: starting with >
|
|
15
|
+
* Fenced code blocks: using ```
|
|
16
|
+
* Inline styles: **bold**, *italic* (or _italic_), `inline code`
|
|
17
|
+
* Links: [label](url)
|
|
18
|
+
* Horizontal rules: ---
|
|
19
|
+
* Tables: standard markdown tables using | and -
|
|
13
20
|
- Cite code locations using: file_path:line_number.
|
|
14
21
|
|
|
15
22
|
# EXECUTION & STYLE
|
package/src/utils/logger.js
CHANGED
|
@@ -16,7 +16,7 @@ try {
|
|
|
16
16
|
data: '#C2C6C5',
|
|
17
17
|
success: '#7AC391',
|
|
18
18
|
error: '#E07070',
|
|
19
|
-
dim: '#
|
|
19
|
+
dim: '#8F9399',
|
|
20
20
|
header: '#A37ACC',
|
|
21
21
|
}
|
|
22
22
|
};
|
|
@@ -31,7 +31,7 @@ function hexToAnsi(hex) {
|
|
|
31
31
|
|
|
32
32
|
const reset = '\x1b[0m';
|
|
33
33
|
|
|
34
|
-
const colors = {
|
|
34
|
+
export const colors = {
|
|
35
35
|
main: hexToAnsi(config.accents.main),
|
|
36
36
|
tool: hexToAnsi(config.accents.tool),
|
|
37
37
|
data: hexToAnsi(config.accents.data),
|
|
@@ -1,3 +1,93 @@
|
|
|
1
|
+
import { colors } from './logger.js';
|
|
2
|
+
|
|
3
|
+
const reset = '\x1b[0m';
|
|
4
|
+
|
|
5
|
+
function stripAnsi(str) {
|
|
6
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function visualLength(str) {
|
|
10
|
+
if (!str) return 0;
|
|
11
|
+
const clean = stripAnsi(str);
|
|
12
|
+
let len = 0;
|
|
13
|
+
for (let i = 0; i < clean.length; i++) {
|
|
14
|
+
const code = clean.charCodeAt(i);
|
|
15
|
+
if ((code >= 0x3000 && code <= 0x9FFF) || (code >= 0xFF00 && code <= 0xFFEF)) {
|
|
16
|
+
len += 2;
|
|
17
|
+
} else {
|
|
18
|
+
len += 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return len;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function applyInlineStyles(str) {
|
|
25
|
+
if (!str) return '';
|
|
26
|
+
|
|
27
|
+
// 1. Inline code: `code` -> tool color (amber) + code + reset foreground color
|
|
28
|
+
str = str.replace(/`([^`]+)`/g, (match, code) => {
|
|
29
|
+
return `${colors.tool}${code}\x1b[39m`;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 2. Bold styling: **text** -> bold terminal style + main color (blue) + text + reset
|
|
33
|
+
str = str.replace(/\*\*([^*]+)\*\*/g, (match, boldText) => {
|
|
34
|
+
return `\x1b[1m${colors.main}${boldText}\x1b[22m\x1b[39m`;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 3. Italic/dim styling: *text* or _text_ -> dim color (grey) + text + reset foreground color
|
|
38
|
+
str = str.replace(/\*([^*]+)\*/g, (match, italicText) => {
|
|
39
|
+
return `${colors.dim}${italicText}\x1b[39m`;
|
|
40
|
+
});
|
|
41
|
+
str = str.replace(/_([^_]+)_/g, (match, italicText) => {
|
|
42
|
+
return `${colors.dim}${italicText}\x1b[39m`;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 4. Link styling: [label](url) -> label (main/blue) + dim url (grey)
|
|
46
|
+
str = str.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, label, url) => {
|
|
47
|
+
return `${colors.main}${label}\x1b[39m ${colors.dim}(${url})\x1b[39m`;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return str;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function balanceAnsiStyles(wrappedLines) {
|
|
54
|
+
let activeColor = null;
|
|
55
|
+
|
|
56
|
+
return wrappedLines.map(line => {
|
|
57
|
+
let prefix = '';
|
|
58
|
+
if (activeColor) prefix += activeColor;
|
|
59
|
+
|
|
60
|
+
const ansiRegex = /\x1b\[([0-9;]*)m/g;
|
|
61
|
+
let match;
|
|
62
|
+
while ((match = ansiRegex.exec(line)) !== null) {
|
|
63
|
+
const code = match[1];
|
|
64
|
+
const parts = code.split(';');
|
|
65
|
+
|
|
66
|
+
if (parts.includes('0') || parts.includes('39')) {
|
|
67
|
+
activeColor = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < parts.length; i++) {
|
|
71
|
+
const part = parts[i];
|
|
72
|
+
if (part === '38') {
|
|
73
|
+
if (parts[i + 1] === '2' || parts[i + 1] === '5') {
|
|
74
|
+
activeColor = match[0];
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
} else if (/^[39][0-9]$/.test(part) && part !== '39') {
|
|
78
|
+
activeColor = match[0];
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let suffix = '';
|
|
85
|
+
if (activeColor) suffix += '\x1b[39m';
|
|
86
|
+
|
|
87
|
+
return prefix + line + suffix;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
1
91
|
/**
|
|
2
92
|
* Wraps text to the current terminal width without breaking words unless necessary.
|
|
3
93
|
* Dynamically detects terminal width and preserves whitespace/indentation.
|
|
@@ -6,52 +96,205 @@
|
|
|
6
96
|
* @returns {string} The formatted text.
|
|
7
97
|
*/
|
|
8
98
|
export function outputFormatted(text, overrideWidth) {
|
|
9
|
-
if (
|
|
99
|
+
if (text === null || text === undefined) return '';
|
|
100
|
+
// Normalize newlines, strip carriage returns, and expand tabs to preserve column alignment
|
|
101
|
+
const strText = String(text).replace(/\r/g, '').replace(/\t/g, ' ');
|
|
10
102
|
|
|
11
|
-
|
|
12
|
-
|
|
103
|
+
let rawWidth = overrideWidth || process.stdout.columns || 80;
|
|
104
|
+
if (typeof rawWidth !== 'number' || isNaN(rawWidth) || rawWidth < 10) {
|
|
105
|
+
rawWidth = 80;
|
|
106
|
+
}
|
|
13
107
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
108
|
+
// Use full terminal width with a small 2-char right margin to prevent
|
|
109
|
+
// edge-wrapping artifacts on terminals that wrap at the last column.
|
|
110
|
+
let width = Math.max(10, rawWidth - 2);
|
|
111
|
+
const lines = strText.split('\n');
|
|
112
|
+
const wrappedLines = [];
|
|
113
|
+
|
|
114
|
+
let inCodeBlock = false;
|
|
115
|
+
let tableBuffer = []; // accumulates raw pipe-delimited rows
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Table renderer — called when a table block is complete
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
function renderTable(rawRows) {
|
|
121
|
+
// Parse each row into trimmed cell strings
|
|
122
|
+
const parsed = rawRows.map(row => {
|
|
123
|
+
const cells = row.split('|');
|
|
124
|
+
// strip the leading/trailing empty strings caused by outer pipes
|
|
125
|
+
if (cells[0].trim() === '') cells.shift();
|
|
126
|
+
if (cells[cells.length - 1].trim() === '') cells.pop();
|
|
127
|
+
return cells.map(c => c.trim());
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (parsed.length === 0) return;
|
|
131
|
+
|
|
132
|
+
// Identify the separator row (only dashes/colons/pipes) and extract alignment
|
|
133
|
+
let sepIdx = parsed.findIndex(row =>
|
|
134
|
+
row.every(c => /^:?-+:?$/.test(c))
|
|
135
|
+
);
|
|
136
|
+
if (sepIdx === -1) sepIdx = 1; // assume row 1 is separator if not found
|
|
137
|
+
|
|
138
|
+
const alignRow = parsed[sepIdx] || [];
|
|
139
|
+
const aligns = alignRow.map(c => {
|
|
140
|
+
if (/^:-+:$/.test(c)) return 'center';
|
|
141
|
+
if (/^-+:$/.test(c)) return 'right';
|
|
142
|
+
return 'left';
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const headerRows = parsed.slice(0, sepIdx);
|
|
146
|
+
const dataRows = parsed.slice(sepIdx + 1);
|
|
147
|
+
const allDataRows = [...headerRows, ...dataRows];
|
|
148
|
+
|
|
149
|
+
// Determine number of columns
|
|
150
|
+
const numCols = Math.max(...allDataRows.map(r => r.length), aligns.length);
|
|
151
|
+
|
|
152
|
+
// Compute natural column widths (visual length of cell content)
|
|
153
|
+
const colWidths = Array(numCols).fill(0);
|
|
154
|
+
for (const row of allDataRows) {
|
|
155
|
+
for (let i = 0; i < numCols; i++) {
|
|
156
|
+
const cell = row[i] || '';
|
|
157
|
+
const cellLen = visualLength(stripAnsi(applyInlineStyles(cell)));
|
|
158
|
+
if (cellLen > colWidths[i]) colWidths[i] = cellLen;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Cap table to available width: shrink columns proportionally if needed
|
|
163
|
+
// Total width = borders(numCols+1) + padding(numCols*2) + sum(colWidths)
|
|
164
|
+
const borderOverhead = (numCols + 1) + (numCols * 2);
|
|
165
|
+
const totalNatural = colWidths.reduce((a, b) => a + b, 0) + borderOverhead;
|
|
166
|
+
if (totalNatural > width) {
|
|
167
|
+
const budget = width - borderOverhead;
|
|
168
|
+
const ratio = budget / colWidths.reduce((a, b) => a + b, 0);
|
|
169
|
+
for (let i = 0; i < numCols; i++) {
|
|
170
|
+
colWidths[i] = Math.max(1, Math.floor(colWidths[i] * ratio));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Helper: pad/truncate a cell to the target visual width
|
|
175
|
+
function fitCell(rawText, colW, align) {
|
|
176
|
+
const styled = applyInlineStyles(rawText || '');
|
|
177
|
+
const visLen = visualLength(stripAnsi(styled));
|
|
178
|
+
const overflow = visLen - colW;
|
|
179
|
+
let content = styled;
|
|
180
|
+
if (overflow > 0) {
|
|
181
|
+
// Truncate the raw text first, then re-style
|
|
182
|
+
const plain = stripAnsi(styled);
|
|
183
|
+
const cut = plain.slice(0, plain.length - overflow - 1) + '…';
|
|
184
|
+
content = applyInlineStyles(rawText.slice(0, cut.length));
|
|
185
|
+
}
|
|
186
|
+
const padTotal = colW - Math.min(visLen, colW);
|
|
187
|
+
if (align === 'right') {
|
|
188
|
+
return ' '.repeat(padTotal) + content;
|
|
189
|
+
} else if (align === 'center') {
|
|
190
|
+
const left = Math.floor(padTotal / 2);
|
|
191
|
+
const right = padTotal - left;
|
|
192
|
+
return ' '.repeat(left) + content + ' '.repeat(right);
|
|
193
|
+
} else {
|
|
194
|
+
return content + ' '.repeat(padTotal);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const D = colors.dim;
|
|
199
|
+
const R = reset;
|
|
200
|
+
|
|
201
|
+
// Box-drawing helpers
|
|
202
|
+
const hbar = (w) => '─'.repeat(w + 2); // col width + 2 padding
|
|
203
|
+
|
|
204
|
+
// ┌─...─┬─...─┐
|
|
205
|
+
const topBorder = D + '┌' + colWidths.map(hbar).join('┬') + '┐' + R;
|
|
206
|
+
// ├─...─┼─...─┤ (after header)
|
|
207
|
+
const midBorder = D + '├' + colWidths.map(hbar).join('┼') + '┤' + R;
|
|
208
|
+
// └─...─┴─...─┘
|
|
209
|
+
const botBorder = D + '└' + colWidths.map(hbar).join('┴') + '┘' + R;
|
|
210
|
+
|
|
211
|
+
function buildRow(cells, isHeader) {
|
|
212
|
+
const parts = colWidths.map((w, i) => {
|
|
213
|
+
const raw = cells[i] || '';
|
|
214
|
+
const align = aligns[i] || 'left';
|
|
215
|
+
const fitted = fitCell(raw, w, align);
|
|
216
|
+
return isHeader
|
|
217
|
+
? ` \x1b[1m${colors.main}${stripAnsi(fitted)}\x1b[22m${R} `
|
|
218
|
+
: ` ${fitted} `;
|
|
219
|
+
});
|
|
220
|
+
return D + '│' + R + parts.join(D + '│' + R) + D + '│' + R;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Add blank line before table if previous line is not blank
|
|
224
|
+
if (wrappedLines.length > 0 && wrappedLines[wrappedLines.length - 1] !== '') {
|
|
225
|
+
wrappedLines.push('');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
wrappedLines.push(topBorder);
|
|
229
|
+
for (const hrow of headerRows) {
|
|
230
|
+
wrappedLines.push(buildRow(hrow, true));
|
|
231
|
+
}
|
|
232
|
+
if (dataRows.length > 0) {
|
|
233
|
+
wrappedLines.push(midBorder);
|
|
234
|
+
for (const drow of dataRows) {
|
|
235
|
+
wrappedLines.push(buildRow(drow, false));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
wrappedLines.push(botBorder);
|
|
239
|
+
wrappedLines.push('');
|
|
240
|
+
}
|
|
17
241
|
|
|
18
|
-
|
|
19
|
-
|
|
242
|
+
// Helper function to wrap a string to a specific width (ANSI and wide-char aware)
|
|
243
|
+
function wrapString(content, wrapWidth) {
|
|
244
|
+
const safeWidth = Math.max(5, wrapWidth);
|
|
245
|
+
if (visualLength(content) <= safeWidth) return [content];
|
|
246
|
+
|
|
247
|
+
const tokens = content.split(/(\s+)/);
|
|
20
248
|
const result = [];
|
|
21
249
|
let currentLine = '';
|
|
22
250
|
|
|
23
251
|
for (const token of tokens) {
|
|
24
252
|
if (!token) continue;
|
|
25
253
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
254
|
+
const tokenLen = visualLength(token);
|
|
255
|
+
const currentLineLen = visualLength(currentLine);
|
|
256
|
+
|
|
257
|
+
if (currentLineLen + tokenLen > safeWidth) {
|
|
258
|
+
// If it's a long word that exceeds the entire width
|
|
259
|
+
if (tokenLen > safeWidth && !/^\s+$/.test(token)) {
|
|
32
260
|
if (currentLine) {
|
|
33
261
|
result.push(currentLine.trimEnd());
|
|
34
262
|
currentLine = '';
|
|
35
263
|
}
|
|
36
264
|
|
|
37
|
-
// Force-break the long word
|
|
38
265
|
let remainingWord = token;
|
|
39
|
-
while (remainingWord
|
|
40
|
-
|
|
41
|
-
|
|
266
|
+
while (visualLength(remainingWord) > safeWidth) {
|
|
267
|
+
let visibleChars = 0;
|
|
268
|
+
let splitIdx = 0;
|
|
269
|
+
while (visibleChars < safeWidth && splitIdx < remainingWord.length) {
|
|
270
|
+
if (remainingWord.startsWith('\x1b[', splitIdx)) {
|
|
271
|
+
const endEsc = remainingWord.indexOf('m', splitIdx);
|
|
272
|
+
if (endEsc !== -1) {
|
|
273
|
+
splitIdx = endEsc + 1;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const code = remainingWord.charCodeAt(splitIdx);
|
|
278
|
+
const isWide = (code >= 0x3000 && code <= 0x9FFF) || (code >= 0xFF00 && code <= 0xFFEF);
|
|
279
|
+
if (visibleChars + (isWide ? 2 : 1) > safeWidth) {
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
visibleChars += isWide ? 2 : 1;
|
|
283
|
+
splitIdx++;
|
|
284
|
+
}
|
|
285
|
+
result.push(remainingWord.substring(0, splitIdx));
|
|
286
|
+
remainingWord = remainingWord.substring(splitIdx);
|
|
42
287
|
}
|
|
43
288
|
currentLine = remainingWord;
|
|
44
289
|
}
|
|
45
|
-
// If it's whitespace that causes overflow
|
|
290
|
+
// If it's whitespace that causes overflow
|
|
46
291
|
else if (/^\s+$/.test(token)) {
|
|
47
292
|
if (currentLine) {
|
|
48
293
|
result.push(currentLine.trimEnd());
|
|
49
294
|
currentLine = '';
|
|
50
295
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
// If it's a normal word that exceeds width, push currentLine and start new with this word
|
|
296
|
+
}
|
|
297
|
+
// Normal word that causes overflow
|
|
55
298
|
else {
|
|
56
299
|
if (currentLine) {
|
|
57
300
|
result.push(currentLine.trimEnd());
|
|
@@ -59,14 +302,183 @@ export function outputFormatted(text, overrideWidth) {
|
|
|
59
302
|
currentLine = token;
|
|
60
303
|
}
|
|
61
304
|
} else {
|
|
62
|
-
// Just append the token (word or whitespace)
|
|
63
305
|
currentLine += token;
|
|
64
306
|
}
|
|
65
307
|
}
|
|
66
308
|
|
|
67
309
|
if (currentLine) result.push(currentLine.trimEnd());
|
|
68
|
-
return result.
|
|
69
|
-
}
|
|
310
|
+
return result.length > 0 ? result : [''];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const line of lines) {
|
|
314
|
+
// --- Table row detection (pipe-delimited, outside code blocks) -----------
|
|
315
|
+
const isTableRow = !inCodeBlock && /^\s*\|/.test(line);
|
|
316
|
+
if (isTableRow) {
|
|
317
|
+
tableBuffer.push(line.trim());
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
// Flush buffered table when a non-table line is encountered
|
|
321
|
+
if (tableBuffer.length > 0) {
|
|
322
|
+
renderTable(tableBuffer);
|
|
323
|
+
tableBuffer = [];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check if the line already contains ANSI escape formatting (e.g., pre-colored command outputs)
|
|
327
|
+
const hasAnsi = /\x1b\[[0-9;]*[a-zA-Z]/.test(line);
|
|
328
|
+
if (hasAnsi) {
|
|
329
|
+
const wrappedSegments = wrapString(line, width);
|
|
330
|
+
const balancedSegments = balanceAnsiStyles(wrappedSegments);
|
|
331
|
+
for (const segment of balancedSegments) {
|
|
332
|
+
wrappedLines.push(segment);
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 1. Detect Horizontal Rules (e.g., ---, ***, ___ )
|
|
338
|
+
if (line.match(/^(\s*)([-*_])\2\2+(\s*)$/)) {
|
|
339
|
+
wrappedLines.push(`${colors.dim}${'─'.repeat(width)}${reset}`);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 2. Detect code block boundaries
|
|
344
|
+
if (line.trim().startsWith('```')) {
|
|
345
|
+
if (!inCodeBlock) {
|
|
346
|
+
inCodeBlock = true;
|
|
347
|
+
const lang = line.trim().substring(3).trim();
|
|
348
|
+
if (lang) {
|
|
349
|
+
wrappedLines.push(`${colors.dim} ${lang}${reset}`);
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
inCodeBlock = false;
|
|
353
|
+
}
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 3. If in code block, preserve line with vertical boundary
|
|
358
|
+
if (inCodeBlock) {
|
|
359
|
+
wrappedLines.push(`${colors.dim}│ ${reset}${line}`);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 4. Check for Markdown Headers: # Header
|
|
364
|
+
const headerMatch = line.match(/^(#+)\s*(.*)/);
|
|
365
|
+
if (headerMatch) {
|
|
366
|
+
const level = headerMatch[1].length;
|
|
367
|
+
const title = headerMatch[2];
|
|
368
|
+
|
|
369
|
+
// Color hierarchy: H1 = header (purple), H2 = main (blue), H3+ = dim
|
|
370
|
+
const levelColor = level === 1 ? colors.header
|
|
371
|
+
: level === 2 ? colors.main
|
|
372
|
+
: colors.dim;
|
|
373
|
+
|
|
374
|
+
// Bold for H1 and H2, normal for H3+
|
|
375
|
+
const boldStart = level <= 2 ? '\x1b[1m' : '';
|
|
376
|
+
const boldEnd = level <= 2 ? '\x1b[22m' : '';
|
|
377
|
+
|
|
378
|
+
// Add a blank line before headers for breathing room
|
|
379
|
+
if (wrappedLines.length > 0 && wrappedLines[wrappedLines.length - 1] !== '') {
|
|
380
|
+
wrappedLines.push('');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const wrapWidth = Math.max(10, width);
|
|
384
|
+
const wrappedTitle = wrapString(title, wrapWidth);
|
|
385
|
+
|
|
386
|
+
for (const segment of wrappedTitle) {
|
|
387
|
+
const formatted = applyInlineStyles(segment)
|
|
388
|
+
.replace(/\x1b\[39m/g, `\x1b[39m${levelColor}`);
|
|
389
|
+
wrappedLines.push(`${boldStart}${levelColor}${formatted}${boldEnd}${reset}`);
|
|
390
|
+
}
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
// 5. Check for List Item structure: bullets (- * +) or numbering (e.g., 1. or 1.1.)
|
|
397
|
+
const listMatch = line.match(/^(\s*)([-*+]\s|\d+(\.\d+)*\.\s)/);
|
|
398
|
+
if (listMatch) {
|
|
399
|
+
const prefix = listMatch[0];
|
|
400
|
+
const content = line.substring(prefix.length);
|
|
401
|
+
const wrapWidth = Math.max(10, width - prefix.length);
|
|
402
|
+
const formattedContent = applyInlineStyles(content);
|
|
403
|
+
const wrappedSegments = wrapString(formattedContent, wrapWidth);
|
|
404
|
+
|
|
405
|
+
let formattedPrefix = prefix;
|
|
406
|
+
const bulletMatch = prefix.match(/^(\s*)([-*+])(\s)/);
|
|
407
|
+
if (bulletMatch) {
|
|
408
|
+
formattedPrefix = `${bulletMatch[1]}${colors.main}•${reset}${bulletMatch[3]}`;
|
|
409
|
+
} else {
|
|
410
|
+
const numberMatch = prefix.match(/^(\s*)(\d+(\.\d+)*\.)(\s)/);
|
|
411
|
+
if (numberMatch) {
|
|
412
|
+
formattedPrefix = `${numberMatch[1]}${colors.main}${numberMatch[2]}${reset}${numberMatch[4]}`;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const balancedSegments = balanceAnsiStyles(wrappedSegments);
|
|
417
|
+
wrappedLines.push(formattedPrefix + balancedSegments[0]);
|
|
418
|
+
const hangingIndent = ' '.repeat(prefix.length);
|
|
419
|
+
for (let i = 1; i < balancedSegments.length; i++) {
|
|
420
|
+
wrappedLines.push(hangingIndent + balancedSegments[i]);
|
|
421
|
+
}
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 6. Check for Blockquote structure
|
|
426
|
+
const quoteMatch = line.match(/^(\s*)(>\s?)/);
|
|
427
|
+
if (quoteMatch) {
|
|
428
|
+
// Add spacing before blockquotes for breathing room
|
|
429
|
+
if (wrappedLines.length > 0 && wrappedLines[wrappedLines.length - 1] !== '') {
|
|
430
|
+
wrappedLines.push('');
|
|
431
|
+
}
|
|
432
|
+
const prefix = quoteMatch[0];
|
|
433
|
+
const content = line.substring(prefix.length);
|
|
434
|
+
const wrapWidth = Math.max(10, width - prefix.length);
|
|
435
|
+
const baseColor = colors.dim;
|
|
436
|
+
|
|
437
|
+
// Prefix quote content with baseColor and restore it after any inline color resets
|
|
438
|
+
const formattedContent = `${baseColor}${applyInlineStyles(content).replace(/\x1b\[39m/g, `\x1b[39m${baseColor}`)}${reset}`;
|
|
439
|
+
const wrappedSegments = wrapString(formattedContent, wrapWidth);
|
|
440
|
+
|
|
441
|
+
// Use dim color for the vertical bar to make blockquotes feel integrated and soft
|
|
442
|
+
const formattedPrefix = prefix.replace(/>\s?/, `${colors.dim}│ ${reset}`);
|
|
443
|
+
const balancedSegments = balanceAnsiStyles(wrappedSegments);
|
|
444
|
+
|
|
445
|
+
for (const segment of balancedSegments) {
|
|
446
|
+
wrappedLines.push(formattedPrefix + segment);
|
|
447
|
+
}
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 7. Check for Indented paragraph structure
|
|
452
|
+
const indentMatch = line.match(/^(\s+)/);
|
|
453
|
+
if (indentMatch) {
|
|
454
|
+
const prefix = indentMatch[0];
|
|
455
|
+
const content = line.substring(prefix.length);
|
|
456
|
+
const wrapWidth = Math.max(10, width - prefix.length);
|
|
457
|
+
const formattedContent = applyInlineStyles(content);
|
|
458
|
+
const wrappedSegments = wrapString(formattedContent, wrapWidth);
|
|
459
|
+
const balancedSegments = balanceAnsiStyles(wrappedSegments);
|
|
460
|
+
|
|
461
|
+
for (const segment of balancedSegments) {
|
|
462
|
+
wrappedLines.push(prefix + segment);
|
|
463
|
+
}
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 8. Fallback: plain text paragraph
|
|
468
|
+
const formatted = applyInlineStyles(line);
|
|
469
|
+
const wrappedSegments = wrapString(formatted, width);
|
|
470
|
+
const balancedSegments = balanceAnsiStyles(wrappedSegments);
|
|
471
|
+
for (const segment of balancedSegments) {
|
|
472
|
+
wrappedLines.push(segment);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Flush any table that ends at end-of-input
|
|
477
|
+
if (tableBuffer.length > 0) {
|
|
478
|
+
renderTable(tableBuffer);
|
|
479
|
+
}
|
|
70
480
|
|
|
71
|
-
return wrappedLines
|
|
481
|
+
return wrappedLines
|
|
482
|
+
.map(line => line.replace(/[\s\u200B]+(?=(?:\x1b\[[0-9;]*[a-zA-Z]|\s)*$)/g, ''))
|
|
483
|
+
.join('\n');
|
|
72
484
|
}
|
package/src/utils/ui.js
CHANGED
|
@@ -10,7 +10,7 @@ const COLORS = {
|
|
|
10
10
|
data: [194, 198, 197], // #C2C6C5 – silver (args, data)
|
|
11
11
|
success: [122, 195, 145], // #7AC391 – green (● on stop)
|
|
12
12
|
error: [224, 112, 112], // #E07070 – red (● on fail)
|
|
13
|
-
dim: [
|
|
13
|
+
dim: [143, 147, 153], // #8F9399 – cool grey (dim annotations)
|
|
14
14
|
header: [163, 122, 204], // #A37ACC – purple (● header dot)
|
|
15
15
|
};
|
|
16
16
|
|