Skip to content

Commit 06b8b77

Browse files
committed
feat(cli): add UserErrorBanner component
1 parent b651b46 commit 06b8b77

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { describe, test, expect } from 'bun:test'
2+
import React from 'react'
3+
import { renderToStaticMarkup } from 'react-dom/server'
4+
5+
import { initializeThemeStore } from '../../hooks/use-theme'
6+
import { UserErrorBanner } from '../user-error-banner'
7+
8+
initializeThemeStore()
9+
10+
describe('UserErrorBanner', () => {
11+
test('renders error message', () => {
12+
const markup = renderToStaticMarkup(
13+
<UserErrorBanner error="Something went wrong" />,
14+
)
15+
16+
expect(markup).toContain('Error')
17+
expect(markup).toContain('Something went wrong')
18+
})
19+
20+
test('renders with context length exceeded error', () => {
21+
const errorMessage =
22+
"This endpoint's maximum context length is 200000 tokens. However, you requested about 201209 tokens."
23+
24+
const markup = renderToStaticMarkup(
25+
<UserErrorBanner error={errorMessage} />,
26+
)
27+
28+
expect(markup).toContain('Error')
29+
expect(markup).toContain('200000 tokens')
30+
expect(markup).toContain('201209 tokens')
31+
})
32+
33+
test('renders with network error', () => {
34+
const markup = renderToStaticMarkup(
35+
<UserErrorBanner error="Network request failed: Connection refused" />,
36+
)
37+
38+
expect(markup).toContain('Error')
39+
expect(markup).toContain('Network request failed')
40+
expect(markup).toContain('Connection refused')
41+
})
42+
43+
test('returns null for empty error message', () => {
44+
const markup = renderToStaticMarkup(<UserErrorBanner error="" />)
45+
46+
// Empty error should render nothing
47+
expect(markup).toBe('')
48+
})
49+
50+
test('returns null for whitespace-only error message', () => {
51+
const markup = renderToStaticMarkup(<UserErrorBanner error=" " />)
52+
53+
// Whitespace-only error should render nothing
54+
expect(markup).toBe('')
55+
})
56+
57+
test('renders with multiline error message', () => {
58+
const multilineError = 'First line of error\nSecond line of error'
59+
60+
const markup = renderToStaticMarkup(
61+
<UserErrorBanner error={multilineError} />,
62+
)
63+
64+
expect(markup).toContain('Error')
65+
expect(markup).toContain('First line of error')
66+
expect(markup).toContain('Second line of error')
67+
})
68+
69+
test('renders with special characters in error message', () => {
70+
const specialCharsError = 'Error with <html> tags & "quotes"'
71+
72+
const markup = renderToStaticMarkup(
73+
<UserErrorBanner error={specialCharsError} />,
74+
)
75+
76+
expect(markup).toContain('Error')
77+
// HTML entities should be escaped in the markup
78+
expect(markup).toContain('&lt;html&gt;')
79+
expect(markup).toContain('&amp;')
80+
expect(markup).toContain('&quot;quotes&quot;')
81+
})
82+
83+
test('renders with long error message', () => {
84+
const longError = 'A'.repeat(500)
85+
86+
const markup = renderToStaticMarkup(
87+
<UserErrorBanner error={longError} />,
88+
)
89+
90+
expect(markup).toContain('Error')
91+
expect(markup).toContain(longError)
92+
})
93+
94+
test('renders with custom title', () => {
95+
const markup = renderToStaticMarkup(
96+
<UserErrorBanner error="Something went wrong" title="Network Error" />,
97+
)
98+
99+
expect(markup).toContain('Network Error')
100+
expect(markup).toContain('Something went wrong')
101+
})
102+
})
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react'
2+
3+
import { useTheme } from '../hooks/use-theme'
4+
import { BORDER_CHARS } from '../utils/ui-constants'
5+
6+
interface UserErrorBannerProps {
7+
error: string
8+
title?: string
9+
}
10+
11+
/** Displays runtime errors in the UI (not sent to LLM). */
12+
export const UserErrorBanner = React.memo(function UserErrorBanner({
13+
error,
14+
title,
15+
}: UserErrorBannerProps) {
16+
const theme = useTheme()
17+
18+
// Handle empty and whitespace-only errors
19+
const trimmedError = error.trim()
20+
if (!trimmedError) {
21+
return null
22+
}
23+
24+
return (
25+
<box
26+
style={{
27+
width: '100%',
28+
borderStyle: 'single',
29+
borderColor: theme.error,
30+
customBorderChars: BORDER_CHARS,
31+
paddingLeft: 1,
32+
paddingRight: 1,
33+
paddingTop: 0,
34+
paddingBottom: 0,
35+
flexDirection: 'column',
36+
gap: 0,
37+
marginTop: 1,
38+
}}
39+
>
40+
<box
41+
style={{
42+
flexDirection: 'column',
43+
justifyContent: 'center',
44+
gap: 0,
45+
}}
46+
>
47+
<text style={{ fg: theme.error, wrapMode: 'word' }}>
48+
{title ?? 'Error'}
49+
</text>
50+
<text style={{ fg: theme.foreground, wrapMode: 'word' }}>
51+
{error}
52+
</text>
53+
</box>
54+
</box>
55+
)
56+
})

0 commit comments

Comments
 (0)