@@ -7,6 +7,14 @@ import { Button, Code } from '@/components/emcn'
77import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool'
88import { getClientTool } from '@/lib/copilot/tools/client/manager'
99import { getRegisteredTools } from '@/lib/copilot/tools/client/registry'
10+ // Initialize all tool UI configs
11+ import '@/lib/copilot/tools/client/init-tool-configs'
12+ import {
13+ getToolUIConfig ,
14+ isSpecialTool as isSpecialToolFromConfig ,
15+ getSubagentLabels as getSubagentLabelsFromConfig ,
16+ hasInterrupt as hasInterruptFromConfig ,
17+ } from '@/lib/copilot/tools/client/ui-config'
1018import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
1119import { SmoothStreamingText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming'
1220import { 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 {
164172
165173/**
166174 * PlanSteps component renders the workflow plan steps from the plan subagent
167- * Only renders the title, not the full plan details
175+ * Displays as a to-do list with checkmarks and strikethrough text
168176 */
169177function PlanSteps ( {
170178 steps,
@@ -193,19 +201,35 @@ function PlanSteps({
193201
194202 return (
195203 < div className = 'mt-2 overflow-hidden rounded-md border border-[var(--border-1)] bg-[var(--surface-1)]' >
196- < div className = 'border-[var(--border-1)] border-b bg-[var(--surface-2)] px-2.5 py-2' >
197- < span className = 'font-medium font-season text-[12px] text-[var(--text-primary)]' >
198- Workflow Plan
204+ < div className = 'flex items-center gap-2 border-[var(--border-1)] border-b bg-[var(--surface-2)] px-2.5 py-2' >
205+ < svg
206+ className = 'h-3.5 w-3.5 text-[var(--text-tertiary)]'
207+ viewBox = '0 0 24 24'
208+ fill = 'none'
209+ stroke = 'currentColor'
210+ strokeWidth = '2'
211+ strokeLinecap = 'round'
212+ strokeLinejoin = 'round'
213+ >
214+ { /* Three horizontal lines with circles at different positions */ }
215+ < line x1 = '4' y1 = '6' x2 = '20' y2 = '6' />
216+ < circle cx = '8' cy = '6' r = '2' fill = 'currentColor' />
217+ < line x1 = '4' y1 = '12' x2 = '20' y2 = '12' />
218+ < circle cx = '16' cy = '12' r = '2' fill = 'currentColor' />
219+ < line x1 = '4' y1 = '18' x2 = '20' y2 = '18' />
220+ < circle cx = '10' cy = '18' r = '2' fill = 'currentColor' />
221+ </ svg >
222+ < span className = 'font-medium text-[12px] text-[var(--text-secondary)]' > To-dos</ span >
223+ < span className = 'font-medium text-[12px] text-[var(--text-tertiary)]' >
224+ { sortedSteps . length }
199225 </ span >
200226 </ div >
201- < div className = 'divide-y divide-[var(--border-1)] ' >
227+ < div className = 'flex flex-col gap-2.5 px-2.5 py-2.5 ' >
202228 { sortedSteps . map ( ( [ num , title ] , index ) => {
203229 const isLastStep = index === sortedSteps . length - 1
204230 return (
205- < div key = { num } className = 'flex items-start gap-2.5 px-2.5 py-2' >
206- < 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)]' >
207- { num }
208- </ div >
231+ < div key = { num } className = 'flex items-start gap-2' >
232+ < div className = 'mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full border border-[var(--border-2)]' />
209233 < 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' >
210234 { streaming && isLastStep ? (
211235 < SmoothStreamingText content = { title } isStreaming = { true } />
@@ -797,51 +821,23 @@ const SUBAGENT_SCROLL_INTERVAL = 100
797821
798822/**
799823 * Get the outer collapse header label for completed subagent tools.
800- * Returns the label to show when subagent is done (e.g., "Planned", "Thought")
824+ * Uses the tool's UI config.
801825 */
802826function getSubagentCompletionLabel ( toolName : string ) : string {
803- switch ( toolName ) {
804- case 'plan' :
805- return 'Planned'
806- default :
807- return 'Thought'
808- }
827+ const labels = getSubagentLabelsFromConfig ( toolName , false )
828+ return labels ?. completed ?? 'Thought'
809829}
810830
811831/**
812- * Get display labels for subagent tools (legacy - used in SubAgentContent)
832+ * Get display labels for subagent tools.
833+ * Uses the tool's UI config.
813834 */
814835function getSubagentLabels ( toolName : string , isStreaming : boolean ) : string {
815- switch ( toolName ) {
816- case 'plan' :
817- return isStreaming ? 'Planning' : 'Planned'
818- case 'edit' :
819- return isStreaming ? 'Editing' : 'Edited'
820- case 'debug' :
821- return isStreaming ? 'Debugging' : 'Debugged'
822- case 'test' :
823- return isStreaming ? 'Testing' : 'Tested'
824- case 'deploy' :
825- return isStreaming ? 'Deploying' : 'Deployed'
826- case 'evaluate' :
827- return isStreaming ? 'Evaluating' : 'Evaluated'
828- case 'auth' :
829- return isStreaming ? 'Authenticating' : 'Authenticated'
830- case 'research' :
831- return isStreaming ? 'Researching' : 'Researched'
832- case 'knowledge' :
833- return isStreaming ? 'Managing knowledge' : 'Knowledge managed'
834- case 'custom_tool' :
835- return isStreaming ? 'Managing custom tool' : 'Custom tool managed'
836- case 'tour' :
837- return isStreaming ? 'Touring' : 'Tour complete'
838- case 'info' :
839- return isStreaming ? 'Getting info' : 'Info retrieved'
840- case 'workflow' :
841- return isStreaming ? 'Managing workflow' : 'Workflow managed'
842- default :
843- return isStreaming ? 'Processing' : 'Processed'
836+ const labels = getSubagentLabelsFromConfig ( toolName , isStreaming )
837+ if ( labels ) {
838+ return isStreaming ? labels . streaming : labels . completed
844839 }
840+ return isStreaming ? 'Processing' : 'Processed'
845841}
846842
847843/**
@@ -915,7 +911,7 @@ function SubAgentContent({
915911 return next
916912 } )
917913 } }
918- 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)]'
914+ 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)]'
919915 type = 'button'
920916 disabled = { ! hasContent }
921917 >
@@ -947,8 +943,8 @@ function SubAgentContent({
947943 { hasContent && (
948944 < ChevronUp
949945 className = { clsx (
950- 'h-3 w-3 transition-transform ' ,
951- isExpanded ? 'rotate-180' : 'rotate-90'
946+ 'h-3 w-3 transition-all group-hover:opacity-100 ' ,
947+ isExpanded ? 'rotate-180 opacity-100 ' : 'rotate-90 opacity-0 '
952948 ) }
953949 aria-hidden = 'true'
954950 />
@@ -1193,14 +1189,14 @@ function SubagentContentRenderer({
11931189 < div className = 'w-full' >
11941190 < button
11951191 onClick = { ( ) => setIsExpanded ( ( v ) => ! v ) }
1196- 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)]'
1192+ 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)]'
11971193 type = 'button'
11981194 >
11991195 < span className = 'text-[var(--text-tertiary)]' > { durationText } </ span >
12001196 < ChevronUp
12011197 className = { clsx (
1202- 'h-3 w-3 transition-transform ' ,
1203- isExpanded ? 'rotate-180' : 'rotate-90'
1198+ 'h-3 w-3 transition-all group-hover:opacity-100 ' ,
1199+ isExpanded ? 'rotate-180 opacity-100 ' : 'rotate-90 opacity-0 '
12041200 ) }
12051201 aria-hidden = 'true'
12061202 />
@@ -1223,18 +1219,10 @@ function SubagentContentRenderer({
12231219
12241220/**
12251221 * Determines if a tool call is "special" and should display with gradient styling.
1226- * Only workflow operation tools (edit, build, run, deploy) get the purple gradient .
1222+ * Uses the tool's UI config .
12271223 */
12281224function isSpecialToolCall ( toolCall : CopilotToolCall ) : boolean {
1229- const workflowOperationTools = [
1230- 'edit_workflow' ,
1231- 'build_workflow' ,
1232- 'run_workflow' ,
1233- 'deploy_api' ,
1234- 'deploy_chat' ,
1235- ]
1236-
1237- return workflowOperationTools . includes ( toolCall . name )
1225+ return isSpecialToolFromConfig ( toolCall . name )
12381226}
12391227
12401228/**
@@ -1468,29 +1456,27 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
14681456 key = { `${ type } -${ change . blockId } ` }
14691457 className = 'overflow-hidden rounded-md border border-[var(--border-1)] bg-[var(--surface-1)]'
14701458 >
1471- { /* Block header */ }
1472- < div className = 'flex items-center justify-between px-2.5 py-2' >
1473- < div className = 'flex items-center gap-2' >
1474- { /* Toolbar-style icon: colored square with white icon */ }
1475- < div
1476- className = 'flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-[3px]'
1477- style = { { background : bgColor } }
1478- >
1479- { Icon && < Icon className = 'h-[10px] w-[10px] text-white' /> }
1480- </ div >
1481- < span
1482- className = { `font-medium font-season text-[12px] ${ type === 'delete' ? 'text-[var(--text-tertiary)] line-through' : 'text-[var(--text-primary)]' } ` }
1483- >
1484- { change . blockName }
1485- </ span >
1459+ { /* Block header - gray background like plan/table headers */ }
1460+ < div className = 'flex items-center bg-[var(--surface-2)] px-2.5 py-2' >
1461+ { /* Toolbar-style icon: colored square with white icon */ }
1462+ < div
1463+ className = 'flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-[3px]'
1464+ style = { { background : bgColor } }
1465+ >
1466+ { Icon && < Icon className = 'h-[10px] w-[10px] text-white' /> }
14861467 </ div >
1487- { /* Action icon in top right */ }
1488- < span className = { `font-bold font-mono text-[14px] ${ color } ` } > { symbol } </ span >
1468+ < span
1469+ className = { `ml-2 font-medium font-season text-[12px] ${ type === 'delete' ? 'text-[var(--text-tertiary)] line-through' : 'text-[var(--text-primary)]' } ` }
1470+ >
1471+ { change . blockName }
1472+ </ span >
1473+ { /* Action icon next to block name */ }
1474+ < span className = { `ml-1.5 font-bold font-mono text-[12px] ${ color } ` } > { symbol } </ span >
14891475 </ div >
14901476
1491- { /* Subblock details - uses same title and value formatting as canvas */ }
1477+ { /* Subblock details - dark background like table/plan body */ }
14921478 { subBlocksToShow && subBlocksToShow . length > 0 && (
1493- < div className = 'border-[var(--border-1)] border-t bg-[var(--surface-2)] px-2.5 py-1.5' >
1479+ < div className = 'border-[var(--border-1)] border-t px-2.5 py-1.5' >
14941480 { subBlocksToShow . map ( ( sb ) => {
14951481 // Mask password fields like the canvas does
14961482 const displayValue = sb . isPassword ? '•••' : getDisplayValue ( sb . value )
@@ -1533,6 +1519,12 @@ function isIntegrationTool(toolName: string): boolean {
15331519}
15341520
15351521function shouldShowRunSkipButtons ( toolCall : CopilotToolCall ) : boolean {
1522+ // First check UI config for interrupt
1523+ if ( hasInterruptFromConfig ( toolCall . name ) && toolCall . state === 'pending' ) {
1524+ return true
1525+ }
1526+
1527+ // Then check instance-level interrupt
15361528 const instance = getClientTool ( toolCall . id )
15371529 let hasInterrupt = ! ! instance ?. getInterruptDisplays ?.( )
15381530 if ( ! hasInterrupt ) {
@@ -1895,21 +1887,39 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
18951887 if ( ! isClientTool && ! isIntegrationToolInBuildMode ) {
18961888 return null
18971889 }
1890+ // Check if tool has params table config (meaning it's expandable)
1891+ const hasParamsTable = ! ! getToolUIConfig ( toolCall . name ) ?. paramsTable
18981892 const isExpandableTool =
1893+ hasParamsTable ||
18991894 toolCall . name === 'make_api_request' ||
19001895 toolCall . name === 'set_global_workflow_variables' ||
19011896 toolCall . name === 'run_workflow'
19021897
19031898 const showButtons = shouldShowRunSkipButtons ( toolCall )
1899+
1900+ // Check UI config for secondary action
1901+ const toolUIConfig = getToolUIConfig ( toolCall . name )
1902+ const secondaryAction = toolUIConfig ?. secondaryAction
1903+ const showSecondaryAction =
1904+ secondaryAction &&
1905+ secondaryAction . showInStates . includes ( toolCall . state as ClientToolCallState )
1906+
1907+ // Legacy fallbacks for tools that haven't migrated to UI config
19041908 const showMoveToBackground =
1905- toolCall . name === 'run_workflow' &&
1906- ( toolCall . state === ( ClientToolCallState . executing as any ) ||
1907- toolCall . state === ( 'executing' as any ) )
1909+ showSecondaryAction && secondaryAction ?. text === 'Move to Background'
1910+ ? true
1911+ : ! secondaryAction &&
1912+ toolCall . name === 'run_workflow' &&
1913+ ( toolCall . state === ( ClientToolCallState . executing as any ) ||
1914+ toolCall . state === ( 'executing' as any ) )
19081915
19091916 const showWake =
1910- toolCall . name === 'sleep' &&
1911- ( toolCall . state === ( ClientToolCallState . executing as any ) ||
1912- toolCall . state === ( 'executing' as any ) )
1917+ showSecondaryAction && secondaryAction ?. text === 'Wake'
1918+ ? true
1919+ : ! secondaryAction &&
1920+ toolCall . name === 'sleep' &&
1921+ ( toolCall . state === ( ClientToolCallState . executing as any ) ||
1922+ toolCall . state === ( 'executing' as any ) )
19131923
19141924 const handleStateChange = ( state : any ) => {
19151925 forceUpdate ( { } )
@@ -2262,8 +2272,12 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
22622272 return null
22632273 }
22642274
2265- // Special handling for set_environment_variables - always stacked, always expanded
2266- if ( toolCall . name === 'set_environment_variables' && toolCall . state === 'pending' ) {
2275+ // Special handling for tools with alwaysExpanded config (e.g., set_environment_variables)
2276+ const isAlwaysExpanded = toolUIConfig ?. alwaysExpanded
2277+ if (
2278+ ( isAlwaysExpanded || toolCall . name === 'set_environment_variables' ) &&
2279+ toolCall . state === 'pending'
2280+ ) {
22672281 const isEnvVarsClickable = isAutoAllowed
22682282
22692283 const handleEnvVarsClick = ( ) => {
@@ -2313,8 +2327,8 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
23132327 )
23142328 }
23152329
2316- // Special rendering for function_execute - show code block
2317- if ( toolCall . name === 'function_execute' ) {
2330+ // Special rendering for tools with ' code' customRenderer (e.g., function_execute)
2331+ if ( toolUIConfig ?. customRenderer === 'code' || toolCall . name === 'function_execute' ) {
23182332 const code = params . code || ''
23192333 const isFunctionExecuteClickable = isAutoAllowed
23202334
0 commit comments