takt: github-issue-105-aieejento-no (#138)
This commit is contained in:
parent
db789bbaba
commit
92f97bbd42
@ -145,6 +145,110 @@ describe('StreamDisplay', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ANSI escape sequence stripping', () => {
|
||||||
|
it('should strip ANSI codes from text before writing to stdout', () => {
|
||||||
|
const display = new StreamDisplay('test-agent', false);
|
||||||
|
display.showText('\x1b[41mRed background\x1b[0m');
|
||||||
|
|
||||||
|
expect(stdoutWriteSpy).toHaveBeenCalledWith('Red background');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip ANSI codes from thinking before writing to stdout', () => {
|
||||||
|
const display = new StreamDisplay('test-agent', false);
|
||||||
|
display.showThinking('\x1b[31mColored thinking\x1b[0m');
|
||||||
|
|
||||||
|
// chalk.gray.italic wraps the stripped text, so check it does NOT contain raw ANSI
|
||||||
|
const writtenText = stdoutWriteSpy.mock.calls[0]?.[0] as string;
|
||||||
|
expect(writtenText).not.toContain('\x1b[41m');
|
||||||
|
expect(writtenText).not.toContain('\x1b[31m');
|
||||||
|
expect(writtenText).toContain('Colored thinking');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accumulate stripped text in textBuffer', () => {
|
||||||
|
const display = new StreamDisplay('test-agent', false);
|
||||||
|
display.showText('\x1b[31mRed\x1b[0m');
|
||||||
|
display.showText('\x1b[32m Green\x1b[0m');
|
||||||
|
|
||||||
|
// Flush should work correctly with stripped content
|
||||||
|
display.flushText();
|
||||||
|
|
||||||
|
// After flush, buffer is cleared — verify no crash and text was output
|
||||||
|
expect(stdoutWriteSpy).toHaveBeenCalledWith('Red');
|
||||||
|
expect(stdoutWriteSpy).toHaveBeenCalledWith(' Green');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accumulate stripped text in thinkingBuffer', () => {
|
||||||
|
const display = new StreamDisplay('test-agent', false);
|
||||||
|
display.showThinking('\x1b[31mThought 1\x1b[0m');
|
||||||
|
display.showThinking('\x1b[32m Thought 2\x1b[0m');
|
||||||
|
|
||||||
|
display.flushThinking();
|
||||||
|
|
||||||
|
// Verify stripped text was written (wrapped in chalk styling)
|
||||||
|
expect(stdoutWriteSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not strip ANSI from text that has no ANSI codes', () => {
|
||||||
|
const display = new StreamDisplay('test-agent', false);
|
||||||
|
display.showText('Plain text');
|
||||||
|
|
||||||
|
expect(stdoutWriteSpy).toHaveBeenCalledWith('Plain text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip ANSI codes from tool output before buffering', () => {
|
||||||
|
const display = new StreamDisplay('test-agent', false);
|
||||||
|
display.showToolUse('Bash', { command: 'ls' });
|
||||||
|
display.showToolOutput('\x1b[32mgreen output\x1b[0m\n');
|
||||||
|
|
||||||
|
const outputLine = consoleLogSpy.mock.calls.find(
|
||||||
|
(call) => typeof call[0] === 'string' && (call[0] as string).includes('green output'),
|
||||||
|
);
|
||||||
|
expect(outputLine).toBeDefined();
|
||||||
|
expect(outputLine![0]).not.toContain('\x1b[32m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip ANSI codes from tool output across multiple chunks', () => {
|
||||||
|
const display = new StreamDisplay('test-agent', false);
|
||||||
|
display.showToolUse('Bash', { command: 'ls' });
|
||||||
|
display.showToolOutput('\x1b[31mpartial');
|
||||||
|
display.showToolOutput(' line\x1b[0m\n');
|
||||||
|
|
||||||
|
const outputLine = consoleLogSpy.mock.calls.find(
|
||||||
|
(call) => typeof call[0] === 'string' && (call[0] as string).includes('partial line'),
|
||||||
|
);
|
||||||
|
expect(outputLine).toBeDefined();
|
||||||
|
expect(outputLine![0]).not.toContain('\x1b[31m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip ANSI codes from tool result content', () => {
|
||||||
|
const display = new StreamDisplay('test-agent', false);
|
||||||
|
display.showToolUse('Read', { file_path: '/test.ts' });
|
||||||
|
display.showToolResult('\x1b[41mResult with red bg\x1b[0m', false);
|
||||||
|
|
||||||
|
const resultLine = consoleLogSpy.mock.calls.find(
|
||||||
|
(call) => typeof call[0] === 'string' && (call[0] as string).includes('✓'),
|
||||||
|
);
|
||||||
|
expect(resultLine).toBeDefined();
|
||||||
|
const fullOutput = resultLine!.join(' ');
|
||||||
|
expect(fullOutput).toContain('Result with red bg');
|
||||||
|
expect(fullOutput).not.toContain('\x1b[41m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip ANSI codes from tool result error content', () => {
|
||||||
|
const display = new StreamDisplay('test-agent', false);
|
||||||
|
display.showToolUse('Bash', { command: 'fail' });
|
||||||
|
display.showToolResult('\x1b[31mError message\x1b[0m', true);
|
||||||
|
|
||||||
|
const errorLine = consoleLogSpy.mock.calls.find(
|
||||||
|
(call) => typeof call[0] === 'string' && (call[0] as string).includes('✗'),
|
||||||
|
);
|
||||||
|
expect(errorLine).toBeDefined();
|
||||||
|
const fullOutput = errorLine!.join(' ');
|
||||||
|
expect(fullOutput).toContain('Error message');
|
||||||
|
expect(fullOutput).not.toContain('\x1b[31m');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('progress prefix format', () => {
|
describe('progress prefix format', () => {
|
||||||
it('should format progress as (iteration/max) step index/total', () => {
|
it('should format progress as (iteration/max) step index/total', () => {
|
||||||
const progressInfo: ProgressInfo = {
|
const progressInfo: ProgressInfo = {
|
||||||
|
|||||||
@ -392,6 +392,90 @@ describe('ParallelLogger', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ANSI escape sequence stripping', () => {
|
||||||
|
it('should strip ANSI codes from text events', () => {
|
||||||
|
const logger = new ParallelLogger({
|
||||||
|
subMovementNames: ['step-a'],
|
||||||
|
writeFn,
|
||||||
|
});
|
||||||
|
const handler = logger.createStreamHandler('step-a', 0);
|
||||||
|
|
||||||
|
handler({ type: 'text', data: { text: '\x1b[41mRed background\x1b[0m\n' } } as StreamEvent);
|
||||||
|
|
||||||
|
expect(output).toHaveLength(1);
|
||||||
|
expect(output[0]).toContain('Red background');
|
||||||
|
expect(output[0]).not.toContain('\x1b[41m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip ANSI codes from thinking events', () => {
|
||||||
|
const logger = new ParallelLogger({
|
||||||
|
subMovementNames: ['step-a'],
|
||||||
|
writeFn,
|
||||||
|
});
|
||||||
|
const handler = logger.createStreamHandler('step-a', 0);
|
||||||
|
|
||||||
|
handler({
|
||||||
|
type: 'thinking',
|
||||||
|
data: { thinking: '\x1b[31mColored thought\x1b[0m' },
|
||||||
|
} as StreamEvent);
|
||||||
|
|
||||||
|
expect(output).toHaveLength(1);
|
||||||
|
expect(output[0]).toContain('Colored thought');
|
||||||
|
expect(output[0]).not.toContain('\x1b[31m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip ANSI codes from tool_output events', () => {
|
||||||
|
const logger = new ParallelLogger({
|
||||||
|
subMovementNames: ['step-a'],
|
||||||
|
writeFn,
|
||||||
|
});
|
||||||
|
const handler = logger.createStreamHandler('step-a', 0);
|
||||||
|
|
||||||
|
handler({
|
||||||
|
type: 'tool_output',
|
||||||
|
data: { tool: 'Bash', output: '\x1b[32mGreen output\x1b[0m' },
|
||||||
|
} as StreamEvent);
|
||||||
|
|
||||||
|
expect(output).toHaveLength(1);
|
||||||
|
expect(output[0]).toContain('Green output');
|
||||||
|
expect(output[0]).not.toContain('\x1b[32m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip ANSI codes from tool_result events', () => {
|
||||||
|
const logger = new ParallelLogger({
|
||||||
|
subMovementNames: ['step-a'],
|
||||||
|
writeFn,
|
||||||
|
});
|
||||||
|
const handler = logger.createStreamHandler('step-a', 0);
|
||||||
|
|
||||||
|
handler({
|
||||||
|
type: 'tool_result',
|
||||||
|
data: { content: '\x1b[31mResult with ANSI\x1b[0m', isError: false },
|
||||||
|
} as StreamEvent);
|
||||||
|
|
||||||
|
expect(output).toHaveLength(1);
|
||||||
|
expect(output[0]).toContain('Result with ANSI');
|
||||||
|
expect(output[0]).not.toContain('\x1b[31m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip ANSI codes from buffered text across multiple events', () => {
|
||||||
|
const logger = new ParallelLogger({
|
||||||
|
subMovementNames: ['step-a'],
|
||||||
|
writeFn,
|
||||||
|
});
|
||||||
|
const handler = logger.createStreamHandler('step-a', 0);
|
||||||
|
|
||||||
|
handler({ type: 'text', data: { text: '\x1b[31mHello' } } as StreamEvent);
|
||||||
|
handler({ type: 'text', data: { text: ' World\x1b[0m\n' } } as StreamEvent);
|
||||||
|
|
||||||
|
expect(output).toHaveLength(1);
|
||||||
|
expect(output[0]).toContain('Hello World');
|
||||||
|
// The prefix contains its own ANSI codes (\x1b[36m, \x1b[0m), so
|
||||||
|
// verify the AI-originated \x1b[31m was stripped, not the prefix's codes
|
||||||
|
expect(output[0]).not.toContain('\x1b[31m');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('interleaved output from multiple sub-movements', () => {
|
describe('interleaved output from multiple sub-movements', () => {
|
||||||
it('should correctly interleave prefixed output', () => {
|
it('should correctly interleave prefixed output', () => {
|
||||||
const logger = new ParallelLogger({
|
const logger = new ParallelLogger({
|
||||||
|
|||||||
83
src/__tests__/strip-ansi.test.ts
Normal file
83
src/__tests__/strip-ansi.test.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Tests for stripAnsi utility function
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { stripAnsi } from '../shared/utils/text.js';
|
||||||
|
|
||||||
|
describe('stripAnsi', () => {
|
||||||
|
it('should return plain text unchanged', () => {
|
||||||
|
expect(stripAnsi('Hello World')).toBe('Hello World');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty string unchanged', () => {
|
||||||
|
expect(stripAnsi('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip foreground color codes', () => {
|
||||||
|
// Red text: ESC[31m ... ESC[0m
|
||||||
|
expect(stripAnsi('\x1b[31mError\x1b[0m')).toBe('Error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip background color codes', () => {
|
||||||
|
// Red background: ESC[41m ... ESC[0m
|
||||||
|
expect(stripAnsi('\x1b[41mHighlighted\x1b[0m')).toBe('Highlighted');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip combined foreground and background codes', () => {
|
||||||
|
// White text on red background: ESC[37;41m
|
||||||
|
expect(stripAnsi('\x1b[37;41mAlert\x1b[0m')).toBe('Alert');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip multiple SGR sequences in one string', () => {
|
||||||
|
const input = '\x1b[1mBold\x1b[0m normal \x1b[32mGreen\x1b[0m';
|
||||||
|
expect(stripAnsi(input)).toBe('Bold normal Green');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip 256-color sequences', () => {
|
||||||
|
// ESC[38;5;196m (foreground 256-color red)
|
||||||
|
expect(stripAnsi('\x1b[38;5;196mRed256\x1b[0m')).toBe('Red256');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip cursor movement sequences', () => {
|
||||||
|
// Cursor up: ESC[1A, Cursor right: ESC[5C
|
||||||
|
expect(stripAnsi('\x1b[1AUp\x1b[5CRight')).toBe('UpRight');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip erase sequences', () => {
|
||||||
|
// Clear line: ESC[2K
|
||||||
|
expect(stripAnsi('\x1b[2KCleared')).toBe('Cleared');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip OSC sequences terminated by BEL', () => {
|
||||||
|
// Set terminal title: ESC]0;Title BEL
|
||||||
|
expect(stripAnsi('\x1b]0;My Title\x07Text')).toBe('Text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip OSC sequences terminated by ST', () => {
|
||||||
|
// Set terminal title: ESC]0;Title ESC\
|
||||||
|
expect(stripAnsi('\x1b]0;My Title\x1b\\Text')).toBe('Text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip other single-character escape codes', () => {
|
||||||
|
// ESC followed by a single char (e.g., ESC M = reverse line feed)
|
||||||
|
expect(stripAnsi('\x1bMText')).toBe('Text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve newlines and whitespace', () => {
|
||||||
|
expect(stripAnsi('\x1b[31mLine1\n\x1b[32mLine2\n')).toBe('Line1\nLine2\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip sequences without a reset at the end', () => {
|
||||||
|
// Simulates the reported bug: background color set without reset
|
||||||
|
expect(stripAnsi('\x1b[41mRed background text')).toBe('Red background text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle text with only ANSI sequences', () => {
|
||||||
|
expect(stripAnsi('\x1b[31m\x1b[0m')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle consecutive ANSI sequences', () => {
|
||||||
|
expect(stripAnsi('\x1b[1m\x1b[31m\x1b[42mStyled\x1b[0m')).toBe('Styled');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { StreamCallback, StreamEvent } from '../types.js';
|
import type { StreamCallback, StreamEvent } from '../types.js';
|
||||||
|
import { stripAnsi } from '../../../shared/utils/text.js';
|
||||||
|
|
||||||
/** ANSI color codes for sub-movement prefixes (cycled in order) */
|
/** ANSI color codes for sub-movement prefixes (cycled in order) */
|
||||||
const COLORS = ['\x1b[36m', '\x1b[33m', '\x1b[35m', '\x1b[32m'] as const; // cyan, yellow, magenta, green
|
const COLORS = ['\x1b[36m', '\x1b[33m', '\x1b[35m', '\x1b[32m'] as const; // cyan, yellow, magenta, green
|
||||||
@ -117,7 +118,7 @@ export class ParallelLogger {
|
|||||||
*/
|
*/
|
||||||
private handleTextEvent(name: string, prefix: string, text: string): void {
|
private handleTextEvent(name: string, prefix: string, text: string): void {
|
||||||
const buffer = this.lineBuffers.get(name) ?? '';
|
const buffer = this.lineBuffers.get(name) ?? '';
|
||||||
const combined = buffer + text;
|
const combined = buffer + stripAnsi(text);
|
||||||
const parts = combined.split('\n');
|
const parts = combined.split('\n');
|
||||||
|
|
||||||
// Last part is incomplete (no trailing newline) — keep in buffer
|
// Last part is incomplete (no trailing newline) — keep in buffer
|
||||||
@ -145,13 +146,13 @@ export class ParallelLogger {
|
|||||||
text = `[tool] ${event.data.tool}`;
|
text = `[tool] ${event.data.tool}`;
|
||||||
break;
|
break;
|
||||||
case 'tool_result':
|
case 'tool_result':
|
||||||
text = event.data.content;
|
text = stripAnsi(event.data.content);
|
||||||
break;
|
break;
|
||||||
case 'tool_output':
|
case 'tool_output':
|
||||||
text = event.data.output;
|
text = stripAnsi(event.data.output);
|
||||||
break;
|
break;
|
||||||
case 'thinking':
|
case 'thinking':
|
||||||
text = event.data.thinking;
|
text = stripAnsi(event.data.thinking);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import chalk from 'chalk';
|
|||||||
// dependent event-data types, which is out of scope for this refactoring.
|
// dependent event-data types, which is out of scope for this refactoring.
|
||||||
import type { StreamEvent, StreamCallback } from '../../core/piece/index.js';
|
import type { StreamEvent, StreamCallback } from '../../core/piece/index.js';
|
||||||
import { truncate } from './LogManager.js';
|
import { truncate } from './LogManager.js';
|
||||||
|
import { stripAnsi } from '../utils/text.js';
|
||||||
|
|
||||||
/** Progress information for stream display */
|
/** Progress information for stream display */
|
||||||
export interface ProgressInfo {
|
export interface ProgressInfo {
|
||||||
@ -116,7 +117,7 @@ export class StreamDisplay {
|
|||||||
this.lastToolUse = tool;
|
this.lastToolUse = tool;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toolOutputBuffer += output;
|
this.toolOutputBuffer += stripAnsi(output);
|
||||||
const lines = this.toolOutputBuffer.split(/\r?\n/);
|
const lines = this.toolOutputBuffer.split(/\r?\n/);
|
||||||
this.toolOutputBuffer = lines.pop() ?? '';
|
this.toolOutputBuffer = lines.pop() ?? '';
|
||||||
|
|
||||||
@ -129,11 +130,12 @@ export class StreamDisplay {
|
|||||||
|
|
||||||
showToolResult(content: string, isError: boolean): void {
|
showToolResult(content: string, isError: boolean): void {
|
||||||
this.stopToolSpinner();
|
this.stopToolSpinner();
|
||||||
|
const sanitizedContent = stripAnsi(content);
|
||||||
|
|
||||||
if (this.quiet) {
|
if (this.quiet) {
|
||||||
if (isError) {
|
if (isError) {
|
||||||
const toolName = this.lastToolUse || 'Tool';
|
const toolName = this.lastToolUse || 'Tool';
|
||||||
const errorContent = content || 'Unknown error';
|
const errorContent = sanitizedContent || 'Unknown error';
|
||||||
console.log(chalk.red(` ✗ ${toolName}:`), chalk.red(truncate(errorContent, 70)));
|
console.log(chalk.red(` ✗ ${toolName}:`), chalk.red(truncate(errorContent, 70)));
|
||||||
}
|
}
|
||||||
this.lastToolUse = null;
|
this.lastToolUse = null;
|
||||||
@ -149,10 +151,10 @@ export class StreamDisplay {
|
|||||||
|
|
||||||
const toolName = this.lastToolUse || 'Tool';
|
const toolName = this.lastToolUse || 'Tool';
|
||||||
if (isError) {
|
if (isError) {
|
||||||
const errorContent = content || 'Unknown error';
|
const errorContent = sanitizedContent || 'Unknown error';
|
||||||
console.log(chalk.red(` ✗ ${toolName}:`), chalk.red(truncate(errorContent, 70)));
|
console.log(chalk.red(` ✗ ${toolName}:`), chalk.red(truncate(errorContent, 70)));
|
||||||
} else if (content && content.length > 0) {
|
} else if (sanitizedContent && sanitizedContent.length > 0) {
|
||||||
const preview = content.split('\n')[0] || content;
|
const preview = sanitizedContent.split('\n')[0] || sanitizedContent;
|
||||||
console.log(chalk.green(` ✓ ${toolName}`), chalk.gray(truncate(preview, 60)));
|
console.log(chalk.green(` ✓ ${toolName}`), chalk.gray(truncate(preview, 60)));
|
||||||
} else {
|
} else {
|
||||||
console.log(chalk.green(` ✓ ${toolName}`));
|
console.log(chalk.green(` ✓ ${toolName}`));
|
||||||
@ -174,8 +176,9 @@ export class StreamDisplay {
|
|||||||
console.log(chalk.magenta(`💭 [${this.agentName}]${progressPart} thinking:`));
|
console.log(chalk.magenta(`💭 [${this.agentName}]${progressPart} thinking:`));
|
||||||
this.isFirstThinking = false;
|
this.isFirstThinking = false;
|
||||||
}
|
}
|
||||||
process.stdout.write(chalk.gray.italic(thinking));
|
const sanitized = stripAnsi(thinking);
|
||||||
this.thinkingBuffer += thinking;
|
process.stdout.write(chalk.gray.italic(sanitized));
|
||||||
|
this.thinkingBuffer += sanitized;
|
||||||
}
|
}
|
||||||
|
|
||||||
flushThinking(): void {
|
flushThinking(): void {
|
||||||
@ -200,8 +203,9 @@ export class StreamDisplay {
|
|||||||
console.log(chalk.cyan(`[${this.agentName}]${progressPart}:`));
|
console.log(chalk.cyan(`[${this.agentName}]${progressPart}:`));
|
||||||
this.isFirstText = false;
|
this.isFirstText = false;
|
||||||
}
|
}
|
||||||
process.stdout.write(text);
|
const sanitized = stripAnsi(text);
|
||||||
this.textBuffer += text;
|
process.stdout.write(sanitized);
|
||||||
|
this.textBuffer += sanitized;
|
||||||
}
|
}
|
||||||
|
|
||||||
flushText(): void {
|
flushText(): void {
|
||||||
|
|||||||
@ -35,6 +35,21 @@ export function getDisplayWidth(text: string): number {
|
|||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSI (Control Sequence Introducer): ESC [ ... final_byte
|
||||||
|
// OSC (Operating System Command): ESC ] ... (ST | BEL)
|
||||||
|
// Other escape: ESC followed by a single character
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
const ANSI_PATTERN = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[^[\]]/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip all ANSI escape sequences from a string.
|
||||||
|
* Removes CSI sequences (colors, cursor movement, etc.),
|
||||||
|
* OSC sequences, and other single-character escape codes.
|
||||||
|
*/
|
||||||
|
export function stripAnsi(text: string): string {
|
||||||
|
return text.replace(ANSI_PATTERN, '');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Truncate plain text to fit within maxWidth display columns.
|
* Truncate plain text to fit within maxWidth display columns.
|
||||||
* Appends '…' if truncated. The ellipsis itself counts as 1 column.
|
* Appends '…' if truncated. The ellipsis itself counts as 1 column.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user