mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 23:48:09 -05:00
Plan
This commit is contained in:
committed by
Emir Karabeg
parent
51b2297e35
commit
29eefd8416
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { ChevronUp } from 'lucide-react'
|
||||
import CopilotMarkdownRenderer from './markdown-renderer'
|
||||
|
||||
/**
|
||||
* Max height for thinking content before internal scrolling kicks in
|
||||
@@ -144,7 +145,7 @@ export function ThinkingBlock({
|
||||
return next
|
||||
})
|
||||
}}
|
||||
className='mb-1 inline-flex items-center gap-1 text-left font-[470] font-season text-[var(--text-secondary)] text-sm transition-colors hover:text-[var(--text-primary)]'
|
||||
className='group mb-1 inline-flex items-center gap-1 text-left font-[470] font-season text-[var(--text-secondary)] text-sm transition-colors hover:text-[var(--text-primary)]'
|
||||
type='button'
|
||||
>
|
||||
<span className='relative inline-block'>
|
||||
@@ -173,8 +174,8 @@ export function ThinkingBlock({
|
||||
{hasContent && (
|
||||
<ChevronUp
|
||||
className={clsx(
|
||||
'h-3 w-3 transition-transform',
|
||||
isExpanded ? 'rotate-180' : 'rotate-90'
|
||||
'h-3 w-3 transition-all group-hover:opacity-100',
|
||||
isExpanded ? 'rotate-180 opacity-100' : 'rotate-90 opacity-0'
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
@@ -188,10 +189,10 @@ export function ThinkingBlock({
|
||||
isExpanded ? 'mt-1 max-h-[200px] opacity-100' : 'max-h-0 opacity-0'
|
||||
)}
|
||||
>
|
||||
<pre className='whitespace-pre-wrap font-[470] font-season text-[12px] text-[var(--text-tertiary)] leading-[1.15rem]'>
|
||||
{content}
|
||||
<span className='ml-1 inline-block h-2 w-1 animate-pulse bg-[var(--text-tertiary)]' />
|
||||
</pre>
|
||||
<div className='whitespace-pre-wrap font-[470] font-season text-[12px] text-[var(--text-muted)] leading-none [&_*]:!text-[var(--text-muted)] [&_*]:!text-[12px] [&_*]:!leading-none [&_*]:!m-0 [&_*]:!p-0 [&_*]:!mb-0 [&_*]:!mt-0 [&_p]:!m-0 [&_h1]:!text-[12px] [&_h1]:!font-semibold [&_h2]:!text-[12px] [&_h2]:!font-semibold [&_h3]:!text-[12px] [&_h3]:!font-semibold [&_code]:!text-[11px] [&_ul]:!pl-4 [&_ul]:!my-0 [&_ol]:!pl-4 [&_ol]:!my-0 [&_li]:!my-0 [&_li]:!py-0 [&_br]:!leading-[0.5]'>
|
||||
<CopilotMarkdownRenderer content={content} />
|
||||
<span className='ml-1 inline-block h-2 w-1 animate-pulse bg-[var(--text-muted)]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -204,7 +205,7 @@ export function ThinkingBlock({
|
||||
onClick={() => {
|
||||
setIsExpanded((v) => !v)
|
||||
}}
|
||||
className='mb-1 inline-flex items-center gap-1 text-left font-[470] font-season text-[var(--text-secondary)] text-sm transition-colors hover:text-[var(--text-primary)]'
|
||||
className='group mb-1 inline-flex items-center gap-1 text-left font-[470] font-season text-[var(--text-secondary)] text-sm transition-colors hover:text-[var(--text-primary)]'
|
||||
type='button'
|
||||
disabled={!hasContent}
|
||||
>
|
||||
@@ -212,8 +213,8 @@ export function ThinkingBlock({
|
||||
{hasContent && (
|
||||
<ChevronUp
|
||||
className={clsx(
|
||||
'h-3 w-3 transition-transform',
|
||||
isExpanded ? 'rotate-180' : 'rotate-90'
|
||||
'h-3 w-3 transition-all group-hover:opacity-100',
|
||||
isExpanded ? 'rotate-180 opacity-100' : 'rotate-90 opacity-0'
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
@@ -227,9 +228,9 @@ export function ThinkingBlock({
|
||||
isExpanded ? 'mt-1 max-h-[200px] opacity-100' : 'max-h-0 opacity-0'
|
||||
)}
|
||||
>
|
||||
<pre className='whitespace-pre-wrap font-[470] font-season text-[12px] text-[var(--text-tertiary)] leading-[1.15rem]'>
|
||||
{content}
|
||||
</pre>
|
||||
<div className='whitespace-pre-wrap font-[470] font-season text-[12px] text-[var(--text-muted)] leading-none [&_*]:!text-[var(--text-muted)] [&_*]:!text-[12px] [&_*]:!leading-none [&_*]:!m-0 [&_*]:!p-0 [&_*]:!mb-0 [&_*]:!mt-0 [&_p]:!m-0 [&_h1]:!text-[12px] [&_h1]:!font-semibold [&_h2]:!text-[12px] [&_h2]:!font-semibold [&_h3]:!text-[12px] [&_h3]:!font-semibold [&_code]:!text-[11px] [&_ul]:!pl-4 [&_ul]:!my-0 [&_ol]:!pl-4 [&_ol]:!my-0 [&_li]:!my-0 [&_li]:!py-0 [&_br]:!leading-[0.5]'>
|
||||
<CopilotMarkdownRenderer content={content} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -7,6 +7,14 @@ import { Button, Code } from '@/components/emcn'
|
||||
import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool'
|
||||
import { getClientTool } from '@/lib/copilot/tools/client/manager'
|
||||
import { getRegisteredTools } from '@/lib/copilot/tools/client/registry'
|
||||
// Initialize all tool UI configs
|
||||
import '@/lib/copilot/tools/client/init-tool-configs'
|
||||
import {
|
||||
getToolUIConfig,
|
||||
isSpecialTool as isSpecialToolFromConfig,
|
||||
getSubagentLabels as getSubagentLabelsFromConfig,
|
||||
hasInterrupt as hasInterruptFromConfig,
|
||||
} from '@/lib/copilot/tools/client/ui-config'
|
||||
import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
|
||||
import { SmoothStreamingText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming'
|
||||
import { ThinkingBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block'
|
||||
@@ -164,7 +172,7 @@ export function parseSpecialTags(content: string): ParsedTags {
|
||||
|
||||
/**
|
||||
* PlanSteps component renders the workflow plan steps from the plan subagent
|
||||
* Only renders the title, not the full plan details
|
||||
* Displays as a to-do list with checkmarks and strikethrough text
|
||||
*/
|
||||
function PlanSteps({
|
||||
steps,
|
||||
@@ -193,19 +201,35 @@ function PlanSteps({
|
||||
|
||||
return (
|
||||
<div className='mt-2 overflow-hidden rounded-md border border-[var(--border-1)] bg-[var(--surface-1)]'>
|
||||
<div className='border-[var(--border-1)] border-b bg-[var(--surface-2)] px-2.5 py-2'>
|
||||
<span className='font-medium font-season text-[12px] text-[var(--text-primary)]'>
|
||||
Workflow Plan
|
||||
<div className='flex items-center gap-2 border-[var(--border-1)] border-b bg-[var(--surface-2)] px-2.5 py-2'>
|
||||
<svg
|
||||
className='h-3.5 w-3.5 text-[var(--text-tertiary)]'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
{/* Three horizontal lines with circles at different positions */}
|
||||
<line x1='4' y1='6' x2='20' y2='6' />
|
||||
<circle cx='8' cy='6' r='2' fill='currentColor' />
|
||||
<line x1='4' y1='12' x2='20' y2='12' />
|
||||
<circle cx='16' cy='12' r='2' fill='currentColor' />
|
||||
<line x1='4' y1='18' x2='20' y2='18' />
|
||||
<circle cx='10' cy='18' r='2' fill='currentColor' />
|
||||
</svg>
|
||||
<span className='font-medium text-[12px] text-[var(--text-secondary)]'>To-dos</span>
|
||||
<span className='font-medium text-[12px] text-[var(--text-tertiary)]'>
|
||||
{sortedSteps.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className='divide-y divide-[var(--border-1)]'>
|
||||
<div className='flex flex-col gap-2.5 px-2.5 py-2.5'>
|
||||
{sortedSteps.map(([num, title], index) => {
|
||||
const isLastStep = index === sortedSteps.length - 1
|
||||
return (
|
||||
<div key={num} className='flex items-start gap-2.5 px-2.5 py-2'>
|
||||
<div className='flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-3)] font-medium font-mono text-[11px] text-[var(--text-secondary)]'>
|
||||
{num}
|
||||
</div>
|
||||
<div key={num} className='flex items-start gap-2'>
|
||||
<div className='mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full border border-[var(--border-2)]' />
|
||||
<div className='min-w-0 flex-1 text-[12px] text-[var(--text-secondary)] leading-5 [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-[11px] [&_p]:m-0 [&_p]:leading-5'>
|
||||
{streaming && isLastStep ? (
|
||||
<SmoothStreamingText content={title} isStreaming={true} />
|
||||
@@ -797,51 +821,23 @@ const SUBAGENT_SCROLL_INTERVAL = 100
|
||||
|
||||
/**
|
||||
* Get the outer collapse header label for completed subagent tools.
|
||||
* Returns the label to show when subagent is done (e.g., "Planned", "Thought")
|
||||
* Uses the tool's UI config.
|
||||
*/
|
||||
function getSubagentCompletionLabel(toolName: string): string {
|
||||
switch (toolName) {
|
||||
case 'plan':
|
||||
return 'Planned'
|
||||
default:
|
||||
return 'Thought'
|
||||
}
|
||||
const labels = getSubagentLabelsFromConfig(toolName, false)
|
||||
return labels?.completed ?? 'Thought'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display labels for subagent tools (legacy - used in SubAgentContent)
|
||||
* Get display labels for subagent tools.
|
||||
* Uses the tool's UI config.
|
||||
*/
|
||||
function getSubagentLabels(toolName: string, isStreaming: boolean): string {
|
||||
switch (toolName) {
|
||||
case 'plan':
|
||||
return isStreaming ? 'Planning' : 'Planned'
|
||||
case 'edit':
|
||||
return isStreaming ? 'Editing' : 'Edited'
|
||||
case 'debug':
|
||||
return isStreaming ? 'Debugging' : 'Debugged'
|
||||
case 'test':
|
||||
return isStreaming ? 'Testing' : 'Tested'
|
||||
case 'deploy':
|
||||
return isStreaming ? 'Deploying' : 'Deployed'
|
||||
case 'evaluate':
|
||||
return isStreaming ? 'Evaluating' : 'Evaluated'
|
||||
case 'auth':
|
||||
return isStreaming ? 'Authenticating' : 'Authenticated'
|
||||
case 'research':
|
||||
return isStreaming ? 'Researching' : 'Researched'
|
||||
case 'knowledge':
|
||||
return isStreaming ? 'Managing knowledge' : 'Knowledge managed'
|
||||
case 'custom_tool':
|
||||
return isStreaming ? 'Managing custom tool' : 'Custom tool managed'
|
||||
case 'tour':
|
||||
return isStreaming ? 'Touring' : 'Tour complete'
|
||||
case 'info':
|
||||
return isStreaming ? 'Getting info' : 'Info retrieved'
|
||||
case 'workflow':
|
||||
return isStreaming ? 'Managing workflow' : 'Workflow managed'
|
||||
default:
|
||||
return isStreaming ? 'Processing' : 'Processed'
|
||||
const labels = getSubagentLabelsFromConfig(toolName, isStreaming)
|
||||
if (labels) {
|
||||
return isStreaming ? labels.streaming : labels.completed
|
||||
}
|
||||
return isStreaming ? 'Processing' : 'Processed'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -915,7 +911,7 @@ function SubAgentContent({
|
||||
return next
|
||||
})
|
||||
}}
|
||||
className='mb-1 inline-flex items-center gap-1 text-left font-[470] font-season text-[var(--text-secondary)] text-sm transition-colors hover:text-[var(--text-primary)]'
|
||||
className='group mb-1 inline-flex items-center gap-1 text-left font-[470] font-season text-[var(--text-secondary)] text-sm transition-colors hover:text-[var(--text-primary)]'
|
||||
type='button'
|
||||
disabled={!hasContent}
|
||||
>
|
||||
@@ -947,8 +943,8 @@ function SubAgentContent({
|
||||
{hasContent && (
|
||||
<ChevronUp
|
||||
className={clsx(
|
||||
'h-3 w-3 transition-transform',
|
||||
isExpanded ? 'rotate-180' : 'rotate-90'
|
||||
'h-3 w-3 transition-all group-hover:opacity-100',
|
||||
isExpanded ? 'rotate-180 opacity-100' : 'rotate-90 opacity-0'
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
@@ -1193,14 +1189,14 @@ function SubagentContentRenderer({
|
||||
<div className='w-full'>
|
||||
<button
|
||||
onClick={() => setIsExpanded((v) => !v)}
|
||||
className='mb-1 inline-flex items-center gap-1 text-left font-[470] font-season text-[var(--text-secondary)] text-sm transition-colors hover:text-[var(--text-primary)]'
|
||||
className='group mb-1 inline-flex items-center gap-1 text-left font-[470] font-season text-[var(--text-secondary)] text-sm transition-colors hover:text-[var(--text-primary)]'
|
||||
type='button'
|
||||
>
|
||||
<span className='text-[var(--text-tertiary)]'>{durationText}</span>
|
||||
<ChevronUp
|
||||
className={clsx(
|
||||
'h-3 w-3 transition-transform',
|
||||
isExpanded ? 'rotate-180' : 'rotate-90'
|
||||
'h-3 w-3 transition-all group-hover:opacity-100',
|
||||
isExpanded ? 'rotate-180 opacity-100' : 'rotate-90 opacity-0'
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
@@ -1223,18 +1219,10 @@ function SubagentContentRenderer({
|
||||
|
||||
/**
|
||||
* Determines if a tool call is "special" and should display with gradient styling.
|
||||
* Only workflow operation tools (edit, build, run, deploy) get the purple gradient.
|
||||
* Uses the tool's UI config.
|
||||
*/
|
||||
function isSpecialToolCall(toolCall: CopilotToolCall): boolean {
|
||||
const workflowOperationTools = [
|
||||
'edit_workflow',
|
||||
'build_workflow',
|
||||
'run_workflow',
|
||||
'deploy_api',
|
||||
'deploy_chat',
|
||||
]
|
||||
|
||||
return workflowOperationTools.includes(toolCall.name)
|
||||
return isSpecialToolFromConfig(toolCall.name)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1468,29 +1456,27 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
|
||||
key={`${type}-${change.blockId}`}
|
||||
className='overflow-hidden rounded-md border border-[var(--border-1)] bg-[var(--surface-1)]'
|
||||
>
|
||||
{/* Block header */}
|
||||
<div className='flex items-center justify-between px-2.5 py-2'>
|
||||
<div className='flex items-center gap-2'>
|
||||
{/* Toolbar-style icon: colored square with white icon */}
|
||||
<div
|
||||
className='flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-[3px]'
|
||||
style={{ background: bgColor }}
|
||||
>
|
||||
{Icon && <Icon className='h-[10px] w-[10px] text-white' />}
|
||||
</div>
|
||||
<span
|
||||
className={`font-medium font-season text-[12px] ${type === 'delete' ? 'text-[var(--text-tertiary)] line-through' : 'text-[var(--text-primary)]'}`}
|
||||
>
|
||||
{change.blockName}
|
||||
</span>
|
||||
{/* Block header - gray background like plan/table headers */}
|
||||
<div className='flex items-center bg-[var(--surface-2)] px-2.5 py-2'>
|
||||
{/* Toolbar-style icon: colored square with white icon */}
|
||||
<div
|
||||
className='flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-[3px]'
|
||||
style={{ background: bgColor }}
|
||||
>
|
||||
{Icon && <Icon className='h-[10px] w-[10px] text-white' />}
|
||||
</div>
|
||||
{/* Action icon in top right */}
|
||||
<span className={`font-bold font-mono text-[14px] ${color}`}>{symbol}</span>
|
||||
<span
|
||||
className={`ml-2 font-medium font-season text-[12px] ${type === 'delete' ? 'text-[var(--text-tertiary)] line-through' : 'text-[var(--text-primary)]'}`}
|
||||
>
|
||||
{change.blockName}
|
||||
</span>
|
||||
{/* Action icon next to block name */}
|
||||
<span className={`ml-1.5 font-bold font-mono text-[12px] ${color}`}>{symbol}</span>
|
||||
</div>
|
||||
|
||||
{/* Subblock details - uses same title and value formatting as canvas */}
|
||||
{/* Subblock details - dark background like table/plan body */}
|
||||
{subBlocksToShow && subBlocksToShow.length > 0 && (
|
||||
<div className='border-[var(--border-1)] border-t bg-[var(--surface-2)] px-2.5 py-1.5'>
|
||||
<div className='border-[var(--border-1)] border-t px-2.5 py-1.5'>
|
||||
{subBlocksToShow.map((sb) => {
|
||||
// Mask password fields like the canvas does
|
||||
const displayValue = sb.isPassword ? '•••' : getDisplayValue(sb.value)
|
||||
@@ -1533,6 +1519,12 @@ function isIntegrationTool(toolName: string): boolean {
|
||||
}
|
||||
|
||||
function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean {
|
||||
// First check UI config for interrupt
|
||||
if (hasInterruptFromConfig(toolCall.name) && toolCall.state === 'pending') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Then check instance-level interrupt
|
||||
const instance = getClientTool(toolCall.id)
|
||||
let hasInterrupt = !!instance?.getInterruptDisplays?.()
|
||||
if (!hasInterrupt) {
|
||||
@@ -1895,21 +1887,39 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
if (!isClientTool && !isIntegrationToolInBuildMode) {
|
||||
return null
|
||||
}
|
||||
// Check if tool has params table config (meaning it's expandable)
|
||||
const hasParamsTable = !!getToolUIConfig(toolCall.name)?.paramsTable
|
||||
const isExpandableTool =
|
||||
hasParamsTable ||
|
||||
toolCall.name === 'make_api_request' ||
|
||||
toolCall.name === 'set_global_workflow_variables' ||
|
||||
toolCall.name === 'run_workflow'
|
||||
|
||||
const showButtons = shouldShowRunSkipButtons(toolCall)
|
||||
|
||||
// Check UI config for secondary action
|
||||
const toolUIConfig = getToolUIConfig(toolCall.name)
|
||||
const secondaryAction = toolUIConfig?.secondaryAction
|
||||
const showSecondaryAction =
|
||||
secondaryAction &&
|
||||
secondaryAction.showInStates.includes(toolCall.state as ClientToolCallState)
|
||||
|
||||
// Legacy fallbacks for tools that haven't migrated to UI config
|
||||
const showMoveToBackground =
|
||||
toolCall.name === 'run_workflow' &&
|
||||
(toolCall.state === (ClientToolCallState.executing as any) ||
|
||||
toolCall.state === ('executing' as any))
|
||||
showSecondaryAction && secondaryAction?.text === 'Move to Background'
|
||||
? true
|
||||
: !secondaryAction &&
|
||||
toolCall.name === 'run_workflow' &&
|
||||
(toolCall.state === (ClientToolCallState.executing as any) ||
|
||||
toolCall.state === ('executing' as any))
|
||||
|
||||
const showWake =
|
||||
toolCall.name === 'sleep' &&
|
||||
(toolCall.state === (ClientToolCallState.executing as any) ||
|
||||
toolCall.state === ('executing' as any))
|
||||
showSecondaryAction && secondaryAction?.text === 'Wake'
|
||||
? true
|
||||
: !secondaryAction &&
|
||||
toolCall.name === 'sleep' &&
|
||||
(toolCall.state === (ClientToolCallState.executing as any) ||
|
||||
toolCall.state === ('executing' as any))
|
||||
|
||||
const handleStateChange = (state: any) => {
|
||||
forceUpdate({})
|
||||
@@ -2262,8 +2272,12 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
return null
|
||||
}
|
||||
|
||||
// Special handling for set_environment_variables - always stacked, always expanded
|
||||
if (toolCall.name === 'set_environment_variables' && toolCall.state === 'pending') {
|
||||
// Special handling for tools with alwaysExpanded config (e.g., set_environment_variables)
|
||||
const isAlwaysExpanded = toolUIConfig?.alwaysExpanded
|
||||
if (
|
||||
(isAlwaysExpanded || toolCall.name === 'set_environment_variables') &&
|
||||
toolCall.state === 'pending'
|
||||
) {
|
||||
const isEnvVarsClickable = isAutoAllowed
|
||||
|
||||
const handleEnvVarsClick = () => {
|
||||
@@ -2313,8 +2327,8 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
)
|
||||
}
|
||||
|
||||
// Special rendering for function_execute - show code block
|
||||
if (toolCall.name === 'function_execute') {
|
||||
// Special rendering for tools with 'code' customRenderer (e.g., function_execute)
|
||||
if (toolUIConfig?.customRenderer === 'code' || toolCall.name === 'function_execute') {
|
||||
const code = params.code || ''
|
||||
const isFunctionExecuteClickable = isAutoAllowed
|
||||
|
||||
|
||||
125
apps/sim/lib/copilot/tools/client/base-subagent-tool.ts
Normal file
125
apps/sim/lib/copilot/tools/client/base-subagent-tool.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Base class for subagent tools.
|
||||
*
|
||||
* Subagent tools spawn a server-side subagent that does the actual work.
|
||||
* The tool auto-executes and the subagent's output is streamed back
|
||||
* as nested content under the tool call.
|
||||
*
|
||||
* Examples: edit, plan, debug, evaluate, research, etc.
|
||||
*/
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import {
|
||||
BaseClientTool,
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from './base-tool'
|
||||
import type { SubagentConfig, ToolUIConfig } from './ui-config'
|
||||
import { registerToolUIConfig } from './ui-config'
|
||||
|
||||
/**
|
||||
* Configuration for creating a subagent tool
|
||||
*/
|
||||
export interface SubagentToolConfig {
|
||||
/** Unique tool ID */
|
||||
id: string
|
||||
/** Display names per state */
|
||||
displayNames: {
|
||||
streaming: { text: string; icon: LucideIcon }
|
||||
success: { text: string; icon: LucideIcon }
|
||||
error: { text: string; icon: LucideIcon }
|
||||
}
|
||||
/** Subagent UI configuration */
|
||||
subagent: SubagentConfig
|
||||
/**
|
||||
* Optional: Whether this is a "special" tool (gets gradient styling).
|
||||
* Default: false
|
||||
*/
|
||||
isSpecial?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Create metadata for a subagent tool from config
|
||||
*/
|
||||
function createSubagentMetadata(config: SubagentToolConfig): BaseClientToolMetadata {
|
||||
const { displayNames, subagent, isSpecial } = config
|
||||
const { streaming, success, error } = displayNames
|
||||
|
||||
const uiConfig: ToolUIConfig = {
|
||||
isSpecial: isSpecial ?? false,
|
||||
subagent,
|
||||
}
|
||||
|
||||
return {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: streaming,
|
||||
[ClientToolCallState.pending]: streaming,
|
||||
[ClientToolCallState.executing]: streaming,
|
||||
[ClientToolCallState.success]: success,
|
||||
[ClientToolCallState.error]: error,
|
||||
[ClientToolCallState.rejected]: {
|
||||
text: `${config.id.charAt(0).toUpperCase() + config.id.slice(1)} skipped`,
|
||||
icon: error.icon,
|
||||
},
|
||||
[ClientToolCallState.aborted]: {
|
||||
text: `${config.id.charAt(0).toUpperCase() + config.id.slice(1)} aborted`,
|
||||
icon: error.icon,
|
||||
},
|
||||
},
|
||||
uiConfig,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for subagent tools.
|
||||
* Extends BaseClientTool with subagent-specific behavior.
|
||||
*/
|
||||
export abstract class BaseSubagentTool extends BaseClientTool {
|
||||
/**
|
||||
* Subagent configuration.
|
||||
* Override in subclasses to customize behavior.
|
||||
*/
|
||||
static readonly subagentConfig: SubagentToolConfig
|
||||
|
||||
constructor(toolCallId: string, config: SubagentToolConfig) {
|
||||
super(toolCallId, config.id, createSubagentMetadata(config))
|
||||
// Register UI config for this tool
|
||||
registerToolUIConfig(config.id, this.metadata.uiConfig!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the subagent tool.
|
||||
* Immediately transitions to executing state - the actual work
|
||||
* is done server-side by the subagent.
|
||||
*/
|
||||
async execute(_args?: Record<string, any>): Promise<void> {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
// The tool result will come from the server via tool_result event
|
||||
// when the subagent completes its work
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a subagent tool class.
|
||||
* Use this for simple subagent tools that don't need custom behavior.
|
||||
*/
|
||||
export function createSubagentToolClass(config: SubagentToolConfig) {
|
||||
// Register UI config at class creation time
|
||||
const uiConfig: ToolUIConfig = {
|
||||
isSpecial: config.isSpecial ?? false,
|
||||
subagent: config.subagent,
|
||||
}
|
||||
registerToolUIConfig(config.id, uiConfig)
|
||||
|
||||
return class extends BaseClientTool {
|
||||
static readonly id = config.id
|
||||
|
||||
constructor(toolCallId: string) {
|
||||
super(toolCallId, config.id, createSubagentMetadata(config))
|
||||
}
|
||||
|
||||
async execute(_args?: Record<string, any>): Promise<void> {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Lazy require in setState to avoid circular init issues
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import type { ToolUIConfig } from './ui-config'
|
||||
|
||||
const baseToolLogger = createLogger('BaseClientTool')
|
||||
|
||||
@@ -51,6 +52,11 @@ export interface BaseClientToolMetadata {
|
||||
* If provided, this will override the default text in displayNames
|
||||
*/
|
||||
getDynamicText?: DynamicTextFormatter
|
||||
/**
|
||||
* UI configuration for how this tool renders in the tool-call component.
|
||||
* This replaces hardcoded logic in tool-call.tsx with declarative config.
|
||||
*/
|
||||
uiConfig?: ToolUIConfig
|
||||
}
|
||||
|
||||
export class BaseClientTool {
|
||||
@@ -258,4 +264,12 @@ export class BaseClientTool {
|
||||
hasInterrupt(): boolean {
|
||||
return !!this.metadata.interrupt
|
||||
}
|
||||
|
||||
/**
|
||||
* Get UI configuration for this tool.
|
||||
* Used by tool-call component to determine rendering behavior.
|
||||
*/
|
||||
getUIConfig(): ToolUIConfig | undefined {
|
||||
return this.metadata.uiConfig
|
||||
}
|
||||
}
|
||||
|
||||
49
apps/sim/lib/copilot/tools/client/init-tool-configs.ts
Normal file
49
apps/sim/lib/copilot/tools/client/init-tool-configs.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Initialize all tool UI configurations.
|
||||
*
|
||||
* This module imports all client tools to trigger their UI config registration.
|
||||
* Import this module early in the app to ensure all tool configs are available.
|
||||
*/
|
||||
|
||||
// Other tools (subagents)
|
||||
import './other/auth'
|
||||
import './other/custom-tool'
|
||||
import './other/debug'
|
||||
import './other/deploy'
|
||||
import './other/edit'
|
||||
import './other/evaluate'
|
||||
import './other/info'
|
||||
import './other/knowledge'
|
||||
import './other/make-api-request'
|
||||
import './other/plan'
|
||||
import './other/research'
|
||||
import './other/sleep'
|
||||
import './other/test'
|
||||
import './other/tour'
|
||||
import './other/workflow'
|
||||
|
||||
// Workflow tools
|
||||
import './workflow/deploy-api'
|
||||
import './workflow/deploy-chat'
|
||||
import './workflow/deploy-mcp'
|
||||
import './workflow/edit-workflow'
|
||||
import './workflow/run-workflow'
|
||||
import './workflow/set-global-workflow-variables'
|
||||
|
||||
// User tools
|
||||
import './user/set-environment-variables'
|
||||
|
||||
// Re-export UI config utilities for convenience
|
||||
export {
|
||||
getToolUIConfig,
|
||||
isSubagentTool,
|
||||
isSpecialTool,
|
||||
hasInterrupt,
|
||||
getSubagentLabels,
|
||||
type ToolUIConfig,
|
||||
type SubagentConfig,
|
||||
type InterruptConfig,
|
||||
type SecondaryActionConfig,
|
||||
type ParamsTableConfig,
|
||||
} from './ui-config'
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
interface AuthArgs {
|
||||
instruction: string
|
||||
@@ -31,6 +32,14 @@ export class AuthClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.rejected]: { text: 'Auth skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Auth aborted', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
subagent: {
|
||||
streamingLabel: 'Authenticating',
|
||||
completedLabel: 'Authenticated',
|
||||
shouldCollapse: true,
|
||||
outputArtifacts: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,3 +51,6 @@ export class AuthClientTool extends BaseClientTool {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(AuthClientTool.id, AuthClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
interface CustomToolArgs {
|
||||
instruction: string
|
||||
@@ -31,6 +32,14 @@ export class CustomToolClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.rejected]: { text: 'Custom tool skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Custom tool aborted', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
subagent: {
|
||||
streamingLabel: 'Managing custom tool',
|
||||
completedLabel: 'Custom tool managed',
|
||||
shouldCollapse: true,
|
||||
outputArtifacts: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,3 +51,6 @@ export class CustomToolClientTool extends BaseClientTool {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(CustomToolClientTool.id, CustomToolClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
interface DebugArgs {
|
||||
error_description: string
|
||||
@@ -32,6 +33,14 @@ export class DebugClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.rejected]: { text: 'Debug skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Debug aborted', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
subagent: {
|
||||
streamingLabel: 'Debugging',
|
||||
completedLabel: 'Debugged',
|
||||
shouldCollapse: true,
|
||||
outputArtifacts: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,3 +55,6 @@ export class DebugClientTool extends BaseClientTool {
|
||||
// when the debug subagent completes its work
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(DebugClientTool.id, DebugClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
interface DeployArgs {
|
||||
instruction: string
|
||||
@@ -31,6 +32,14 @@ export class DeployClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.rejected]: { text: 'Deploy skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Deploy aborted', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
subagent: {
|
||||
streamingLabel: 'Deploying',
|
||||
completedLabel: 'Deployed',
|
||||
shouldCollapse: true,
|
||||
outputArtifacts: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,3 +51,6 @@ export class DeployClientTool extends BaseClientTool {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(DeployClientTool.id, DeployClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
interface EditArgs {
|
||||
instruction: string
|
||||
@@ -31,6 +32,16 @@ export class EditClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.rejected]: { text: 'Edit skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Edit aborted', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
isSpecial: true,
|
||||
subagent: {
|
||||
streamingLabel: 'Editing',
|
||||
completedLabel: 'Edited',
|
||||
shouldCollapse: false, // Edit subagent stays expanded
|
||||
outputArtifacts: ['edit_summary'],
|
||||
hideThinkingText: true, // We show WorkflowEditSummary instead
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,3 +56,6 @@ export class EditClientTool extends BaseClientTool {
|
||||
// when the edit subagent completes its work
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(EditClientTool.id, EditClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
interface EvaluateArgs {
|
||||
instruction: string
|
||||
@@ -31,6 +32,14 @@ export class EvaluateClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.rejected]: { text: 'Evaluation skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Evaluation aborted', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
subagent: {
|
||||
streamingLabel: 'Evaluating',
|
||||
completedLabel: 'Evaluated',
|
||||
shouldCollapse: true,
|
||||
outputArtifacts: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,3 +52,5 @@ export class EvaluateClientTool extends BaseClientTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(EvaluateClientTool.id, EvaluateClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
interface InfoArgs {
|
||||
instruction: string
|
||||
@@ -31,6 +32,14 @@ export class InfoClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.rejected]: { text: 'Info skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Info aborted', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
subagent: {
|
||||
streamingLabel: 'Getting info',
|
||||
completedLabel: 'Info retrieved',
|
||||
shouldCollapse: true,
|
||||
outputArtifacts: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,3 +51,6 @@ export class InfoClientTool extends BaseClientTool {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(InfoClientTool.id, InfoClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
interface KnowledgeArgs {
|
||||
instruction: string
|
||||
@@ -31,6 +32,14 @@ export class KnowledgeClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.rejected]: { text: 'Knowledge skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Knowledge aborted', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
subagent: {
|
||||
streamingLabel: 'Managing knowledge',
|
||||
completedLabel: 'Knowledge managed',
|
||||
shouldCollapse: true,
|
||||
outputArtifacts: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,3 +51,6 @@ export class KnowledgeClientTool extends BaseClientTool {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(KnowledgeClientTool.id, KnowledgeClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
import { ExecuteResponseSuccessSchema } from '@/lib/copilot/tools/shared/schemas'
|
||||
|
||||
interface MakeApiRequestArgs {
|
||||
@@ -36,6 +37,23 @@ export class MakeApiRequestClientTool extends BaseClientTool {
|
||||
accept: { text: 'Execute', icon: Globe2 },
|
||||
reject: { text: 'Skip', icon: MinusCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
interrupt: {
|
||||
accept: { text: 'Execute', icon: Globe2 },
|
||||
reject: { text: 'Skip', icon: MinusCircle },
|
||||
showAllowOnce: true,
|
||||
showAllowAlways: true,
|
||||
},
|
||||
paramsTable: {
|
||||
columns: [
|
||||
{ key: 'method', label: 'Method', width: '26%', editable: true, mono: true },
|
||||
{ key: 'url', label: 'Endpoint', width: '74%', editable: true, mono: true },
|
||||
],
|
||||
extractRows: (params) => {
|
||||
return [['request', (params.method || 'GET').toUpperCase(), params.url || '']]
|
||||
},
|
||||
},
|
||||
},
|
||||
getDynamicText: (params, state) => {
|
||||
if (params?.url && typeof params.url === 'string') {
|
||||
const method = params.method || 'GET'
|
||||
@@ -110,3 +128,6 @@ export class MakeApiRequestClientTool extends BaseClientTool {
|
||||
await this.handleAccept(args)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(MakeApiRequestClientTool.id, MakeApiRequestClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
interface PlanArgs {
|
||||
request: string
|
||||
@@ -31,6 +32,14 @@ export class PlanClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.rejected]: { text: 'Plan skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Plan aborted', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
subagent: {
|
||||
streamingLabel: 'Planning',
|
||||
completedLabel: 'Planned',
|
||||
shouldCollapse: true,
|
||||
outputArtifacts: ['plan'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,3 +54,6 @@ export class PlanClientTool extends BaseClientTool {
|
||||
// when the plan subagent completes its work
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(PlanClientTool.id, PlanClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
interface ResearchArgs {
|
||||
instruction: string
|
||||
@@ -31,6 +32,14 @@ export class ResearchClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.rejected]: { text: 'Research skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Research aborted', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
subagent: {
|
||||
streamingLabel: 'Researching',
|
||||
completedLabel: 'Researched',
|
||||
shouldCollapse: true,
|
||||
outputArtifacts: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,3 +51,6 @@ export class ResearchClientTool extends BaseClientTool {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(ResearchClientTool.id, ResearchClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
/** Maximum sleep duration in seconds (3 minutes) */
|
||||
const MAX_SLEEP_SECONDS = 180
|
||||
@@ -44,6 +45,15 @@ export class SleepClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.aborted]: { text: 'Sleep aborted', icon: MinusCircle },
|
||||
[ClientToolCallState.background]: { text: 'Resumed', icon: Moon },
|
||||
},
|
||||
uiConfig: {
|
||||
secondaryAction: {
|
||||
text: 'Wake',
|
||||
title: 'Wake',
|
||||
variant: 'tertiary',
|
||||
showInStates: [ClientToolCallState.executing],
|
||||
targetState: ClientToolCallState.background,
|
||||
},
|
||||
},
|
||||
// No interrupt - auto-execute immediately
|
||||
getDynamicText: (params, state) => {
|
||||
const seconds = params?.seconds
|
||||
@@ -142,3 +152,6 @@ export class SleepClientTool extends BaseClientTool {
|
||||
await this.handleAccept(args)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(SleepClientTool.id, SleepClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
interface TestArgs {
|
||||
instruction: string
|
||||
@@ -31,6 +32,14 @@ export class TestClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.rejected]: { text: 'Test skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Test aborted', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
subagent: {
|
||||
streamingLabel: 'Testing',
|
||||
completedLabel: 'Tested',
|
||||
shouldCollapse: true,
|
||||
outputArtifacts: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,3 +51,6 @@ export class TestClientTool extends BaseClientTool {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(TestClientTool.id, TestClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
interface TourArgs {
|
||||
instruction: string
|
||||
@@ -31,6 +32,14 @@ export class TourClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.rejected]: { text: 'Tour skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Tour aborted', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
subagent: {
|
||||
streamingLabel: 'Touring',
|
||||
completedLabel: 'Tour complete',
|
||||
shouldCollapse: true,
|
||||
outputArtifacts: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,3 +51,6 @@ export class TourClientTool extends BaseClientTool {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(TourClientTool.id, TourClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
|
||||
interface WorkflowArgs {
|
||||
instruction: string
|
||||
@@ -31,6 +32,14 @@ export class WorkflowClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.rejected]: { text: 'Workflow skipped', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Workflow aborted', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
subagent: {
|
||||
streamingLabel: 'Managing workflow',
|
||||
completedLabel: 'Workflow managed',
|
||||
shouldCollapse: true,
|
||||
outputArtifacts: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,3 +51,6 @@ export class WorkflowClientTool extends BaseClientTool {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(WorkflowClientTool.id, WorkflowClientTool.metadata.uiConfig!)
|
||||
|
||||
239
apps/sim/lib/copilot/tools/client/ui-config.ts
Normal file
239
apps/sim/lib/copilot/tools/client/ui-config.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* UI Configuration Types for Copilot Tools
|
||||
*
|
||||
* This module defines the configuration interfaces that control how tools
|
||||
* are rendered in the tool-call component. All UI behavior should be defined
|
||||
* here rather than hardcoded in the rendering component.
|
||||
*/
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import type { ClientToolCallState } from './base-tool'
|
||||
|
||||
/**
|
||||
* Configuration for a params table column
|
||||
*/
|
||||
export interface ParamsTableColumn {
|
||||
/** Key to extract from params */
|
||||
key: string
|
||||
/** Display label for the column header */
|
||||
label: string
|
||||
/** Width as percentage or CSS value */
|
||||
width?: string
|
||||
/** Whether values in this column are editable */
|
||||
editable?: boolean
|
||||
/** Whether to use monospace font */
|
||||
mono?: boolean
|
||||
/** Whether to mask the value (for passwords) */
|
||||
masked?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for params table rendering
|
||||
*/
|
||||
export interface ParamsTableConfig {
|
||||
/** Column definitions */
|
||||
columns: ParamsTableColumn[]
|
||||
/**
|
||||
* Extract rows from tool params.
|
||||
* Returns array of [key, ...cellValues] for each row.
|
||||
*/
|
||||
extractRows: (params: Record<string, any>) => Array<[string, ...any[]]>
|
||||
/**
|
||||
* Optional: Update params when a cell is edited.
|
||||
* Returns the updated params object.
|
||||
*/
|
||||
updateCell?: (
|
||||
params: Record<string, any>,
|
||||
rowKey: string,
|
||||
columnKey: string,
|
||||
newValue: any
|
||||
) => Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for secondary action button (like "Move to Background")
|
||||
*/
|
||||
export interface SecondaryActionConfig {
|
||||
/** Button text */
|
||||
text: string
|
||||
/** Button title/tooltip */
|
||||
title?: string
|
||||
/** Button variant */
|
||||
variant?: 'tertiary' | 'default' | 'outline'
|
||||
/** States in which to show this button */
|
||||
showInStates: ClientToolCallState[]
|
||||
/**
|
||||
* Message to send when the action is triggered.
|
||||
* Used by markToolComplete.
|
||||
*/
|
||||
completionMessage?: string
|
||||
/**
|
||||
* Target state after action.
|
||||
* If not provided, defaults to 'background'.
|
||||
*/
|
||||
targetState?: ClientToolCallState
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for subagent tools (tools that spawn subagents)
|
||||
*/
|
||||
export interface SubagentConfig {
|
||||
/** Label shown while streaming (e.g., "Planning", "Editing") */
|
||||
streamingLabel: string
|
||||
/** Label shown when complete (e.g., "Planned", "Edited") */
|
||||
completedLabel: string
|
||||
/**
|
||||
* Whether the content should collapse when streaming ends.
|
||||
* Default: true
|
||||
*/
|
||||
shouldCollapse?: boolean
|
||||
/**
|
||||
* Output artifacts that should NOT be collapsed.
|
||||
* These are rendered outside the collapsible content.
|
||||
* Examples: 'plan' for PlanSteps, 'options' for OptionsSelector
|
||||
*/
|
||||
outputArtifacts?: Array<'plan' | 'options' | 'edit_summary'>
|
||||
/**
|
||||
* Whether this subagent renders its own specialized content
|
||||
* and the thinking text should be minimal or hidden.
|
||||
* Used for tools like 'edit' where we show WorkflowEditSummary instead.
|
||||
*/
|
||||
hideThinkingText?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Interrupt button configuration
|
||||
*/
|
||||
export interface InterruptButtonConfig {
|
||||
text: string
|
||||
icon: LucideIcon
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for interrupt behavior (Run/Skip buttons)
|
||||
*/
|
||||
export interface InterruptConfig {
|
||||
/** Accept button config */
|
||||
accept: InterruptButtonConfig
|
||||
/** Reject button config */
|
||||
reject: InterruptButtonConfig
|
||||
/**
|
||||
* Whether to show "Allow Once" button (default accept behavior).
|
||||
* Default: true
|
||||
*/
|
||||
showAllowOnce?: boolean
|
||||
/**
|
||||
* Whether to show "Allow Always" button (auto-approve this tool in future).
|
||||
* Default: true for most tools
|
||||
*/
|
||||
showAllowAlways?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete UI configuration for a tool
|
||||
*/
|
||||
export interface ToolUIConfig {
|
||||
/**
|
||||
* Whether this is a "special" tool that gets gradient styling.
|
||||
* Used for workflow operation tools like edit_workflow, build_workflow, etc.
|
||||
*/
|
||||
isSpecial?: boolean
|
||||
|
||||
/**
|
||||
* Interrupt configuration for tools that require user confirmation.
|
||||
* If not provided, tool auto-executes.
|
||||
*/
|
||||
interrupt?: InterruptConfig
|
||||
|
||||
/**
|
||||
* Secondary action button (like "Move to Background" for run_workflow)
|
||||
*/
|
||||
secondaryAction?: SecondaryActionConfig
|
||||
|
||||
/**
|
||||
* Configuration for rendering params as a table.
|
||||
* If provided, tool will show an expandable/inline table.
|
||||
*/
|
||||
paramsTable?: ParamsTableConfig
|
||||
|
||||
/**
|
||||
* Subagent configuration for tools that spawn subagents.
|
||||
* If provided, tool is treated as a subagent tool.
|
||||
*/
|
||||
subagent?: SubagentConfig
|
||||
|
||||
/**
|
||||
* Whether this tool should always show params expanded (not collapsible).
|
||||
* Used for tools like set_environment_variables that always show their table.
|
||||
*/
|
||||
alwaysExpanded?: boolean
|
||||
|
||||
/**
|
||||
* Custom component type for special rendering.
|
||||
* The tool-call component will use this to render specialized content.
|
||||
*/
|
||||
customRenderer?: 'code' | 'edit_summary' | 'none'
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of tool UI configurations.
|
||||
* Tools can register their UI config here for the tool-call component to use.
|
||||
*/
|
||||
const toolUIConfigs: Record<string, ToolUIConfig> = {}
|
||||
|
||||
/**
|
||||
* Register a tool's UI configuration
|
||||
*/
|
||||
export function registerToolUIConfig(toolName: string, config: ToolUIConfig): void {
|
||||
toolUIConfigs[toolName] = config
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a tool's UI configuration
|
||||
*/
|
||||
export function getToolUIConfig(toolName: string): ToolUIConfig | undefined {
|
||||
return toolUIConfigs[toolName]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool is a subagent tool
|
||||
*/
|
||||
export function isSubagentTool(toolName: string): boolean {
|
||||
return !!toolUIConfigs[toolName]?.subagent
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool is a "special" tool (gets gradient styling)
|
||||
*/
|
||||
export function isSpecialTool(toolName: string): boolean {
|
||||
return !!toolUIConfigs[toolName]?.isSpecial
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool has interrupt (requires user confirmation)
|
||||
*/
|
||||
export function hasInterrupt(toolName: string): boolean {
|
||||
return !!toolUIConfigs[toolName]?.interrupt
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subagent labels for a tool
|
||||
*/
|
||||
export function getSubagentLabels(
|
||||
toolName: string,
|
||||
isStreaming: boolean
|
||||
): { streaming: string; completed: string } | undefined {
|
||||
const config = toolUIConfigs[toolName]?.subagent
|
||||
if (!config) return undefined
|
||||
return {
|
||||
streaming: config.streamingLabel,
|
||||
completed: config.completedLabel,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered tool UI configs (for debugging)
|
||||
*/
|
||||
export function getAllToolUIConfigs(): Record<string, ToolUIConfig> {
|
||||
return { ...toolUIConfigs }
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
import { ExecuteResponseSuccessSchema } from '@/lib/copilot/tools/shared/schemas'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
@@ -48,6 +49,33 @@ export class SetEnvironmentVariablesClientTool extends BaseClientTool {
|
||||
accept: { text: 'Apply', icon: Settings2 },
|
||||
reject: { text: 'Skip', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
alwaysExpanded: true,
|
||||
interrupt: {
|
||||
accept: { text: 'Apply', icon: Settings2 },
|
||||
reject: { text: 'Skip', icon: XCircle },
|
||||
showAllowOnce: true,
|
||||
showAllowAlways: true,
|
||||
},
|
||||
paramsTable: {
|
||||
columns: [
|
||||
{ key: 'name', label: 'Variable', width: '36%', editable: true },
|
||||
{ key: 'value', label: 'Value', width: '64%', editable: true, mono: true },
|
||||
],
|
||||
extractRows: (params) => {
|
||||
const variables = params.variables || {}
|
||||
const entries = Array.isArray(variables)
|
||||
? variables.map((v: any, i: number) => [String(i), v.name || `var_${i}`, v.value || ''])
|
||||
: Object.entries(variables).map(([key, val]) => {
|
||||
if (typeof val === 'object' && val !== null && 'value' in (val as any)) {
|
||||
return [key, key, (val as any).value]
|
||||
}
|
||||
return [key, key, val]
|
||||
})
|
||||
return entries as Array<[string, ...any[]]>
|
||||
},
|
||||
},
|
||||
},
|
||||
getDynamicText: (params, state) => {
|
||||
if (params?.variables && typeof params.variables === 'object') {
|
||||
const count = Object.keys(params.variables).length
|
||||
@@ -121,3 +149,9 @@ export class SetEnvironmentVariablesClientTool extends BaseClientTool {
|
||||
await this.handleAccept(args)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(
|
||||
SetEnvironmentVariablesClientTool.id,
|
||||
SetEnvironmentVariablesClientTool.metadata.uiConfig!
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
import { getInputFormatExample } from '@/lib/workflows/operations/deployment-utils'
|
||||
import { useCopilotStore } from '@/stores/panel/copilot/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
@@ -76,6 +77,15 @@ export class DeployApiClientTool extends BaseClientTool {
|
||||
accept: { text: 'Deploy', icon: Rocket },
|
||||
reject: { text: 'Skip', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
isSpecial: true,
|
||||
interrupt: {
|
||||
accept: { text: 'Deploy', icon: Rocket },
|
||||
reject: { text: 'Skip', icon: XCircle },
|
||||
showAllowOnce: true,
|
||||
showAllowAlways: true,
|
||||
},
|
||||
},
|
||||
getDynamicText: (params, state) => {
|
||||
const action = params?.action === 'undeploy' ? 'undeploy' : 'deploy'
|
||||
|
||||
@@ -276,3 +286,6 @@ export class DeployApiClientTool extends BaseClientTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(DeployApiClientTool.id, DeployApiClientTool.metadata.uiConfig!)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
import { useCopilotStore } from '@/stores/panel/copilot/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
@@ -85,6 +86,15 @@ export class DeployChatClientTool extends BaseClientTool {
|
||||
accept: { text: 'Deploy Chat', icon: MessageSquare },
|
||||
reject: { text: 'Skip', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
isSpecial: true,
|
||||
interrupt: {
|
||||
accept: { text: 'Deploy Chat', icon: MessageSquare },
|
||||
reject: { text: 'Skip', icon: XCircle },
|
||||
showAllowOnce: true,
|
||||
showAllowAlways: true,
|
||||
},
|
||||
},
|
||||
getDynamicText: (params, state) => {
|
||||
const action = params?.action === 'undeploy' ? 'undeploy' : 'deploy'
|
||||
|
||||
@@ -351,3 +361,6 @@ export class DeployChatClientTool extends BaseClientTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(DeployChatClientTool.id, DeployChatClientTool.metadata.uiConfig!)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
import { useCopilotStore } from '@/stores/panel/copilot/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
@@ -61,6 +62,15 @@ export class DeployMcpClientTool extends BaseClientTool {
|
||||
accept: { text: 'Deploy', icon: Server },
|
||||
reject: { text: 'Skip', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
isSpecial: true,
|
||||
interrupt: {
|
||||
accept: { text: 'Deploy', icon: Server },
|
||||
reject: { text: 'Skip', icon: XCircle },
|
||||
showAllowOnce: true,
|
||||
showAllowAlways: true,
|
||||
},
|
||||
},
|
||||
getDynamicText: (params, state) => {
|
||||
const toolName = params?.toolName || 'workflow'
|
||||
switch (state) {
|
||||
@@ -196,3 +206,6 @@ export class DeployMcpClientTool extends BaseClientTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(DeployMcpClientTool.id, DeployMcpClientTool.metadata.uiConfig!)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
import { ExecuteResponseSuccessSchema } from '@/lib/copilot/tools/shared/schemas'
|
||||
import { stripWorkflowDiffMarkers } from '@/lib/workflows/diff'
|
||||
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
||||
@@ -124,6 +125,10 @@ export class EditWorkflowClientTool extends BaseClientTool {
|
||||
[ClientToolCallState.aborted]: { text: 'Aborted editing your workflow', icon: MinusCircle },
|
||||
[ClientToolCallState.pending]: { text: 'Editing your workflow', icon: Loader2 },
|
||||
},
|
||||
uiConfig: {
|
||||
isSpecial: true,
|
||||
customRenderer: 'edit_summary',
|
||||
},
|
||||
getDynamicText: (params, state) => {
|
||||
const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (workflowId) {
|
||||
@@ -412,3 +417,6 @@ export class EditWorkflowClientTool extends BaseClientTool {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(EditWorkflowClientTool.id, EditWorkflowClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ClientToolCallState,
|
||||
WORKFLOW_EXECUTION_TIMEOUT_MS,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
import { executeWorkflowWithFullLogging } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
|
||||
import { useExecutionStore } from '@/stores/execution/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
@@ -39,6 +40,49 @@ export class RunWorkflowClientTool extends BaseClientTool {
|
||||
accept: { text: 'Run', icon: Play },
|
||||
reject: { text: 'Skip', icon: MinusCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
isSpecial: true,
|
||||
interrupt: {
|
||||
accept: { text: 'Run', icon: Play },
|
||||
reject: { text: 'Skip', icon: MinusCircle },
|
||||
showAllowOnce: true,
|
||||
showAllowAlways: true,
|
||||
},
|
||||
secondaryAction: {
|
||||
text: 'Move to Background',
|
||||
title: 'Move to Background',
|
||||
variant: 'tertiary',
|
||||
showInStates: [ClientToolCallState.executing],
|
||||
completionMessage:
|
||||
'The user has chosen to move the workflow execution to the background. Check back with them later to know when the workflow execution is complete',
|
||||
targetState: ClientToolCallState.background,
|
||||
},
|
||||
paramsTable: {
|
||||
columns: [
|
||||
{ key: 'input', label: 'Input', width: '36%' },
|
||||
{ key: 'value', label: 'Value', width: '64%', editable: true, mono: true },
|
||||
],
|
||||
extractRows: (params) => {
|
||||
let inputs = params.input || params.inputs || params.workflow_input
|
||||
if (typeof inputs === 'string') {
|
||||
try {
|
||||
inputs = JSON.parse(inputs)
|
||||
} catch {
|
||||
inputs = {}
|
||||
}
|
||||
}
|
||||
if (params.workflow_input && typeof params.workflow_input === 'object') {
|
||||
inputs = params.workflow_input
|
||||
}
|
||||
if (!inputs || typeof inputs !== 'object') {
|
||||
const { workflowId, workflow_input, ...rest } = params
|
||||
inputs = rest
|
||||
}
|
||||
const safeInputs = inputs && typeof inputs === 'object' ? inputs : {}
|
||||
return Object.entries(safeInputs).map(([key, value]) => [key, key, String(value)])
|
||||
},
|
||||
},
|
||||
},
|
||||
getDynamicText: (params, state) => {
|
||||
const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (workflowId) {
|
||||
@@ -182,3 +226,6 @@ export class RunWorkflowClientTool extends BaseClientTool {
|
||||
await this.handleAccept(args)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(RunWorkflowClientTool.id, RunWorkflowClientTool.metadata.uiConfig!)
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config'
|
||||
import { useVariablesStore } from '@/stores/panel/variables/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
@@ -48,6 +49,28 @@ export class SetGlobalWorkflowVariablesClientTool extends BaseClientTool {
|
||||
accept: { text: 'Apply', icon: Settings2 },
|
||||
reject: { text: 'Skip', icon: XCircle },
|
||||
},
|
||||
uiConfig: {
|
||||
interrupt: {
|
||||
accept: { text: 'Apply', icon: Settings2 },
|
||||
reject: { text: 'Skip', icon: XCircle },
|
||||
showAllowOnce: true,
|
||||
showAllowAlways: true,
|
||||
},
|
||||
paramsTable: {
|
||||
columns: [
|
||||
{ key: 'name', label: 'Name', width: '40%', editable: true, mono: true },
|
||||
{ key: 'value', label: 'Value', width: '60%', editable: true, mono: true },
|
||||
],
|
||||
extractRows: (params) => {
|
||||
const operations = params.operations || []
|
||||
return operations.map((op: any, idx: number) => [
|
||||
String(idx),
|
||||
op.name || '',
|
||||
String(op.value ?? ''),
|
||||
])
|
||||
},
|
||||
},
|
||||
},
|
||||
getDynamicText: (params, state) => {
|
||||
if (params?.operations && Array.isArray(params.operations)) {
|
||||
const varNames = params.operations
|
||||
@@ -243,3 +266,9 @@ export class SetGlobalWorkflowVariablesClientTool extends BaseClientTool {
|
||||
await this.handleAccept(args)
|
||||
}
|
||||
}
|
||||
|
||||
// Register UI config at module load
|
||||
registerToolUIConfig(
|
||||
SetGlobalWorkflowVariablesClientTool.id,
|
||||
SetGlobalWorkflowVariablesClientTool.metadata.uiConfig!
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user