@@ -29,6 +29,19 @@ interface ToolRenderContext {
2929 relatedLinkUrls : string [ ] ;
3030}
3131
32+ interface DerivedRelatedLink {
33+ title : string ;
34+ url : string ;
35+ }
36+
37+ interface DerivedPropsRow {
38+ name : string ;
39+ type : string ;
40+ required : boolean ;
41+ description : string ;
42+ defaultValue : string | null ;
43+ }
44+
3245function getRecord ( value : unknown ) : Record < string , unknown > {
3346 if ( ! value || typeof value !== "object" ) return { } ;
3447 return value as Record < string , unknown > ;
@@ -97,6 +110,172 @@ function getReactTypeTableRowsFromOutput(output: unknown): Array<{
97110 ) ;
98111}
99112
113+ function normalizeSeedDocsUrl ( rawUrl : string ) : string | null {
114+ try {
115+ const parsed = new URL ( rawUrl ) ;
116+ const isSeedDomain =
117+ parsed . hostname === "seed-design.io" || parsed . hostname === "www.seed-design.io" ;
118+
119+ if ( ! isSeedDomain ) return null ;
120+ return `https://seed-design.io${ parsed . pathname } ${ parsed . search } ${ parsed . hash } ` ;
121+ } catch {
122+ return null ;
123+ }
124+ }
125+
126+ function extractRelatedLinksFromText ( text : string ) : { text : string ; links : DerivedRelatedLink [ ] } {
127+ if ( ! text . includes ( "http" ) ) {
128+ return { text, links : [ ] } ;
129+ }
130+
131+ const links : DerivedRelatedLink [ ] = [ ] ;
132+ const lines = text . split ( "\n" ) ;
133+ const keptLines : string [ ] = [ ] ;
134+ const markdownLinkRegex = / \[ ( [ ^ \] ] + ) \] \( ( h t t p s ? : \/ \/ [ ^ ) ] + ) \) / g;
135+
136+ for ( const line of lines ) {
137+ const markdownMatches = [ ...line . matchAll ( markdownLinkRegex ) ] ;
138+ if ( markdownMatches . length > 0 ) {
139+ let lineWithoutLinks = line ;
140+ let allSeedLinks = true ;
141+
142+ for ( const match of markdownMatches ) {
143+ const title = match [ 1 ] ?. trim ( ) ;
144+ const normalizedUrl = normalizeSeedDocsUrl ( match [ 2 ] ?? "" ) ;
145+
146+ if ( ! title || ! normalizedUrl ) {
147+ allSeedLinks = false ;
148+ continue ;
149+ }
150+
151+ links . push ( { title, url : normalizedUrl } ) ;
152+ lineWithoutLinks = lineWithoutLinks . replace ( match [ 0 ] , "" ) ;
153+ }
154+
155+ if ( allSeedLinks && lineWithoutLinks . replace ( / ^ [ \s * \- 0 - 9 . ] + / , "" ) . trim ( ) . length === 0 ) {
156+ continue ;
157+ }
158+
159+ keptLines . push ( lineWithoutLinks ) ;
160+ continue ;
161+ }
162+
163+ const rawUrlMatch = line . match ( / h t t p s ? : \/ \/ s e e d - d e s i g n \. i o [ ^ \s ) \] ] * / i) ;
164+ if ( rawUrlMatch ) {
165+ const normalizedUrl = normalizeSeedDocsUrl ( rawUrlMatch [ 0 ] ) ;
166+ if ( normalizedUrl ) {
167+ const prefix = line . slice ( 0 , rawUrlMatch . index ?? 0 ) . trim ( ) ;
168+ const title = prefix . replace ( / ^ [ - * ] \s * / , "" ) . trim ( ) || normalizedUrl ;
169+ links . push ( { title, url : normalizedUrl } ) ;
170+
171+ if ( line . replace ( rawUrlMatch [ 0 ] , "" ) . trim ( ) . length === 0 ) {
172+ continue ;
173+ }
174+ }
175+ }
176+
177+ keptLines . push ( line ) ;
178+ }
179+
180+ const deduped = Array . from (
181+ links . reduce ( ( map , link ) => {
182+ if ( ! map . has ( link . url ) ) {
183+ map . set ( link . url , link ) ;
184+ }
185+ return map ;
186+ } , new Map < string , DerivedRelatedLink > ( ) ) ,
187+ ) . map ( ( [ , link ] ) => link ) ;
188+
189+ return {
190+ text : keptLines . join ( "\n" ) ,
191+ links : deduped ,
192+ } ;
193+ }
194+
195+ function parsePropNames ( raw : string ) : string [ ] {
196+ const normalized = raw
197+ . replaceAll ( "`" , "" )
198+ . replaceAll ( "**" , "" )
199+ . replaceAll ( "'" , "" )
200+ . replaceAll ( '"' , "" )
201+ . trim ( ) ;
202+
203+ return normalized
204+ . split ( "/" )
205+ . flatMap ( ( token ) => token . split ( "," ) )
206+ . map ( ( token ) => token . trim ( ) )
207+ . filter ( Boolean ) ;
208+ }
209+
210+ function extractPropsTableRowsFromText ( text : string ) : { text : string ; rows : DerivedPropsRow [ ] } {
211+ const propsSectionRegex =
212+ / (?: ^ | \n ) # { 1 , 6 } \s * (?: 주 요 \s * ) ? p r o p s [ ^ \n ] * \n ( [ \s \S ] * ?) (? = \n # { 1 , 6 } \s | \n - - - | \n $ | $ ) / i;
213+ const match = text . match ( propsSectionRegex ) ;
214+ if ( ! match || ! match [ 1 ] ) {
215+ return { text, rows : [ ] } ;
216+ }
217+
218+ const sectionBody = match [ 1 ] ;
219+ const rows : DerivedPropsRow [ ] = [ ] ;
220+
221+ for ( const line of sectionBody . split ( "\n" ) ) {
222+ const bulletMatch = line . match ( / ^ \s * [ - * ] \s * ( .+ ?) \s * : \s * ( .+ ) \s * $ / ) ;
223+ if ( ! bulletMatch ) continue ;
224+
225+ const names = parsePropNames ( bulletMatch [ 1 ] ) ;
226+ const description = bulletMatch [ 2 ] . trim ( ) ;
227+ if ( ! description || names . length === 0 ) continue ;
228+
229+ for ( const name of names ) {
230+ rows . push ( {
231+ name,
232+ type : "-" ,
233+ required : false ,
234+ description,
235+ defaultValue : null ,
236+ } ) ;
237+ }
238+ }
239+
240+ if ( rows . length === 0 ) {
241+ return { text, rows : [ ] } ;
242+ }
243+
244+ return {
245+ text : text . replace ( propsSectionRegex , "\n" ) ,
246+ rows,
247+ } ;
248+ }
249+
250+ function extractInstallTargetFromCode ( code : string , language : string ) : string | null {
251+ if ( ! / ( b a s h | s h | z s h | s h e l l | c o n s o l e ) / i. test ( language ) && ! code . includes ( "@seed-design/cli@latest" ) ) {
252+ return null ;
253+ }
254+
255+ const match = code . match ( / @ s e e d - d e s i g n \/ c l i @ l a t e s t \s + a d d \s + ( [ ^ \s ] + ) / i) ;
256+ if ( ! match ?. [ 1 ] ) return null ;
257+ return match [ 1 ] . trim ( ) ;
258+ }
259+
260+ function extractComponentPreviewNameFromCode ( code : string , language : string ) : string | null {
261+ if ( ! / ( t s x | j s x | t y p e s c r i p t | j a v a s c r i p t | t s | j s ) / i. test ( language ) ) {
262+ return null ;
263+ }
264+
265+ const match = code . match ( / f r o m \s + [ " ' ] s e e d - d e s i g n \/ u i \/ ( [ a - z 0 - 9 - ] + ) [ " ' ] / i) ;
266+ if ( ! match ?. [ 1 ] ) return null ;
267+
268+ return `react/${ match [ 1 ] } /preview` ;
269+ }
270+
271+ function isRelatedLinksToolPart ( part : UIMessage [ "parts" ] [ number ] ) : boolean {
272+ if ( part . type === "dynamic-tool" ) {
273+ return part . toolName === "findRelatedLinks" ;
274+ }
275+
276+ return typeof part . type === "string" && part . type === "tool-findRelatedLinks" ;
277+ }
278+
100279function getToolCopyText ( toolName : string , input : unknown , output : unknown ) : string [ ] {
101280 const lines : string [ ] = [ ] ;
102281 const safeInput = getRecord ( input ) ;
@@ -325,6 +504,7 @@ function sanitizeTextForTools(text: string, toolContext: ToolRenderContext): str
325504 }
326505
327506 sanitized = sanitized
507+ . replace ( / ^ # { 1 , 6 } \s * / gm, "" )
328508 . split ( "\n" )
329509 . map ( ( line ) => line . replace ( / \s + $ / g, "" ) )
330510 . join ( "\n" )
@@ -420,6 +600,16 @@ export function ChatMessage({ message }: { message: UIMessage }) {
420600 }
421601 } ;
422602
603+ const orderedParts = isUser
604+ ? message . parts
605+ : [
606+ ...message . parts . filter ( ( part ) => ! isRelatedLinksToolPart ( part ) ) ,
607+ ...message . parts . filter ( ( part ) => isRelatedLinksToolPart ( part ) ) ,
608+ ] ;
609+
610+ const derivedRelatedLinks : DerivedRelatedLink [ ] = [ ] ;
611+ const derivedPropsRows : DerivedPropsRow [ ] = [ ] ;
612+
423613 return (
424614 < div className = { `flex ${ isUser ? "justify-end" : "justify-start" } ` } >
425615 < div className = { isUser ? "max-w-[90%]" : "min-w-[85%] max-w-[90%]" } >
@@ -430,7 +620,7 @@ export function ChatMessage({ message }: { message: UIMessage }) {
430620 : ""
431621 } `}
432622 >
433- { message . parts . map ( ( part , i ) => {
623+ { orderedParts . map ( ( part , i ) => {
434624 if ( part . type === "text" && part . text ) {
435625 const segments = ! isUser
436626 ? parseMarkdownCodeBlocks ( part . text )
@@ -449,6 +639,43 @@ export function ChatMessage({ message }: { message: UIMessage }) {
449639 return null ;
450640 }
451641
642+ if ( ! isUser ) {
643+ const installTarget = ! toolContext . hasInstallation
644+ ? extractInstallTargetFromCode ( segment . code , segment . language )
645+ : null ;
646+
647+ if ( installTarget ) {
648+ return (
649+ < ToolResultRenderer
650+ key = { `derived-install-${ i } -${ segmentIndex } ` }
651+ toolName = "showInstallation"
652+ input = { { name : installTarget } }
653+ state = "output-available"
654+ />
655+ ) ;
656+ }
657+
658+ const previewName =
659+ ! toolContext . hasComponentExample && ! toolContext . hasCodeTool
660+ ? extractComponentPreviewNameFromCode ( segment . code , segment . language )
661+ : null ;
662+
663+ if ( previewName ) {
664+ return (
665+ < ToolResultRenderer
666+ key = { `derived-preview-${ i } -${ segmentIndex } ` }
667+ toolName = "showComponentExample"
668+ input = { {
669+ name : previewName ,
670+ code : segment . code ,
671+ language : segment . language ,
672+ } }
673+ state = "output-available"
674+ />
675+ ) ;
676+ }
677+ }
678+
452679 return (
453680 < div key = { `segment-code-${ segmentIndex } ` } className = "my-2" >
454681 < DynamicCodeBlock lang = { segment . language } code = { segment . code } />
@@ -468,6 +695,49 @@ export function ChatMessage({ message }: { message: UIMessage }) {
468695 return null ;
469696 }
470697
698+ if ( ! isUser ) {
699+ let derivedText = visibleText ;
700+
701+ if ( ! toolContext . hasRelatedLinks ) {
702+ const relatedExtracted = extractRelatedLinksFromText ( derivedText ) ;
703+ if ( relatedExtracted . links . length > 0 ) {
704+ for ( const link of relatedExtracted . links ) {
705+ if (
706+ ! derivedRelatedLinks . some ( ( existing ) => existing . url === link . url )
707+ ) {
708+ derivedRelatedLinks . push ( link ) ;
709+ }
710+ }
711+ }
712+ derivedText = relatedExtracted . text ;
713+ }
714+
715+ if ( ! toolContext . hasReactTypeTable ) {
716+ const propsExtracted = extractPropsTableRowsFromText ( derivedText ) ;
717+ if ( propsExtracted . rows . length > 0 ) {
718+ for ( const row of propsExtracted . rows ) {
719+ if ( ! derivedPropsRows . some ( ( existing ) => existing . name === row . name ) ) {
720+ derivedPropsRows . push ( row ) ;
721+ }
722+ }
723+ }
724+ derivedText = propsExtracted . text ;
725+ }
726+
727+ if ( ! derivedText . trim ( ) ) {
728+ return null ;
729+ }
730+
731+ return (
732+ < div
733+ key = { `segment-text-${ segmentIndex } ` }
734+ className = "text-sm whitespace-pre-wrap break-words prose prose-sm dark:prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
735+ >
736+ { derivedText }
737+ </ div >
738+ ) ;
739+ }
740+
471741 return (
472742 < div
473743 key = { `segment-text-${ segmentIndex } ` }
@@ -521,6 +791,24 @@ export function ChatMessage({ message }: { message: UIMessage }) {
521791
522792 return null ;
523793 } ) }
794+
795+ { ! isUser && ! toolContext . hasReactTypeTable && derivedPropsRows . length > 0 && (
796+ < ToolResultRenderer
797+ toolName = "showReactTypeTable"
798+ input = { { } }
799+ state = "output-available"
800+ output = { { rows : derivedPropsRows } }
801+ />
802+ ) }
803+
804+ { ! isUser && ! toolContext . hasRelatedLinks && derivedRelatedLinks . length > 0 && (
805+ < ToolResultRenderer
806+ toolName = "findRelatedLinks"
807+ input = { { } }
808+ state = "output-available"
809+ output = { { links : derivedRelatedLinks } }
810+ />
811+ ) }
524812 </ div >
525813
526814 { ! isUser && (
0 commit comments