1+ import { TextAttributes } from '@opentui/core'
12import { useRenderer , useTerminalDimensions } from '@opentui/react'
2- import React , { useCallback , useEffect , useMemo , useRef , useState } from 'react'
3+ import open from 'open'
4+ import path from 'path'
5+ import React , {
6+ type ReactNode ,
7+ useCallback ,
8+ useEffect ,
9+ useMemo ,
10+ useRef ,
11+ useState ,
12+ } from 'react'
313import stringWidth from 'string-width'
414import { useShallow } from 'zustand/react/shallow'
515
616import { AgentModeToggle } from './components/agent-mode-toggle'
717import { LoginModal } from './components/login-modal'
18+ import { TerminalLink } from './components/terminal-link'
819import {
920 MultilineInput ,
1021 type MultilineInputHandle ,
@@ -55,7 +66,18 @@ type AgentMessage = {
5566}
5667
5768export type ContentBlock =
58- | { type : 'text' ; content : string }
69+ | {
70+ type : 'text'
71+ content : string
72+ marginTop ?: number
73+ marginBottom ?: number
74+ }
75+ | {
76+ type : 'html'
77+ marginTop ?: number
78+ marginBottom ?: number
79+ render : ( context : { textColor : string ; theme : ChatTheme } ) => ReactNode
80+ }
5981 | {
6082 type : 'tool'
6183 toolCallId : string
@@ -101,6 +123,7 @@ export const App = ({
101123 requireAuth,
102124 hasInvalidCredentials,
103125 loadedAgentsData,
126+ validationErrors,
104127} : {
105128 initialPrompt : string | null
106129 agentId ?: string
@@ -110,6 +133,7 @@ export const App = ({
110133 agents : Array < { id : string ; displayName : string } >
111134 agentsDir : string
112135 } | null
136+ validationErrors : Array < { id : string ; message : string } >
113137} ) => {
114138 const renderer = useRenderer ( )
115139 const { width : measuredWidth } = useTerminalDimensions ( )
@@ -215,19 +239,141 @@ export const App = ({
215239 } )
216240 }
217241
218- blocks . push (
219- {
242+ blocks . push ( {
243+ type : 'text' ,
244+ content :
245+ 'Codebuff can read and write files in this repository, and run terminal commands to help you build.' ,
246+ } )
247+
248+ // Add validation errors if any exist
249+ if ( validationErrors . length > 0 ) {
250+ const errorCount = validationErrors . length
251+ const errorHeader =
252+ errorCount === 1
253+ ? '**⚠️ 1 agent has validation issues**'
254+ : `**⚠️ ${ errorCount } agents have validation issues**`
255+
256+ // Add header
257+ blocks . push ( {
220258 type : 'text' ,
221- content :
222- 'Codebuff can read and write files in this repository, and run terminal commands to help you build.' ,
223- } ,
224- {
225- type : 'agent-list' ,
226- id : agentListId ,
227- agents : loadedAgentsData . agents ,
228- agentsDir : loadedAgentsData . agentsDir ,
229- } ,
230- )
259+ content : `\n${ errorHeader } ` ,
260+ } )
261+
262+ // Add each error as a separate, nicely formatted block
263+ const agentInfoById = new Map (
264+ loadedAgentsData . agents . map ( ( agent ) => [ agent . id , agent ] ) ,
265+ )
266+
267+ const normalizeRelativePath = ( filePath : string ) : string => {
268+ const relativeToAgentsDir = path . relative (
269+ loadedAgentsData . agentsDir ,
270+ filePath ,
271+ )
272+ const normalized = relativeToAgentsDir . replace ( / \\ / g, '/' )
273+ return `.agents/${ normalized } `
274+ }
275+
276+ validationErrors . forEach ( ( error , errorIndex ) => {
277+ // Extract just the key error message, removing verbose schema details
278+ let message = error . message
279+ . replace ( / A g e n t " [ ^ " ] + " \s * (?: \( [ ^ ) ] + \) ) ? \s * : \s * / , '' )
280+ . replace ( / S c h e m a v a l i d a t i o n f a i l e d : \s * / i, '' )
281+ . trim ( )
282+
283+ // If message starts with JSON array, extract first error
284+ if ( message . startsWith ( '[' ) ) {
285+ try {
286+ const errors = JSON . parse ( message )
287+ if ( Array . isArray ( errors ) && errors . length > 0 ) {
288+ const firstError = errors [ 0 ]
289+ // Get the field path and message
290+ const field = firstError . path ?. join ( '.' ) || 'field'
291+ message = `${ field } : ${ firstError . message } `
292+ }
293+ } catch {
294+ // Keep original message if parsing fails
295+ }
296+ }
297+
298+ // Clean up common validation messages to be more user-friendly
299+ message = message
300+ . replace ( / I n v a l i d i n p u t : e x p e c t e d ( \w + ) , r e c e i v e d ( \w + ) / i, 'Expected $1, got $2' )
301+ . replace ( / A g e n t I D m u s t c o n t a i n o n l y l o w e r c a s e l e t t e r s , n u m b e r s , a n d h y p h e n s / i, 'ID must be lowercase with hyphens only' )
302+ . replace ( / C a n n o t s p e c i f y b o t h ( \w + ) a n d ( \w + ) \. .* / i, 'Cannot use both $1 and $2' )
303+
304+ // Take first line only and limit length
305+ message = message . split ( '\n' ) [ 0 ]
306+ if ( message . length > 80 ) {
307+ message = message . substring ( 0 , 77 ) + '...'
308+ }
309+
310+ const agentId = error . id . replace ( / _ \d + $ / , '' )
311+ const agentInfo = agentInfoById . get ( agentId )
312+ const relativePath = agentInfo
313+ ? normalizeRelativePath ( agentInfo . filePath )
314+ : null
315+
316+ const fieldMatch = message . match ( / ^ ( [ ^ : ] + ) : \s * ( .+ ) $ / )
317+ const fieldName = fieldMatch ? fieldMatch [ 1 ] : null
318+ const errorBody = fieldMatch ? fieldMatch [ 2 ] : message
319+
320+ blocks . push ( {
321+ type : 'html' ,
322+ marginTop : errorIndex === 0 ? 1 : 0 ,
323+ render : ( { textColor } ) => (
324+ < box style = { { flexDirection : 'row' , gap : 1 , alignItems : 'center' } } >
325+ < text wrap style = { { fg : textColor } } >
326+ < span attributes = { TextAttributes . BOLD } > { agentId } </ span >
327+ </ text >
328+ { relativePath ? (
329+ < TerminalLink
330+ text = { `(${ relativePath } )` }
331+ containerStyle = { {
332+ width : 'auto' ,
333+ flexDirection : 'row' ,
334+ alignItems : 'center' ,
335+ } }
336+ formatLines = { ( text ) => [ text ] }
337+ />
338+ ) : null }
339+ </ box >
340+ ) ,
341+ } )
342+
343+ blocks . push ( {
344+ type : 'html' ,
345+ marginBottom : 1 ,
346+ render : ( { textColor } ) => (
347+ < text wrap style = { { fg : textColor , marginLeft : 2 } } >
348+ { fieldName ? (
349+ < >
350+ < span attributes = { TextAttributes . ITALIC } >
351+ { `${ fieldName } :` }
352+ </ span > { ' ' }
353+ { errorBody }
354+ </ >
355+ ) : (
356+ errorBody
357+ ) }
358+ </ text >
359+ ) ,
360+ } )
361+ } )
362+
363+ // Add closing instruction
364+ blocks . push ( {
365+ type : 'text' ,
366+ content : '*Fix these in your .agents directory.*' ,
367+ marginTop : 1 ,
368+ } )
369+ }
370+
371+ blocks . push ( {
372+ type : 'agent-list' ,
373+ id : agentListId ,
374+ agents : loadedAgentsData . agents ,
375+ agentsDir : loadedAgentsData . agentsDir ,
376+ } )
231377
232378 const initialMessage : ChatMessage = {
233379 id : `system-loaded-agents-${ Date . now ( ) } ` ,
0 commit comments