|
2 | 2 |
|
3 | 3 | import { useEffect, useRef, useState } from 'react' |
4 | 4 | import clsx from 'clsx' |
5 | | -import { ChevronUp } from 'lucide-react' |
| 5 | +import { ChevronDown, ChevronUp } from 'lucide-react' |
6 | 6 | import { Button, Code } from '@/components/emcn' |
7 | 7 | import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool' |
8 | 8 | import { getClientTool } from '@/lib/copilot/tools/client/manager' |
9 | 9 | import { getRegisteredTools } from '@/lib/copilot/tools/client/registry' |
| 10 | +import { getBlock } from '@/blocks/registry' |
10 | 11 | import { CLASS_TOOL_METADATA, useCopilotStore } from '@/stores/panel/copilot/store' |
11 | 12 | import type { CopilotToolCall, SubAgentContentBlock } from '@/stores/panel/copilot/types' |
| 13 | +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' |
| 14 | +import { useWorkflowStore } from '@/stores/workflows/workflow/store' |
12 | 15 |
|
13 | 16 | interface ToolCallProps { |
14 | 17 | toolCall?: CopilotToolCall |
@@ -231,7 +234,13 @@ function ShimmerOverlayText({ |
231 | 234 | /** |
232 | 235 | * SubAgentToolCall renders a nested tool call from a subagent in a muted/thinking style. |
233 | 236 | */ |
234 | | -function SubAgentToolCall({ toolCall }: { toolCall: CopilotToolCall }) { |
| 237 | +function SubAgentToolCall({ toolCall: toolCallProp }: { toolCall: CopilotToolCall }) { |
| 238 | + // Get live toolCall from store to ensure we have the latest state and params |
| 239 | + const liveToolCall = useCopilotStore((s) => |
| 240 | + toolCallProp.id ? s.toolCallsById[toolCallProp.id] : undefined |
| 241 | + ) |
| 242 | + const toolCall = liveToolCall || toolCallProp |
| 243 | + |
235 | 244 | const displayName = getDisplayNameForSubAgent(toolCall) |
236 | 245 |
|
237 | 246 | const isLoading = |
@@ -360,15 +369,23 @@ function SubAgentToolCall({ toolCall }: { toolCall: CopilotToolCall }) { |
360 | 369 | return null |
361 | 370 | } |
362 | 371 |
|
| 372 | + // For edit_workflow, only show the WorkflowEditSummary component (replaces text display) |
| 373 | + const isEditWorkflow = toolCall.name === 'edit_workflow' |
| 374 | + const hasOperations = Array.isArray(params.operations) && params.operations.length > 0 |
| 375 | + |
363 | 376 | return ( |
364 | 377 | <div className='py-0.5'> |
365 | | - <ShimmerOverlayText |
366 | | - text={displayName} |
367 | | - active={isLoading && !showButtons} |
368 | | - isSpecial={isSpecial} |
369 | | - className='font-[470] font-season text-[12px] text-[var(--text-tertiary)]' |
370 | | - /> |
| 378 | + {/* Hide text display for edit_workflow when we have operations to show in summary */} |
| 379 | + {!(isEditWorkflow && hasOperations) && ( |
| 380 | + <ShimmerOverlayText |
| 381 | + text={displayName} |
| 382 | + active={isLoading && !showButtons} |
| 383 | + isSpecial={isSpecial} |
| 384 | + className='font-[470] font-season text-[12px] text-[var(--text-tertiary)]' |
| 385 | + /> |
| 386 | + )} |
371 | 387 | {renderSubAgentTable()} |
| 388 | + <WorkflowEditSummary toolCall={toolCall} /> |
372 | 389 | {showButtons && <RunSkipButtons toolCall={toolCall} />} |
373 | 390 | </div> |
374 | 391 | ) |
@@ -584,6 +601,177 @@ function isSpecialToolCall(toolCall: CopilotToolCall): boolean { |
584 | 601 | return workflowOperationTools.includes(toolCall.name) |
585 | 602 | } |
586 | 603 |
|
| 604 | +/** |
| 605 | + * WorkflowEditSummary shows a full-width summary of workflow edits (like Cursor's diff). |
| 606 | + * Displays: workflow name with stats (+N green, N orange, -N red) |
| 607 | + * Expands inline on click to show individual blocks with their icons. |
| 608 | + */ |
| 609 | +function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) { |
| 610 | + const [isExpanded, setIsExpanded] = useState(false) |
| 611 | + |
| 612 | + // Get workflow name from registry |
| 613 | + const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) |
| 614 | + const workflows = useWorkflowRegistry((s) => s.workflows) |
| 615 | + const workflowName = activeWorkflowId ? workflows[activeWorkflowId]?.name : undefined |
| 616 | + |
| 617 | + // Get block data from current workflow state |
| 618 | + const blocks = useWorkflowStore((s) => s.blocks) |
| 619 | + |
| 620 | + // Show for edit_workflow regardless of state |
| 621 | + if (toolCall.name !== 'edit_workflow') { |
| 622 | + return null |
| 623 | + } |
| 624 | + |
| 625 | + // Extract operations from tool call params |
| 626 | + const params = (toolCall as any).parameters || (toolCall as any).input || (toolCall as any).params || {} |
| 627 | + let operations = Array.isArray(params.operations) ? params.operations : [] |
| 628 | + |
| 629 | + // Fallback: check if operations are at top level of toolCall |
| 630 | + if (operations.length === 0 && Array.isArray((toolCall as any).operations)) { |
| 631 | + operations = (toolCall as any).operations |
| 632 | + } |
| 633 | + |
| 634 | + // Group operations by type with block info |
| 635 | + interface BlockChange { |
| 636 | + blockId: string |
| 637 | + blockName: string |
| 638 | + blockType: string |
| 639 | + } |
| 640 | + |
| 641 | + const addedBlocks: BlockChange[] = [] |
| 642 | + const editedBlocks: BlockChange[] = [] |
| 643 | + const deletedBlocks: BlockChange[] = [] |
| 644 | + |
| 645 | + for (const op of operations) { |
| 646 | + const blockId = op.block_id |
| 647 | + if (!blockId) continue |
| 648 | + |
| 649 | + // Get block info from current workflow state or operation params |
| 650 | + const currentBlock = blocks[blockId] |
| 651 | + let blockName = currentBlock?.name || '' |
| 652 | + let blockType = currentBlock?.type || '' |
| 653 | + |
| 654 | + // For add operations, get info from params (type is stored as params.type) |
| 655 | + if (op.operation_type === 'add' && op.params) { |
| 656 | + blockName = blockName || op.params.name || '' |
| 657 | + blockType = blockType || op.params.type || '' |
| 658 | + } |
| 659 | + |
| 660 | + // For edit operations, also check params.type if block not in current state |
| 661 | + if (op.operation_type === 'edit' && op.params && !blockType) { |
| 662 | + blockType = op.params.type || '' |
| 663 | + } |
| 664 | + |
| 665 | + // Fallback name to type or ID |
| 666 | + if (!blockName) blockName = blockType || blockId |
| 667 | + |
| 668 | + const change: BlockChange = { blockId, blockName, blockType } |
| 669 | + |
| 670 | + switch (op.operation_type) { |
| 671 | + case 'add': |
| 672 | + addedBlocks.push(change) |
| 673 | + break |
| 674 | + case 'edit': |
| 675 | + editedBlocks.push(change) |
| 676 | + break |
| 677 | + case 'delete': |
| 678 | + deletedBlocks.push(change) |
| 679 | + break |
| 680 | + } |
| 681 | + } |
| 682 | + |
| 683 | + const hasChanges = addedBlocks.length > 0 || editedBlocks.length > 0 || deletedBlocks.length > 0 |
| 684 | + |
| 685 | + if (!hasChanges) { |
| 686 | + return null |
| 687 | + } |
| 688 | + |
| 689 | + // Get block config by type (for icon and bgColor) |
| 690 | + const getBlockConfig = (blockType: string) => { |
| 691 | + return getBlock(blockType) |
| 692 | + } |
| 693 | + |
| 694 | + // Render a single block row (toolbar style: colored square with white icon) |
| 695 | + const renderBlockRow = ( |
| 696 | + change: BlockChange, |
| 697 | + type: 'add' | 'edit' | 'delete' |
| 698 | + ) => { |
| 699 | + const blockConfig = getBlockConfig(change.blockType) |
| 700 | + const Icon = blockConfig?.icon |
| 701 | + const bgColor = blockConfig?.bgColor || '#6B7280' |
| 702 | + |
| 703 | + const symbols = { |
| 704 | + add: { symbol: '+', color: 'text-[#22c55e]' }, |
| 705 | + edit: { symbol: '•', color: 'text-[#f97316]' }, |
| 706 | + delete: { symbol: '-', color: 'text-[#ef4444]' }, |
| 707 | + } |
| 708 | + const { symbol, color } = symbols[type] |
| 709 | + |
| 710 | + return ( |
| 711 | + <div |
| 712 | + key={`${type}-${change.blockId}`} |
| 713 | + className='flex items-center gap-2 px-2.5 py-1.5' |
| 714 | + > |
| 715 | + <span className={`font-mono text-[11px] font-medium ${color} w-3`}>{symbol}</span> |
| 716 | + {/* Toolbar-style icon: colored square with white icon */} |
| 717 | + <div |
| 718 | + className='flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-[3px]' |
| 719 | + style={{ background: bgColor }} |
| 720 | + > |
| 721 | + {Icon && <Icon className='h-[10px] w-[10px] text-white' />} |
| 722 | + </div> |
| 723 | + <span |
| 724 | + className={`font-season text-[12px] ${type === 'delete' ? 'text-[var(--text-tertiary)] line-through' : 'text-[var(--text-secondary)]'}`} |
| 725 | + > |
| 726 | + {change.blockName} |
| 727 | + </span> |
| 728 | + </div> |
| 729 | + ) |
| 730 | + } |
| 731 | + |
| 732 | + return ( |
| 733 | + <div className='mt-2 w-full overflow-hidden rounded-md border border-[var(--border-1)] bg-[var(--surface-1)]'> |
| 734 | + {/* Header row - always visible */} |
| 735 | + <button |
| 736 | + type='button' |
| 737 | + onClick={() => setIsExpanded(!isExpanded)} |
| 738 | + className='flex w-full items-center justify-between px-2.5 py-2 transition-colors hover:bg-[var(--surface-2)]' |
| 739 | + > |
| 740 | + <div className='flex items-center gap-2'> |
| 741 | + <span className='font-medium font-season text-[12px] text-[var(--text-primary)]'> |
| 742 | + {workflowName || 'Workflow'} |
| 743 | + </span> |
| 744 | + <span className='flex items-center gap-1.5'> |
| 745 | + {addedBlocks.length > 0 && ( |
| 746 | + <span className='font-mono text-[11px] font-medium text-[#22c55e]'>+{addedBlocks.length}</span> |
| 747 | + )} |
| 748 | + {editedBlocks.length > 0 && ( |
| 749 | + <span className='font-mono text-[11px] font-medium text-[#f97316]'>•{editedBlocks.length}</span> |
| 750 | + )} |
| 751 | + {deletedBlocks.length > 0 && ( |
| 752 | + <span className='font-mono text-[11px] font-medium text-[#ef4444]'>-{deletedBlocks.length}</span> |
| 753 | + )} |
| 754 | + </span> |
| 755 | + </div> |
| 756 | + {isExpanded ? ( |
| 757 | + <ChevronUp className='h-3.5 w-3.5 text-[var(--text-tertiary)]' /> |
| 758 | + ) : ( |
| 759 | + <ChevronDown className='h-3.5 w-3.5 text-[var(--text-tertiary)]' /> |
| 760 | + )} |
| 761 | + </button> |
| 762 | + |
| 763 | + {/* Expanded block list */} |
| 764 | + {isExpanded && ( |
| 765 | + <div className='border-t border-[var(--border-1)]'> |
| 766 | + {addedBlocks.map((change) => renderBlockRow(change, 'add'))} |
| 767 | + {editedBlocks.map((change) => renderBlockRow(change, 'edit'))} |
| 768 | + {deletedBlocks.map((change) => renderBlockRow(change, 'delete'))} |
| 769 | + </div> |
| 770 | + )} |
| 771 | + </div> |
| 772 | + ) |
| 773 | +} |
| 774 | + |
587 | 775 | /** |
588 | 776 | * Checks if a tool is an integration tool (server-side executed, not a client tool) |
589 | 777 | */ |
@@ -1515,6 +1703,9 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }: |
1515 | 1703 | </Button> |
1516 | 1704 | </div> |
1517 | 1705 | ) : null} |
| 1706 | + {/* Workflow edit summary - shows block changes after edit_workflow completes */} |
| 1707 | + <WorkflowEditSummary toolCall={toolCall} /> |
| 1708 | + |
1518 | 1709 | {/* Render subagent content (from debug tool or other subagents) */} |
1519 | 1710 | {toolCall.subAgentBlocks && toolCall.subAgentBlocks.length > 0 && ( |
1520 | 1711 | <SubAgentContent |
|
0 commit comments