Skip to content

Commit 557675c

Browse files
committed
improve CLI rendering: faster loading indicators, better markdown
parsing, and null safety - Reduce loading indicator threshold from 1000ms to 50ms for more responsive feedback - Enable breaks in markdown-it to preserve newlines in code content - Add indented code block detection for consistent handling - Improve tool renderer null safety and refined newline trimming logic - Add comprehensive null checks in XML stream parser to prevent crashes 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent 584f1c1 commit 557675c

File tree

3 files changed

+41
-25
lines changed

3 files changed

+41
-25
lines changed

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

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

7373
constructor(opts: MarkdownStreamRendererOptions = {}) {
@@ -80,7 +80,7 @@ export class MarkdownStreamRenderer {
8080
// Initialize markdown-it with terminal renderer
8181
this.md = new MarkdownIt({
8282
html: false,
83-
breaks: false,
83+
breaks: true, // Enable breaks to preserve newlines in code-like content
8484
linkify: false,
8585
typographer: false,
8686
highlight: this.syntaxHighlight
@@ -219,6 +219,11 @@ export class MarkdownStreamRenderer {
219219
return 'code-fence'
220220
}
221221

222+
// Indented code block (4+ spaces)
223+
if (line.match(/^ /) && trimmed.length > 0) {
224+
return 'code-fence' // Treat as code-fence for consistent handling
225+
}
226+
222227
// Heading
223228
if (trimmed.match(/^#+\s/)) {
224229
return 'heading'
@@ -291,9 +296,14 @@ export class MarkdownStreamRenderer {
291296
)
292297

293298
case 'code-fence':
299+
// Handle fenced code blocks
294300
if (block.metadata?.fenceMarker) {
295301
return currentLine.trim() === block.metadata.fenceMarker
296302
}
303+
// Handle indented code blocks - complete when next line isn't indented
304+
if (!block.metadata?.fenceMarker) {
305+
return !nextLine?.match(/^ /) && trimmedNext !== ''
306+
}
297307
return false
298308

299309
case 'list':
@@ -636,9 +646,9 @@ export class MarkdownStreamRenderer {
636646
)
637647
rightNL++
638648

639-
// Don't add extra newlines - rely on cleanup to normalize spacing
640-
const needLeft = 0
641-
const needRight = 0
649+
// Add padding: ensure at least one blank line before and after code blocks
650+
const needLeft = leftNL < 2 ? 2 - leftNL : 0
651+
const needRight = rightNL < 2 ? 2 - rightNL : 0
642652

643653
return `${'\n'.repeat(needLeft)}${block}${'\n'.repeat(needRight)}`
644654
},
@@ -647,15 +657,6 @@ export class MarkdownStreamRenderer {
647657
rendered = rendered.replace(/^ \x1b\[0m\* /gm, ' \x1b[0m• ')
648658
rendered = rendered.replace(/^ \x1b\[0m(\d+) /gm, ' \x1b[0m$1. ')
649659

650-
// Normalize spacing around code blocks to ensure consistent single blank lines
651-
// Pattern: paragraph\n\n\ncode -> paragraph\n\ncode (single blank line before)
652-
rendered = rendered.replace(
653-
/(\x1b\[0m\n)\n+(\x1b\[0m\n\x1b\[48;5;236m)/g,
654-
'$1\n$2',
655-
)
656-
// Pattern: code\n\n\nparagraph -> code\n\nparagraph (single blank line after)
657-
rendered = rendered.replace(/(\x1b\[0m\n)\n+(\x1b\[0m[^0])/g, '$1\n$2')
658-
659660
// Preserve spacing around agent completion messages (lines with dashes)
660661
// First, protect agent completion messages from normalization
661662
const protectedLines: string[] = []
@@ -681,7 +682,7 @@ export class MarkdownStreamRenderer {
681682
protectionIndex = 0
682683
rendered = rendered.replace(
683684
/__PROTECTED_AGENT_MESSAGE_(\d+)__/g,
684-
() => protectedLines[protectionIndex++],
685+
(_, idx) => protectedLines[+idx] ?? '',
685686
)
686687

687688
return rendered

npm-app/src/utils/tool-renderers.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,11 @@ export const defaultToolCallRenderer: ToolCallRenderer = {
9797
},
9898

9999
onParamChunk: (content, paramName, toolName) => {
100-
if (toolStart && content.startsWith('\n')) content = content.slice(1)
100+
if (content == null || content === '') return null
101+
// Only trim the first newline if there are multiple leading newlines
102+
if (toolStart && content.startsWith('\n\n')) {
103+
content = content.slice(1)
104+
}
101105
toolStart = false
102106
return gray(content)
103107
},

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

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,15 @@ export function createXMLStreamParser(
8181

8282
// Set up event handlers
8383
parser.on('tagopen', (tag) => {
84-
inToolCallTag = true
84+
if (tag.name === toolXmlName) {
85+
inToolCallTag = true
86+
}
8587
})
8688

8789
parser.on('text', (data) => {
90+
if (!data || typeof data.contents !== 'string') {
91+
return
92+
}
8893
if (!inToolCallTag) {
8994
const outs = ensureRenderer().write(data.contents)
9095
for (const out of outs) {
@@ -133,7 +138,11 @@ export function createXMLStreamParser(
133138

134139
// handle tool params
135140
const stringValue =
136-
typeof value === 'string' ? value : JSON.stringify(value)
141+
value == null
142+
? ''
143+
: typeof value === 'string'
144+
? value
145+
: JSON.stringify(value) ?? ''
137146
if (key === endsAgentStepParam) {
138147
continue
139148
}
@@ -144,7 +153,7 @@ export function createXMLStreamParser(
144153
const output = toolRenderer.onParamEnd(
145154
currentParam,
146155
toolName,
147-
result[currentParam],
156+
result[currentParam] ?? '',
148157
)
149158
if (typeof output === 'string') {
150159
parser.push(output)
@@ -172,7 +181,7 @@ export function createXMLStreamParser(
172181
}
173182
}
174183
}
175-
if (toolRenderer.onParamChunk) {
184+
if (toolRenderer.onParamChunk && stringValue !== '') {
176185
const output = toolRenderer.onParamChunk(stringValue, key, toolName)
177186
if (typeof output === 'string') {
178187
parser.push(output)
@@ -187,9 +196,11 @@ export function createXMLStreamParser(
187196
const output = toolRenderer.onParamEnd(
188197
key,
189198
toolName,
190-
typeof result[key] === 'string'
191-
? result[key]
192-
: JSON.stringify(result[key]),
199+
result[key] == null
200+
? ''
201+
: typeof result[key] === 'string'
202+
? result[key]
203+
: JSON.stringify(result[key]) ?? '',
193204
)
194205
if (typeof output === 'string') {
195206
parser.push(output)
@@ -205,7 +216,7 @@ export function createXMLStreamParser(
205216
params = Object.fromEntries(
206217
Object.entries(result).map(([k, v]) => [
207218
k,
208-
typeof v === 'string' ? v : JSON.stringify(v),
219+
v == null ? '' : typeof v === 'string' ? v : JSON.stringify(v) ?? '',
209220
]),
210221
)
211222
})
@@ -222,7 +233,7 @@ export function createXMLStreamParser(
222233
const output = toolRenderer.onParamEnd(
223234
currentParam,
224235
toolName,
225-
params[currentParam],
236+
params[currentParam] ?? '',
226237
)
227238
if (typeof output === 'string') {
228239
parser.push(output)

0 commit comments

Comments
 (0)