Skip to content

Commit 50f1a4c

Browse files
committed
fix: better validation error formatting
1 parent 91d6560 commit 50f1a4c

File tree

8 files changed

+278
-40
lines changed

8 files changed

+278
-40
lines changed

cli/src/chat.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
} from './utils/theme-system'
5454
import { openFileAtPath } from './utils/open-file'
5555
import { formatValidationError } from './utils/validation-error-formatting'
56+
import { createValidationErrorBlocks } from './utils/create-validation-error-blocks'
5657

5758
import type { User } from './utils/auth'
5859
import type { ToolName } from '@codebuff/sdk'
@@ -122,6 +123,7 @@ export type ChatMessage = {
122123
credits?: number
123124
completionTime?: string
124125
isComplete?: boolean
126+
metadata?: Record<string, any>
125127
}
126128

127129
export const App = ({
@@ -291,9 +293,30 @@ export const App = ({
291293

292294
// Set as collapsed by default
293295
setCollapsedAgents((prev) => new Set([...prev, agentListId]))
294-
setMessages([initialMessage])
296+
297+
const messagesToAdd: ChatMessage[] = [initialMessage]
298+
299+
// Add validation error message if there are errors
300+
if (validationErrors.length > 0) {
301+
const errorBlocks = createValidationErrorBlocks({
302+
errors: validationErrors,
303+
loadedAgentsData,
304+
})
305+
306+
const validationErrorMessage: ChatMessage = {
307+
id: `validation-error-${Date.now()}`,
308+
variant: 'error',
309+
content: '',
310+
blocks: errorBlocks,
311+
timestamp: new Date().toISOString(),
312+
}
313+
314+
messagesToAdd.push(validationErrorMessage)
315+
}
316+
317+
setMessages(messagesToAdd)
295318
}
296-
}, [loadedAgentsData]) // Only run when loadedAgentsData changes
319+
}, [loadedAgentsData, validationErrors]) // Run when loadedAgentsData or validationErrors change
297320

298321
const {
299322
inputValue,
@@ -780,6 +803,7 @@ export const App = ({
780803
agentId,
781804
onBeforeMessageSend: validateAgents,
782805
setMainAgentStreamStartTime,
806+
scrollToLatest,
783807
})
784808

785809
sendMessageRef.current = sendMessage
@@ -1097,9 +1121,6 @@ export const App = ({
10971121
flexGrow: 1,
10981122
}}
10991123
>
1100-
{/* Validation banner at the top */}
1101-
{renderValidationBanner()}
1102-
11031124
<box
11041125
style={{
11051126
flexDirection: 'column',

cli/src/components/terminal-link.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ export interface TerminalLinkProps {
1010
activeColor?: string
1111
underlineOnHover?: boolean
1212
isActive?: boolean
13-
onActivate?: () => void | Promise<void>
13+
onActivate?: () => void | Promise<any>
1414
containerStyle?: Record<string, unknown>
1515
lineWrap?: boolean
16+
inline?: boolean
1617
}
1718

1819
const defaultFormatLines: FormatLinesFn = (text) => [text]
@@ -28,6 +29,7 @@ export const TerminalLink: React.FC<TerminalLinkProps> = ({
2829
onActivate,
2930
containerStyle,
3031
lineWrap = false,
32+
inline = false,
3133
}) => {
3234
const [isHovered, setIsHovered] = useState(false)
3335

@@ -44,10 +46,15 @@ export const TerminalLink: React.FC<TerminalLinkProps> = ({
4446

4547
const handleActivate = useCallback(() => {
4648
if (onActivate) {
47-
void onActivate()
49+
onActivate()
4850
}
4951
}, [onActivate])
5052

53+
// For inline mode, render as a simple span (no hover/click in inline mode)
54+
if (inline) {
55+
return <span fg={displayColor}>{text}</span>
56+
}
57+
5158
return (
5259
<box
5360
style={{

cli/src/hooks/__tests__/use-send-message-timer.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe('useSendMessage timer', () => {
2424
let mockSetIsStreaming: ReturnType<typeof mock>
2525
let mockSetCanProcessQueue: ReturnType<typeof mock>
2626
let mockSetMainAgentStreamStartTime: ReturnType<typeof mock>
27+
let mockScrollToLatest: ReturnType<typeof mock>
2728
let inputRef: React.MutableRefObject<any>
2829
let activeSubagentsRef: React.MutableRefObject<Set<string>>
2930
let isChainInProgressRef: React.MutableRefObject<boolean>
@@ -60,6 +61,7 @@ describe('useSendMessage timer', () => {
6061
mockSetIsStreaming = mock(() => {})
6162
mockSetCanProcessQueue = mock(() => {})
6263
mockSetMainAgentStreamStartTime = mock(() => {})
64+
mockScrollToLatest = mock(() => {})
6365
inputRef = { current: { focus: mock(() => {}) } }
6466
activeSubagentsRef = { current: new Set() }
6567
isChainInProgressRef = { current: false }
@@ -101,6 +103,7 @@ describe('useSendMessage timer', () => {
101103
setCanProcessQueue: mockSetCanProcessQueue,
102104
abortControllerRef,
103105
setMainAgentStreamStartTime: mockSetMainAgentStreamStartTime,
106+
scrollToLatest: mockScrollToLatest,
104107
}),
105108
)
106109

@@ -157,6 +160,7 @@ describe('useSendMessage timer', () => {
157160
setCanProcessQueue: mockSetCanProcessQueue,
158161
abortControllerRef,
159162
setMainAgentStreamStartTime: mockSetMainAgentStreamStartTime,
163+
scrollToLatest: mockScrollToLatest,
160164
}),
161165
)
162166

@@ -205,6 +209,7 @@ describe('useSendMessage timer', () => {
205209
setCanProcessQueue: mockSetCanProcessQueue,
206210
abortControllerRef,
207211
setMainAgentStreamStartTime: mockSetMainAgentStreamStartTime,
212+
scrollToLatest: mockScrollToLatest,
208213
}),
209214
)
210215

cli/src/hooks/use-agent-validation.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@ export type ValidationError = {
1010
message: string
1111
}
1212

13+
export type ValidationCheckResult = {
14+
success: boolean
15+
errors: ValidationError[]
16+
}
17+
1318
type UseAgentValidationResult = {
1419
validationErrors: ValidationError[]
1520
isValidating: boolean
16-
validate: () => Promise<boolean>
21+
validate: () => Promise<ValidationCheckResult>
1722
}
1823

1924
/**
@@ -28,8 +33,8 @@ export const useAgentValidation = (
2833
const [isValidating, setIsValidating] = useState(false)
2934

3035
// Validate agents and update state
31-
// Returns true if validation passes, false if it fails
32-
const validate = useCallback(async (): Promise<boolean> => {
36+
// Returns validation result with success status and any errors
37+
const validate = useCallback(async (): Promise<ValidationCheckResult> => {
3338
setIsValidating(true)
3439

3540
try {
@@ -46,20 +51,20 @@ export const useAgentValidation = (
4651
if (validationResult.success) {
4752
logger.debug('Agent validation passed')
4853
setValidationErrors([])
49-
return true
54+
return { success: true, errors: [] }
5055
} else {
5156
logger.debug(
5257
{ errorCount: validationResult.validationErrors.length },
5358
'Agent validation found errors',
5459
)
5560
setValidationErrors(validationResult.validationErrors)
56-
return false
61+
return { success: false, errors: validationResult.validationErrors }
5762
}
5863
} catch (error) {
5964
logger.error({ error }, 'Agent validation failed with exception')
6065
// Don't update validation errors on exception - keep previous state
61-
// Return false to block message sending on validation errors
62-
return false
66+
// Return failure to block message sending on validation errors
67+
return { success: false, errors: [] }
6368
} finally {
6469
setIsValidating(false)
6570
}

cli/src/hooks/use-message-renderer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ export const useMessageRenderer = (
272272
const isUser = message.variant === 'user'
273273
const isError = message.variant === 'error'
274274
const lineColor = isError ? 'red' : isAi ? theme.aiLine : theme.userLine
275-
const textColor = isError ? 'red' : isAi ? theme.messageAiText : theme.messageUserText
275+
const textColor = isError ? theme.messageAiText : isAi ? theme.messageAiText : theme.messageUserText
276276
const timestampColor = isError ? 'red' : isAi ? theme.timestampAi : theme.timestampUser
277277
const estimatedMessageWidth = availableWidth
278278
const codeBlockWidth = Math.max(10, estimatedMessageWidth - 8)

cli/src/hooks/use-send-message.ts

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { shouldHideAgent } from '../utils/constants'
55
import { formatTimestamp } from '../utils/helpers'
66
import { loadAgentDefinitions } from '../utils/load-agent-definitions'
77
import { logger } from '../utils/logger'
8+
import { getLoadedAgentsData } from '../utils/local-agent-registry'
9+
import { createValidationErrorBlocks } from '../utils/create-validation-error-blocks'
810

911
import type { ChatMessage, ContentBlock } from '../chat'
1012
import type { AgentDefinition, ToolName } from '@codebuff/sdk'
@@ -97,8 +99,9 @@ interface UseSendMessageOptions {
9799
setCanProcessQueue: (can: boolean) => void
98100
abortControllerRef: React.MutableRefObject<AbortController | null>
99101
agentId?: string
100-
onBeforeMessageSend?: () => Promise<boolean>
102+
onBeforeMessageSend?: () => Promise<{ success: boolean; errors: Array<{ id: string; message: string }> }>
101103
setMainAgentStreamStartTime: (time: number | null) => void
104+
scrollToLatest: () => void
102105
}
103106

104107
export const useSendMessage = ({
@@ -121,6 +124,7 @@ export const useSendMessage = ({
121124
agentId,
122125
onBeforeMessageSend,
123126
setMainAgentStreamStartTime,
127+
scrollToLatest,
124128
}: UseSendMessageOptions) => {
125129
const previousRunStateRef = useRef<any>(null)
126130
const spawnAgentsMapRef = useRef<
@@ -247,19 +251,49 @@ export const useSendMessage = ({
247251

248252
const sendMessage = useCallback(
249253
async (content: string, params: { agentMode: 'FAST' | 'MAX' }) => {
254+
const { agentMode } = params
255+
const timestamp = formatTimestamp()
256+
257+
// Add user message to UI first
258+
const userMessage: ChatMessage = {
259+
id: `user-${Date.now()}`,
260+
variant: 'user',
261+
content,
262+
timestamp,
263+
}
264+
265+
applyMessageUpdate((prev) => {
266+
const newMessages = [...prev, userMessage]
267+
if (newMessages.length > 100) {
268+
return newMessages.slice(-100)
269+
}
270+
return newMessages
271+
})
272+
await yieldToEventLoop()
273+
274+
// Scroll to bottom after user message appears
275+
setTimeout(() => scrollToLatest(), 0)
276+
250277
// Validate agents before sending message (blocking)
251278
if (onBeforeMessageSend) {
252279
try {
253-
const validationPassed = await onBeforeMessageSend()
280+
const validationResult = await onBeforeMessageSend()
254281

255-
if (!validationPassed) {
282+
if (!validationResult.success) {
256283
logger.warn('Message send blocked due to agent validation errors')
257284

258-
// Add an error message to the chat
285+
// Create validation error blocks with clickable file paths
286+
const loadedAgentsData = getLoadedAgentsData()
287+
const errorBlocks = createValidationErrorBlocks({
288+
errors: validationResult.errors,
289+
loadedAgentsData,
290+
})
291+
259292
const errorMessage: ChatMessage = {
260293
id: `error-${Date.now()}`,
261294
variant: 'error',
262-
content: 'Cannot send message: Please fix agent validation errors first. Check the validation errors displayed above.',
295+
content: '',
296+
blocks: errorBlocks,
263297
timestamp: formatTimestamp(),
264298
}
265299

@@ -271,11 +305,10 @@ export const useSendMessage = ({
271305
} catch (error) {
272306
logger.error({ error }, 'Validation before message send failed with exception')
273307

274-
// Add an error message to the chat
275308
const errorMessage: ChatMessage = {
276309
id: `error-${Date.now()}`,
277310
variant: 'error',
278-
content: 'Cannot send message: Agent validation failed unexpectedly.',
311+
content: '⚠️ Agent validation failed unexpectedly. Please try again.',
279312
timestamp: formatTimestamp(),
280313
}
281314

@@ -286,24 +319,6 @@ export const useSendMessage = ({
286319
}
287320
}
288321

289-
const { agentMode } = params
290-
const timestamp = formatTimestamp()
291-
const userMessage: ChatMessage = {
292-
id: `user-${Date.now()}`,
293-
variant: 'user',
294-
content,
295-
timestamp,
296-
}
297-
298-
applyMessageUpdate((prev) => {
299-
const newMessages = [...prev, userMessage]
300-
if (newMessages.length > 100) {
301-
return newMessages.slice(-100)
302-
}
303-
return newMessages
304-
})
305-
await yieldToEventLoop()
306-
307322
setFocusedAgentId(null)
308323
setInputFocused(true)
309324
inputRef.current?.focus()
@@ -1414,6 +1429,7 @@ export const useSendMessage = ({
14141429
removeActiveSubagent,
14151430
onBeforeMessageSend,
14161431
setMainAgentStreamStartTime,
1432+
scrollToLatest,
14171433
],
14181434
)
14191435

0 commit comments

Comments
 (0)