Skip to content

Commit 564c094

Browse files
fix(markdown): compute visible width for wrapping by stripping ANSI escape codes and capping wrap width to terminal width; improves rendering accuracy and cleanup. Sets groundwork for future resize handling.
🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent 08b4561 commit 564c094

File tree

4 files changed

+71
-20
lines changed

4 files changed

+71
-20
lines changed

npm-app/src/cli-handlers/subagent.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,12 @@ export function exitSubagentBuffer(rl: any) {
132132
originalKeyHandlers = []
133133
}
134134

135-
// Remove resize listener
136-
process.stdout.removeAllListeners('resize')
135+
// Remove our specific resize listener if it exists
136+
process.stdout.listeners('resize').forEach((listener) => {
137+
if (listener.name === 'handleResize') {
138+
process.stdout.off('resize', listener as (...args: any[]) => void)
139+
}
140+
})
137141

138142
// Exit alternate screen buffer
139143
process.stdout.write(SHOW_CURSOR)

npm-app/src/cli-handlers/traces.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,12 @@ export function exitSubagentBuffer(rl: any) {
137137
originalKeyHandlers = []
138138
}
139139

140-
// Remove resize listener
141-
process.stdout.removeAllListeners('resize')
140+
// Remove our specific resize listener if it exists
141+
process.stdout.listeners('resize').forEach((listener) => {
142+
if (listener.name === 'handleResize') {
143+
process.stdout.off('resize', listener as (...args: any[]) => void)
144+
}
145+
})
142146

143147
// Exit alternate screen buffer
144148
process.stdout.write(SHOW_CURSOR)

npm-app/src/display/markdown-renderer.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class MarkdownStreamRenderer {
6767
'●•·',
6868
'•··',
6969
]
70-
private readonly indicatorThresholdMs = 200
70+
private readonly indicatorThresholdMs = 1000
7171
private readonly indicatorUpdateMs = 150
7272

7373
constructor(opts: MarkdownStreamRendererOptions = {}) {
@@ -119,7 +119,8 @@ export class MarkdownStreamRenderer {
119119
this.resizeHandler = () => {
120120
this.width = process.stdout.columns || this.width
121121
}
122-
process.stdout.on('resize', this.resizeHandler)
122+
// Use .once with bound handler tracker to avoid duplication
123+
process.stdout.addListener('resize', this.resizeHandler)
123124
}
124125
}
125126

@@ -486,6 +487,7 @@ export class MarkdownStreamRenderer {
486487
cleanup() {
487488
if (this.resizeHandler && process.stdout && 'off' in process.stdout) {
488489
process.stdout.off('resize', this.resizeHandler)
490+
this.resizeHandler = undefined
489491
}
490492
}
491493

@@ -569,9 +571,19 @@ export class MarkdownStreamRenderer {
569571
formattedCode = reapplyBg(formattedCode)
570572

571573
const lines = formattedCode.split('\n')
574+
575+
// Calculate actual width needed based on longest line
576+
const maxLineLength = lines.reduce((max: number, line: string) => {
577+
// Remove ANSI escape codes to get actual visible length
578+
const visibleLength = line.replace(/\x1b\[[^m]*m/g, '').length
579+
return Math.max(max, visibleLength)
580+
}, 0)
581+
582+
// Use the actual content width, but cap it at terminal width
583+
const availableWidth = this.width - padLeft.length - padRight.length
572584
const wrapWidth = Math.max(
573-
20,
574-
Math.min(this.width - padLeft.length - padRight.length, 120),
585+
20, // minimum width
586+
Math.min(maxLineLength, availableWidth, 120), // cap at available terminal width or 120
575587
)
576588
const wrappedLines: string[] = []
577589

npm-app/src/utils/xml-stream-parser.ts

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import { defaultToolCallRenderer } from './tool-renderers'
1111
import type { ToolCallRenderer } from './tool-renderers'
1212
import { MarkdownStreamRenderer } from '../display/markdown-renderer'
1313

14+
// Track active renderer instances with reference counting
15+
let activeRendererCount = 0
16+
1417
/**
1518
* Creates a transform stream that processes XML tool calls
1619
* @param renderer Custom renderer for tool calls or a map of renderers per tool
@@ -24,12 +27,33 @@ export function createXMLStreamParser(
2427
// Create parser with tool schema validation
2528
const parser = new Saxy({ [toolXmlName]: [] })
2629

27-
const md = new MarkdownStreamRenderer({
28-
width: process.stdout.columns || 80,
29-
isTTY: process.stdout.isTTY,
30-
syntaxHighlight: true,
31-
streamingMode: 'smart', // Use smart content-aware streaming with loading indicators
32-
})
30+
let md: MarkdownStreamRenderer | null = null
31+
32+
function ensureRenderer() {
33+
if (!md) {
34+
md = new MarkdownStreamRenderer({
35+
width: process.stdout.columns || 80,
36+
isTTY: process.stdout.isTTY,
37+
syntaxHighlight: true,
38+
streamingMode: 'smart', // Use smart content-aware streaming with loading indicators
39+
})
40+
activeRendererCount++
41+
}
42+
return md
43+
}
44+
45+
function safeCleanup() {
46+
if (md) {
47+
try {
48+
md.cleanup()
49+
} catch (e) {
50+
// swallow errors to guarantee cleanup
51+
} finally {
52+
md = null
53+
activeRendererCount = Math.max(0, activeRendererCount - 1)
54+
}
55+
}
56+
}
3357

3458
// Current state
3559
let inToolCallTag = false
@@ -62,7 +86,7 @@ export function createXMLStreamParser(
6286

6387
parser.on('text', (data) => {
6488
if (!inToolCallTag) {
65-
const outs = md.write(data.contents)
89+
const outs = ensureRenderer().write(data.contents)
6690
for (const out of outs) {
6791
parser.push(out)
6892
if (callback) callback(out)
@@ -228,20 +252,27 @@ export function createXMLStreamParser(
228252
})
229253

230254
parser._flush = function (done: (error?: Error | null) => void) {
231-
const rem = md.end()
232-
if (rem) {
233-
this.push(rem)
234-
if (callback) callback(rem)
255+
if (md) {
256+
const rem = md.end()
257+
if (rem) {
258+
this.push(rem)
259+
if (callback) callback(rem)
260+
}
235261
}
262+
safeCleanup()
236263
done()
237264
}
238265

239266
// Override destroy to ensure markdown renderer cleanup
240267
const originalDestroy = parser.destroy.bind(parser)
241268
parser.destroy = function (error?: Error) {
242-
md.cleanup()
269+
safeCleanup()
243270
return originalDestroy(error)
244271
}
245272

246273
return parser
247274
}
275+
276+
export function getActiveRendererCount() {
277+
return activeRendererCount
278+
}

0 commit comments

Comments
 (0)