Skip to content

Commit 6e0e540

Browse files
committed
Implement terminal compress progress bar feature
This commit introduces a new feature to compress terminal output by processing carriage returns. The `processCarriageReturns` function has been integrated into the `Terminal` class to handle progress bar updates effectively, ensuring only the final state is displayed. Additionally, the `terminalCompressProgressBar` setting has been added to the global settings schema, allowing users to enable or disable this feature. Tests have been updated to validate the new functionality and ensure correct behavior in various scenarios. A Benchmark is also added to test the performance. Not that there is still no i18n support for this.
1 parent 5617c29 commit 6e0e540

File tree

15 files changed

+602
-207
lines changed

15 files changed

+602
-207
lines changed

src/core/webview/webviewMessageHandler.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,13 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
783783
Terminal.setTerminalZdotdir(message.bool)
784784
}
785785
break
786+
case "terminalCompressProgressBar":
787+
await updateGlobalState("terminalCompressProgressBar", message.bool)
788+
await provider.postStateToWebview()
789+
if (message.bool !== undefined) {
790+
Terminal.setCompressProgressBar(message.bool)
791+
}
792+
break
786793
case "mode":
787794
await provider.handleModeSwitch(message.text as Mode)
788795
break

src/exports/roo-code.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ type GlobalSettings = {
275275
terminalZshOhMy?: boolean | undefined
276276
terminalZshP10k?: boolean | undefined
277277
terminalZdotdir?: boolean | undefined
278+
terminalCompressProgressBar?: boolean | undefined
278279
rateLimitSeconds?: number | undefined
279280
diffEnabled?: boolean | undefined
280281
fuzzyMatchThreshold?: number | undefined

src/exports/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ type GlobalSettings = {
278278
terminalZshOhMy?: boolean | undefined
279279
terminalZshP10k?: boolean | undefined
280280
terminalZdotdir?: boolean | undefined
281+
terminalCompressProgressBar?: boolean | undefined
281282
rateLimitSeconds?: number | undefined
282283
diffEnabled?: boolean | undefined
283284
fuzzyMatchThreshold?: number | undefined

src/integrations/misc/__tests__/extract-text.test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
stripLineNumbers,
55
truncateOutput,
66
applyRunLengthEncoding,
7+
processCarriageReturns,
78
} from "../extract-text"
89

910
describe("addLineNumbers", () => {
@@ -261,3 +262,166 @@ describe("applyRunLengthEncoding", () => {
261262
expect(applyRunLengthEncoding(input)).toBe(input)
262263
})
263264
})
265+
266+
describe("processCarriageReturns", () => {
267+
it("should return original input if no carriage returns present", () => {
268+
const input = "Line 1\nLine 2\nLine 3"
269+
expect(processCarriageReturns(input)).toBe(input)
270+
})
271+
272+
it("should process basic progress bar with carriage returns", () => {
273+
const input = "Progress: [===>---------] 30%\rProgress: [======>------] 60%\rProgress: [==========>] 100%"
274+
const expected = "Progress: [==========>] 100%%"
275+
expect(processCarriageReturns(input)).toBe(expected)
276+
})
277+
278+
it("should handle multi-line outputs with carriage returns", () => {
279+
const input = "Line 1\rUpdated Line 1\nLine 2\rUpdated Line 2\rFinal Line 2"
280+
const expected = "Updated Line 1\nFinal Line 2 2"
281+
expect(processCarriageReturns(input)).toBe(expected)
282+
})
283+
284+
it("should handle carriage returns at end of line", () => {
285+
// A carriage return at the end of a line should be treated as if the cursor is at the start
286+
// with no content following it, so we keep the existing content
287+
const input = "Initial text\rReplacement text\r"
288+
// Depending on terminal behavior:
289+
// Option 1: If last CR is ignored because nothing follows it to replace text
290+
const expected = "Replacement text"
291+
expect(processCarriageReturns(input)).toBe(expected)
292+
})
293+
294+
// Additional test to clarify behavior with a terminal-like example
295+
it("should handle carriage returns in a way that matches terminal behavior", () => {
296+
// In a real terminal:
297+
// 1. "Hello" is printed
298+
// 2. CR moves cursor to start of line
299+
// 3. "World" overwrites, becoming "World"
300+
// 4. CR moves cursor to start again
301+
// 5. Nothing follows, so the line remains "World" (cursor just sitting at start)
302+
const input = "Hello\rWorld\r"
303+
const expected = "World"
304+
expect(processCarriageReturns(input)).toBe(expected)
305+
306+
// Same principle applies to CR+NL
307+
// 1. "Line1" is printed
308+
// 2. CR moves cursor to start
309+
// 3. NL moves to next line, so the line remains "Line1"
310+
expect(processCarriageReturns("Line1\r\n")).toBe("Line1\n")
311+
})
312+
313+
it("should preserve lines without carriage returns", () => {
314+
const input = "Line 1\nLine 2\rUpdated Line 2\nLine 3"
315+
const expected = "Line 1\nUpdated Line 2\nLine 3"
316+
expect(processCarriageReturns(input)).toBe(expected)
317+
})
318+
319+
it("should handle complex tqdm-like progress bars", () => {
320+
const input =
321+
"10%|██ | 10/100 [00:01<00:09, 10.00it/s]\r20%|████ | 20/100 [00:02<00:08, 10.00it/s]\r100%|██████████| 100/100 [00:10<00:00, 10.00it/s]"
322+
const expected = "100%|██████████| 100/100 [00:10<00:00, 10.00it/s]"
323+
expect(processCarriageReturns(input)).toBe(expected)
324+
})
325+
326+
it("should handle ANSI escape sequences", () => {
327+
const input = "\x1b]633;C\x07Loading\rLoading.\rLoading..\rLoading...\x1b]633;D\x07"
328+
const expected = "Loading...\x1b]633;D\x07"
329+
expect(processCarriageReturns(input)).toBe(expected)
330+
})
331+
332+
it("should handle mixed content with carriage returns and newlines", () => {
333+
const input =
334+
"Step 1: Starting\rStep 1: In progress\rStep 1: Done\nStep 2: Starting\rStep 2: In progress\rStep 2: Done"
335+
const expected = "Step 1: Donerogress\nStep 2: Donerogress"
336+
expect(processCarriageReturns(input)).toBe(expected)
337+
})
338+
339+
it("should handle empty input", () => {
340+
expect(processCarriageReturns("")).toBe("")
341+
})
342+
343+
it("should handle large number of carriage returns efficiently", () => {
344+
// Create a string with many carriage returns
345+
let input = ""
346+
for (let i = 0; i < 10000; i++) {
347+
input += `Progress: ${i / 100}%\r`
348+
}
349+
input += "Progress: 100%"
350+
351+
const expected = "Progress: 100%9%"
352+
expect(processCarriageReturns(input)).toBe(expected)
353+
})
354+
355+
// Additional edge cases to stress test processCarriageReturns
356+
it("should handle consecutive carriage returns", () => {
357+
const input = "Initial\r\r\r\rFinal"
358+
const expected = "Finalal"
359+
expect(processCarriageReturns(input)).toBe(expected)
360+
})
361+
362+
it("should handle carriage returns at the start of a line", () => {
363+
const input = "\rText after carriage return"
364+
const expected = "Text after carriage return"
365+
expect(processCarriageReturns(input)).toBe(expected)
366+
})
367+
368+
it("should handle only carriage returns", () => {
369+
const input = "\r\r\r\r"
370+
const expected = ""
371+
expect(processCarriageReturns(input)).toBe(expected)
372+
})
373+
374+
it("should handle carriage returns with empty strings between them", () => {
375+
const input = "Start\r\r\r\r\rEnd"
376+
const expected = "Endrt"
377+
expect(processCarriageReturns(input)).toBe(expected)
378+
})
379+
380+
it("should handle multiline with carriage returns at different positions", () => {
381+
const input = "Line1\rLine1Updated\nLine2\nLine3\rLine3Updated\rLine3Final\nLine4"
382+
const expected = "Line1Updated\nLine2\nLine3Finaled\nLine4"
383+
expect(processCarriageReturns(input)).toBe(expected)
384+
})
385+
386+
it("should handle carriage returns with special characters", () => {
387+
const input = "Line with 🚀 emoji\rUpdated with 🔥 emoji"
388+
const expected = "Updated with 🔥 emoji"
389+
expect(processCarriageReturns(input)).toBe(expected)
390+
})
391+
392+
it("should correctly handle multiple consecutive newlines with carriage returns", () => {
393+
const input = "Line with not a emoji\rLine with 🔥 emoji"
394+
const expected = "Line with 🔥 emojioji"
395+
expect(processCarriageReturns(input)).toBe(expected)
396+
})
397+
398+
it("should handle carriage returns in the middle of non-ASCII text", () => {
399+
const input = "你好世界啊\r你好地球"
400+
const expected = "你好地球啊"
401+
expect(processCarriageReturns(input)).toBe(expected)
402+
})
403+
404+
it("should correctly handle complex patterns of alternating carriage returns and newlines", () => {
405+
// Break down the example:
406+
// 1. "Line1" + CR + NL: CR moves cursor to start of line, NL moves to next line, preserving "Line1"
407+
// 2. "Line2" + CR: CR moves cursor to start of line
408+
// 3. "Line2Updated" overwrites "Line2"
409+
// 4. NL: moves to next line
410+
// 5. "Line3" + CR + NL: CR moves cursor to start, NL moves to next line, preserving "Line3"
411+
const input = "Line1\r\nLine2\rLine2Updated\nLine3\r\n"
412+
const expected = "Line1\nLine2Updated\nLine3\n"
413+
expect(processCarriageReturns(input)).toBe(expected)
414+
})
415+
416+
it("should handle partial overwrites with carriage returns", () => {
417+
// In this case:
418+
// 1. "Initial text" is printed
419+
// 2. CR moves cursor to start of line
420+
// 3. "next" is printed, overwriting only the first 4 chars
421+
// 4. CR moves cursor to start, but nothing follows
422+
// Final result should be "nextial text" (first 4 chars overwritten)
423+
const input = "Initial text\rnext\r"
424+
const expected = "nextial text"
425+
expect(processCarriageReturns(input)).toBe(expected)
426+
})
427+
})

0 commit comments

Comments
 (0)