@@ -4,20 +4,21 @@ import { UIMessage } from "ai";
44import {
55 Check ,
66 Copy ,
7- ChevronDown ,
87 Loader ,
98 Pencil ,
109 ChevronDownIcon ,
1110 RefreshCw ,
1211 X ,
1312 Wrench ,
1413 Trash2 ,
14+ ChevronRight ,
15+ TriangleAlert ,
1516} from "lucide-react" ;
1617import { Tooltip , TooltipContent , TooltipTrigger } from "ui/tooltip" ;
1718import { Button } from "ui/button" ;
1819import { Markdown } from "./markdown" ;
1920import { MessagePastesContentCard } from "./message-pasts-content" ;
20- import { cn , safeJSONParse , toAny } from "lib/utils" ;
21+ import { cn , safeJSONParse } from "lib/utils" ;
2122import JsonView from "ui/json-view" ;
2223import {
2324 useMemo ,
@@ -32,7 +33,6 @@ import { MessageEditor } from "./message-editor";
3233import type { UseChatHelpers } from "@ai-sdk/react" ;
3334import { useCopy } from "@/hooks/use-copy" ;
3435
35- import { Card , CardContent } from "ui/card" ;
3636import { AnimatePresence , motion } from "framer-motion" ;
3737import { SelectModel } from "./select-model" ;
3838import {
@@ -49,6 +49,9 @@ import { PieChart } from "./tool-invocation/pie-chart";
4949import { BarChart } from "./tool-invocation/bar-chart" ;
5050import { LineChart } from "./tool-invocation/line-chart" ;
5151import { PROMPT_PASTE_MAX_LENGTH } from "lib/const" ;
52+ import { useTranslations } from "next-intl" ;
53+ import { extractMCPToolId } from "lib/ai/mcp/mcp-tool-id" ;
54+ import { Separator } from "ui/separator" ;
5255
5356type MessagePart = UIMessage [ "parts" ] [ number ] ;
5457
@@ -175,12 +178,9 @@ export const UserMessagePart = ({
175178 return (
176179 < div className = "flex flex-col gap-2 items-end my-2" >
177180 < div
178- onClick = { ( ) => {
179- ref . current ?. scrollIntoView ( { behavior : "smooth" } ) ;
180- } }
181181 data-testid = "message-content"
182182 className = { cn (
183- "flex flex-col gap-4" ,
183+ "flex flex-col gap-4 max-w-full " ,
184184 {
185185 "bg-accent text-accent-foreground px-4 py-3 rounded-2xl" :
186186 isLast || part . text . length <= PROMPT_PASTE_MAX_LENGTH ,
@@ -190,7 +190,7 @@ export const UserMessagePart = ({
190190 ) }
191191 >
192192 { isLast || part . text . length <= PROMPT_PASTE_MAX_LENGTH ? (
193- < p className = { cn ( "whitespace-pre-wrap text-sm" ) } >
193+ < p className = { cn ( "whitespace-pre-wrap text-sm break-words " ) } >
194194 < HighlightedText text = { part . text } mentions = { toolMentions } />
195195 </ p >
196196 ) : (
@@ -401,9 +401,10 @@ export const ToolMessagePart = memo(
401401 message,
402402 setMessages,
403403 } : ToolMessagePartProps ) => {
404+ const t = useTranslations ( "Common" ) ;
404405 const { toolInvocation } = part ;
405406 const { toolName, toolCallId, state, args } = toolInvocation ;
406- const [ isExpanded , setIsExpanded ] = useState ( false ) ;
407+ const [ expanded , setExpanded ] = useState ( false ) ;
407408 const { copied : copiedInput , copy : copyInput } = useCopy ( ) ;
408409 const { copied : copiedOutput , copy : copyOutput } = useCopy ( ) ;
409410 const [ isDeleting , setIsDeleting ] = useState ( false ) ;
@@ -486,112 +487,138 @@ export const ToolMessagePart = memo(
486487 return null ;
487488 } , [ state , toolInvocation ] ) ;
488489
490+ const { serverName : mcpServerName , toolName : mcpToolName } = useMemo ( ( ) => {
491+ return extractMCPToolId ( toolName ) ;
492+ } , [ toolName ] ) ;
493+
494+ const isExpanded = useMemo ( ( ) => {
495+ return expanded || result === null ;
496+ } , [ expanded , result ] ) ;
497+
489498 return (
490- < div key = { toolCallId } className = "flex flex-col gap-2 group w-full" >
499+ < div key = { toolCallId } className = "group w-full" >
491500 { ToolResultComponent ? (
492501 ToolResultComponent
493502 ) : (
494- < >
495- < div className = "flex flex-row gap-2 items-center cursor-pointer" >
496- < Button
497- onClick = { ( ) => setIsExpanded ( ! isExpanded ) }
498- variant = "outline"
499- className = { cn (
500- "flex flex-row gap-2 justify-between items-center text-muted-foreground min-w-44 bg-card" ,
501- isExecuting && "animate-pulse bg-input" ,
502- isError && "border-destructive" ,
503- ) }
504- >
505- < Wrench className = "size-3.5" />
506- < p className = { cn ( "font-bold" ) } > { toolName } </ p >
503+ < div className = "flex flex-col fade-in duration-300 animate-in" >
504+ < div
505+ className = "flex gap-2 items-center cursor-pointer group/title"
506+ onClick = { ( ) => setExpanded ( ! expanded ) }
507+ >
508+ < div className = "p-1.5 text-primary bg-input/40 rounded" >
507509 { isExecuting ? (
508- < Loader className = "size-3 animate-spin" />
510+ < Loader className = "size-3.5 animate-spin" />
511+ ) : isError ? (
512+ < TriangleAlert className = "size-3.5 text-destructive" />
509513 ) : (
510- < ChevronDown
511- className = { cn (
512- isExpanded && "rotate-180" ,
513- "transition-transform" ,
514- "size-4" ,
515- ) }
516- />
514+ < Wrench className = "size-3.5" />
517515 ) }
518- </ Button >
519- { onPoxyToolCall && (
516+ </ div >
517+ < span className = "font-bold flex items-center gap-2" >
518+ { mcpServerName }
519+ </ span >
520+ { mcpToolName && (
520521 < >
521- < Button
522- variant = "outline"
523- className = "bg-input"
524- size = "icon"
525- onClick = { ( ) => onPoxyToolCall ( true ) }
526- >
527- < Check />
528- </ Button >
529- < Button
530- variant = "outline"
531- size = "icon"
532- onClick = { ( ) => onPoxyToolCall ( false ) }
533- >
534- < X />
535- </ Button >
522+ < ChevronRight className = "size-3.5" />
523+ < span className = "text-muted-foreground group-hover/title:text-primary transition-colors duration-300" >
524+ { mcpToolName }
525+ </ span >
536526 </ >
537527 ) }
528+ < div className = "ml-auto group-hover/title:bg-input p-1.5 rounded transition-colors duration-300" >
529+ < ChevronDownIcon
530+ className = { cn ( isExpanded && "rotate-180" , "size-3.5" ) }
531+ />
532+ </ div >
538533 </ div >
539- { isExpanded && (
540- < Card className = "relative mt-2 p-4 max-h-[50vh] overflow-y-auto bg-card" >
541- < CardContent className = "flex flex-row gap-4 text-sm " >
542- < div className = "w-1/2 min-w-0 flex flex-col" >
543- < div className = "flex items-center gap-2 mb-2 pt-2 pb-1 z-10" >
544- < h5 className = "text-muted-foreground text-sm font-medium" >
545- Inputs
546- </ h5 >
547- < div className = "flex-1" />
548-
549- { copiedInput ? (
550- < Check className = "size-4" />
551- ) : (
552- < Button
553- variant = "ghost"
554- size = "icon"
555- className = "size-3 text-muted-foreground"
556- onClick = { ( ) =>
557- copyInput ( JSON . stringify ( toolInvocation . args ) )
558- }
559- >
560- < Copy />
561- </ Button >
562- ) }
563- </ div >
564- < JsonView data = { toolInvocation . args } />
534+ < div className = "flex gap-2 py-2" >
535+ < div className = "w-7 flex justify-center" >
536+ < Separator
537+ orientation = "vertical"
538+ className = "h-full bg-gradient-to-t from-transparent to-border to-5%"
539+ />
540+ </ div >
541+ < div className = "w-full flex flex-col gap-2" >
542+ < div className = "min-w-0 w-full p-4 rounded-lg bg-card px-4 border text-xs" >
543+ < div className = "flex items-center" >
544+ < h5 className = "text-muted-foreground font-medium select-none" >
545+ Request
546+ </ h5 >
547+ < div className = "flex-1" />
548+ { copiedInput ? (
549+ < Check className = "size-3" />
550+ ) : (
551+ < Button
552+ variant = "ghost"
553+ size = "icon"
554+ className = "size-3 text-muted-foreground"
555+ onClick = { ( ) =>
556+ copyInput ( JSON . stringify ( toolInvocation . args ) )
557+ }
558+ >
559+ < Copy />
560+ </ Button >
561+ ) }
565562 </ div >
566-
567- < div className = "w-1/2 min-w-0 pl-4 flex flex-col" >
568- < div className = "flex items-center gap-2 mb-4 pt-2 pb-1 z-10" >
569- < h5 className = "text-muted-foreground text-sm font-medium" >
570- Outputs
563+ { isExpanded && (
564+ < div className = "p-2 max-h-[300px] overflow-y-auto " >
565+ < JsonView data = { toolInvocation . args } />
566+ </ div >
567+ ) }
568+ </ div >
569+ { result && (
570+ < div className = "min-w-0 w-full p-4 rounded-lg bg-card px-4 border text-xs mt-2" >
571+ < div className = "flex items-center" >
572+ < h5 className = "text-muted-foreground font-medium select-none" >
573+ Response
571574 </ h5 >
572575 < div className = "flex-1" />
573576 { copiedOutput ? (
574- < Check className = "size-4 " />
577+ < Check className = "size-3 " />
575578 ) : (
576579 < Button
577580 variant = "ghost"
578581 size = "icon"
579582 className = "size-3 text-muted-foreground"
580- onClick = { ( ) =>
581- copyOutput (
582- JSON . stringify ( toAny ( toolInvocation ) . result ) ,
583- )
584- }
583+ onClick = { ( ) => copyOutput ( JSON . stringify ( result ) ) }
585584 >
586585 < Copy />
587586 </ Button >
588587 ) }
589588 </ div >
590- < JsonView data = { result } />
589+ { isExpanded && (
590+ < div className = "p-2 max-h-[300px] overflow-y-auto" >
591+ < JsonView data = { result } />
592+ </ div >
593+ ) }
591594 </ div >
592- </ CardContent >
593- </ Card >
594- ) }
595+ ) }
596+
597+ { onPoxyToolCall && (
598+ < div className = "flex flex-row gap-2 items-center mt-2" >
599+ < Button
600+ variant = "secondary"
601+ size = "sm"
602+ className = "rounded-full text-xs hover:ring"
603+ onClick = { ( ) => onPoxyToolCall ( true ) }
604+ >
605+ < Check />
606+ { t ( "approve" ) }
607+ </ Button >
608+ < Button
609+ variant = "outline"
610+ size = "sm"
611+ className = "rounded-full text-xs"
612+ onClick = { ( ) => onPoxyToolCall ( false ) }
613+ >
614+ < X />
615+ { t ( "reject" ) }
616+ </ Button >
617+ </ div >
618+ ) }
619+ </ div >
620+ </ div >
621+
595622 { showActions && (
596623 < div className = "flex flex-row gap-2 items-center" >
597624 < Tooltip >
@@ -616,7 +643,7 @@ export const ToolMessagePart = memo(
616643 </ Tooltip >
617644 </ div >
618645 ) }
619- </ >
646+ </ div >
620647 ) }
621648 </ div >
622649 ) ;
0 commit comments