Files
sim/apps/sim/components/ui/tool-call.tsx
Siddharth Ganesan 190f12fd77 feat(copilot): copilot mcp + server side copilot execution (#3173)
* v0

* v1

* Basic ss tes

* Ss tests

* Stuff

* Add mcp

* mcp v1

* Improvement

* Fix

* BROKEN

* Checkpoint

* Streaming

* Fix abort

* Things are broken

* Streaming seems to work but copilot is dumb

* Fix edge issue

* LUAAAA

* Fix stream buffer

* Fix lint

* Checkpoint

* Initial temp state, in the middle of a refactor

* Initial test shows diff store still working

* Tool refactor

* First cleanup pass complete - untested

* Continued cleanup

* Refactor

* Refactor complete - no testing yet

* Fix - cursor makes me sad

* Fix mcp

* Clean up mcp

* Updated mcp

* Add respond to subagents

* Fix definitions

* Add tools

* Add tools

* Add copilot mcp tracking

* Fix lint

* Fix mcp

* Fix

* Updates

* Clean up mcp

* Fix copilot mcp tool names to be sim prefixed

* Add opus 4.6

* Fix discovery tool

* Fix

* Remove logs

* Fix go side tool rendering

* Update docs

* Fix hydration

* Fix tool call resolution

* Fix

* Fix lint

* Fix superagent and autoallow integrations

* Fix always allow

* Update block

* Remove plan docs

* Fix hardcoded ff

* Fix dropped provider

* Fix lint

* Fix tests

* Fix dead messages array

* Fix discovery

* Fix run workflow

* Fix run block

* Fix run from block in copilot

* Fix lint

* Fix skip and mtb

* Fix typing

* Fix tool call

* Bump api version

* Fix bun lock

* Nuke bad files
2026-02-09 19:33:29 -08:00

470 lines
20 KiB
TypeScript

'use client'
import { useState } from 'react'
import { CheckCircle, ChevronDown, ChevronRight, Loader2, Settings, XCircle } from 'lucide-react'
import { Badge } from '@/components/emcn'
import { Button } from '@/components/ui/button'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import { cn } from '@/lib/core/utils/cn'
import { formatDuration } from '@/lib/core/utils/formatting'
interface ToolCallState {
id: string
name: string
displayName?: string
parameters?: Record<string, unknown>
state:
| 'detecting'
| 'pending'
| 'executing'
| 'completed'
| 'error'
| 'rejected'
| 'applied'
| 'ready_for_review'
| 'aborted'
| 'skipped'
| 'background'
startTime?: number
endTime?: number
duration?: number
result?: unknown
error?: string
progress?: string
}
interface ToolCallGroup {
id: string
toolCalls: ToolCallState[]
status: 'pending' | 'in_progress' | 'completed' | 'error'
startTime?: number
endTime?: number
summary?: string
}
interface ToolCallProps {
toolCall: ToolCallState
isCompact?: boolean
}
interface ToolCallGroupProps {
group: ToolCallGroup
isCompact?: boolean
}
interface ToolCallIndicatorProps {
type: 'status' | 'thinking' | 'execution'
content: string
toolNames?: string[]
}
// Detection State Component
export function ToolCallDetection({ content }: { content: string }) {
return (
<div className='flex min-w-0 items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-sm dark:border-blue-800 dark:bg-blue-950'>
<Loader2 className='h-4 w-4 shrink-0 animate-spin text-blue-600 dark:text-blue-400' />
<span className='min-w-0 truncate text-blue-800 dark:text-blue-200'>{content}</span>
</div>
)
}
// Execution State Component
export function ToolCallExecution({ toolCall, isCompact = false }: ToolCallProps) {
const [isExpanded, setIsExpanded] = useState(!isCompact)
return (
<div className='min-w-0 rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950'>
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
<CollapsibleTrigger asChild>
<Button
variant='ghost'
className='w-full min-w-0 justify-between px-3 py-4 hover:bg-amber-100 dark:hover:bg-amber-900'
>
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
<Settings className='h-4 w-4 shrink-0 animate-pulse text-amber-600 dark:text-amber-400' />
<span className='min-w-0 truncate font-mono text-amber-800 text-xs dark:text-amber-200'>
{toolCall.displayName || toolCall.name}
</span>
{toolCall.progress && (
<Badge
variant='outline'
className='shrink-0 text-amber-700 text-xs dark:text-amber-300'
>
{toolCall.progress}
</Badge>
)}
</div>
{isExpanded ? (
<ChevronDown className='h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400' />
) : (
<ChevronRight className='h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400' />
)}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className='min-w-0 max-w-full px-3 pb-3'>
<div className='min-w-0 max-w-full space-y-2'>
<div className='flex items-center gap-2 text-amber-700 text-xs dark:text-amber-300'>
<Loader2 className='h-3 w-3 shrink-0 animate-spin' />
<span>Executing...</span>
</div>
{toolCall.parameters &&
Object.keys(toolCall.parameters).length > 0 &&
(toolCall.name === 'make_api_request' ||
toolCall.name === 'set_environment_variables' ||
toolCall.name === 'set_global_workflow_variables') && (
<div className='min-w-0 max-w-full rounded border border-amber-200 bg-amber-50 p-2 dark:border-amber-800 dark:bg-amber-950'>
{toolCall.name === 'make_api_request' ? (
<div className='w-full overflow-hidden rounded border border-muted bg-card'>
<div className='grid grid-cols-2 gap-0 border-muted/60 border-b bg-muted/40 px-2 py-1.5'>
<div className='font-medium text-[10px] text-muted-foreground uppercase tracking-wide'>
Method
</div>
<div className='font-medium text-[10px] text-muted-foreground uppercase tracking-wide'>
Endpoint
</div>
</div>
<div className='grid grid-cols-[auto_1fr] items-center gap-2 px-2 py-2'>
<div>
<span className='inline-flex rounded bg-muted px-1.5 py-0.5 font-mono text-muted-foreground text-xs'>
{String((toolCall.parameters as any).method || '').toUpperCase() ||
'GET'}
</span>
</div>
<div className='min-w-0'>
<span
className='block overflow-x-auto whitespace-nowrap font-mono text-foreground text-xs'
title={String((toolCall.parameters as any).url || '')}
>
{String((toolCall.parameters as any).url || '') || 'URL not provided'}
</span>
</div>
</div>
</div>
) : null}
{toolCall.name === 'set_environment_variables'
? (() => {
const variables =
(toolCall.parameters as any).variables &&
typeof (toolCall.parameters as any).variables === 'object'
? (toolCall.parameters as any).variables
: {}
const entries = Object.entries(variables)
return (
<div className='w-full overflow-hidden rounded border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950'>
<div className='grid grid-cols-2 gap-0 border-amber-200/60 border-b px-2 py-1.5 dark:border-amber-800/60'>
<div className='font-medium text-[10px] text-amber-700 uppercase tracking-wide dark:text-amber-300'>
Name
</div>
<div className='font-medium text-[10px] text-amber-700 uppercase tracking-wide dark:text-amber-300'>
Value
</div>
</div>
{entries.length === 0 ? (
<div className='px-2 py-2 text-muted-foreground text-xs'>
No variables provided
</div>
) : (
<div className='divide-y divide-amber-200 dark:divide-amber-800'>
{entries.map(([k, v]) => (
<div
key={k}
className='grid grid-cols-[auto_1fr] items-center gap-2 px-2 py-1.5'
>
<div className='truncate font-medium text-amber-800 text-xs dark:text-amber-200'>
{k}
</div>
<div className='min-w-0'>
<span className='block overflow-x-auto whitespace-nowrap font-mono text-amber-700 text-xs dark:text-amber-300'>
{String(v)}
</span>
</div>
</div>
))}
</div>
)}
</div>
)
})()
: null}
{toolCall.name === 'set_global_workflow_variables'
? (() => {
const ops = Array.isArray((toolCall.parameters as any).operations)
? ((toolCall.parameters as any).operations as any[])
: []
return (
<div className='w-full overflow-hidden rounded border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950'>
<div className='grid grid-cols-3 gap-0 border-amber-200/60 border-b px-2 py-1.5 dark:border-amber-800/60'>
<div className='font-medium text-[10px] text-amber-700 uppercase tracking-wide dark:text-amber-300'>
Name
</div>
<div className='font-medium text-[10px] text-amber-700 uppercase tracking-wide dark:text-amber-300'>
Type
</div>
<div className='font-medium text-[10px] text-amber-700 uppercase tracking-wide dark:text-amber-300'>
Value
</div>
</div>
{ops.length === 0 ? (
<div className='px-2 py-2 text-muted-foreground text-xs'>
No operations provided
</div>
) : (
<div className='divide-y divide-amber-200 dark:divide-amber-800'>
{ops.map((op, idx) => (
<div
key={idx}
className='grid grid-cols-3 items-center gap-0 px-2 py-1.5'
>
<div className='min-w-0'>
<span className='truncate text-amber-800 text-xs dark:text-amber-200'>
{String(op.name || '')}
</span>
</div>
<div>
<span className='rounded border px-1 py-0.5 text-[10px] text-muted-foreground'>
{String(op.type || '')}
</span>
</div>
<div className='min-w-0'>
{op.value !== undefined ? (
<span className='block overflow-x-auto whitespace-nowrap font-mono text-amber-700 text-xs dark:text-amber-300'>
{String(op.value)}
</span>
) : (
<span className='text-muted-foreground text-xs'></span>
)}
</div>
</div>
))}
</div>
)}
</div>
)
})()
: null}
</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
</div>
)
}
export function ToolCallCompletion({ toolCall, isCompact = false }: ToolCallProps) {
const [isExpanded, setIsExpanded] = useState(false)
const isSuccess = toolCall.state === 'completed'
const isError = toolCall.state === 'error'
const isAborted = toolCall.state === 'aborted'
return (
<div
className={cn(
'min-w-0 rounded-lg border',
isSuccess && 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950',
isError && 'border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950',
isAborted && 'border-orange-200 bg-orange-50 dark:border-orange-800 dark:bg-orange-950'
)}
>
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
<CollapsibleTrigger asChild>
<Button
variant='ghost'
className={cn(
'w-full min-w-0 justify-between px-3 py-4',
isSuccess && 'hover:bg-green-100 dark:hover:bg-green-900',
isError && 'hover:bg-red-100 dark:hover:bg-red-900',
isAborted && 'hover:bg-orange-100 dark:hover:bg-orange-900'
)}
>
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
{isSuccess && (
<CheckCircle className='h-4 w-4 shrink-0 text-green-600 dark:text-green-400' />
)}
{isError && <XCircle className='h-4 w-4 shrink-0 text-red-600 dark:text-red-400' />}
{isAborted && (
<XCircle className='h-4 w-4 shrink-0 text-orange-600 dark:text-orange-400' />
)}
<span
className={cn(
'min-w-0 truncate font-mono text-xs',
isSuccess && 'text-green-800 dark:text-green-200',
isError && 'text-red-800 dark:text-red-200',
isAborted && 'text-orange-800 dark:text-orange-200'
)}
>
{toolCall.displayName || toolCall.name}
</span>
{toolCall.duration && (
<Badge
variant='outline'
className={cn(
'shrink-0 text-xs',
isSuccess && 'text-green-700 dark:text-green-300',
isError && 'text-red-700 dark:text-red-300',
isAborted && 'text-orange-700 dark:text-orange-300'
)}
style={{ fontSize: '0.625rem' }}
>
{toolCall.duration ? formatDuration(toolCall.duration, { precision: 1 }) : ''}
</Badge>
)}
</div>
<div className='flex shrink-0 items-center'>
{isExpanded ? (
<ChevronDown
className={cn(
'h-4 w-4',
isSuccess && 'text-green-600 dark:text-green-400',
isError && 'text-red-600 dark:text-red-400'
)}
/>
) : (
<ChevronRight
className={cn(
'h-4 w-4',
isSuccess && 'text-green-600 dark:text-green-400',
isError && 'text-red-600 dark:text-red-400'
)}
/>
)}
</div>
</Button>
</CollapsibleTrigger>
<CollapsibleContent className='min-w-0 max-w-full px-3 pb-3'>
<div className='min-w-0 max-w-full space-y-2'>
{toolCall.parameters &&
Object.keys(toolCall.parameters).length > 0 &&
(toolCall.name === 'make_api_request' ||
toolCall.name === 'set_environment_variables') && (
<div
className={cn(
'min-w-0 max-w-full rounded p-2',
isSuccess && 'bg-green-100 dark:bg-green-900',
isError && 'bg-red-100 dark:bg-red-900'
)}
>
<div
className={cn(
'mb-1 font-medium text-xs',
isSuccess && 'text-green-800 dark:text-green-200',
isError && 'text-red-800 dark:text-red-200'
)}
>
Parameters:
</div>
<div
className={cn(
'min-w-0 max-w-full break-all font-mono text-xs',
isSuccess && 'text-green-700 dark:text-green-300',
isError && 'text-red-700 dark:text-red-300'
)}
>
{JSON.stringify(toolCall.parameters, null, 2)}
</div>
</div>
)}
{toolCall.error && (
<div className='min-w-0 max-w-full rounded bg-red-100 p-2 dark:bg-red-900'>
<div className='mb-1 font-medium text-red-800 text-xs dark:text-red-200'>
Error:
</div>
<div className='min-w-0 max-w-full break-all font-mono text-red-700 text-xs dark:text-red-300'>
{toolCall.error}
</div>
</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
</div>
)
}
// Group Component for Multiple Tool Calls
export function ToolCallGroupComponent({ group, isCompact = false }: ToolCallGroupProps) {
const [isExpanded, setIsExpanded] = useState(true)
const completedCount = group.toolCalls.filter((t) => t.state === 'completed').length
const totalCount = group.toolCalls.length
const isAllCompleted = completedCount === totalCount
const hasErrors = group.toolCalls.some((t) => t.state === 'error')
return (
<div className='min-w-0 space-y-2'>
{group.summary && (
<div className='flex min-w-0 items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-sm dark:border-blue-800 dark:bg-blue-950'>
<Settings className='h-4 w-4 shrink-0 text-blue-600 dark:text-blue-400' />
<span className='min-w-0 truncate text-blue-800 dark:text-blue-200'>{group.summary}</span>
{!isAllCompleted && (
<Badge variant='outline' className='shrink-0 text-blue-700 text-xs dark:text-blue-300'>
{completedCount}/{totalCount}
</Badge>
)}
</div>
)}
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
<CollapsibleTrigger asChild>
<Button
variant='ghost'
className='w-full min-w-0 justify-between px-3 py-3 text-sm hover:bg-muted'
>
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
<span className='min-w-0 truncate text-muted-foreground'>
{isAllCompleted ? 'Completed' : 'In Progress'} ({completedCount}/{totalCount})
</span>
{hasErrors && (
<Badge variant='red' className='shrink-0 text-xs'>
Errors
</Badge>
)}
</div>
{isExpanded ? (
<ChevronDown className='h-4 w-4 shrink-0 text-muted-foreground' />
) : (
<ChevronRight className='h-4 w-4 shrink-0 text-muted-foreground' />
)}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className='min-w-0 max-w-full space-y-2'>
{group.toolCalls.map((toolCall) => (
<div key={toolCall.id} className='min-w-0 max-w-full'>
{toolCall.state === 'executing' && (
<ToolCallExecution toolCall={toolCall} isCompact={isCompact} />
)}
{(toolCall.state === 'completed' || toolCall.state === 'error') && (
<ToolCallCompletion toolCall={toolCall} isCompact={isCompact} />
)}
</div>
))}
</CollapsibleContent>
</Collapsible>
</div>
)
}
// Status Indicator Component
export function ToolCallIndicator({ type, content, toolNames }: ToolCallIndicatorProps) {
if (type === 'status' && toolNames) {
return (
<div className='flex min-w-0 items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-sm dark:border-blue-800 dark:bg-blue-950'>
<Loader2 className='h-4 w-4 shrink-0 animate-spin text-blue-600 dark:text-blue-400' />
<span className='min-w-0 truncate text-blue-800 dark:text-blue-200'>
🔄 {toolNames.join(' • ')}
</span>
</div>
)
}
return (
<div className='flex min-w-0 items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-sm dark:border-blue-800 dark:bg-blue-950'>
<Loader2 className='h-4 w-4 shrink-0 animate-spin text-blue-600 dark:text-blue-400' />
<span className='min-w-0 truncate text-blue-800 dark:text-blue-200'>{content}</span>
</div>
)
}