This commit is contained in:
Siddharth Ganesan
2026-01-09 15:55:16 -08:00
committed by Emir Karabeg
parent 51b2297e35
commit 29eefd8416
28 changed files with 894 additions and 104 deletions

View File

@@ -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>
)

View File

@@ -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

View 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)
}
}
}

View File

@@ -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
}
}

View 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'

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View 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 }
}

View File

@@ -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!
)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!)

View File

@@ -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!
)