Skip to content

Commit 14813d6

Browse files
committed
Diff in chat
1 parent 225ca42 commit 14813d6

File tree

1 file changed

+199
-8
lines changed
  • apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call

1 file changed

+199
-8
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx

Lines changed: 199 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
import { useEffect, useRef, useState } from 'react'
44
import clsx from 'clsx'
5-
import { ChevronUp } from 'lucide-react'
5+
import { ChevronDown, ChevronUp } from 'lucide-react'
66
import { Button, Code } from '@/components/emcn'
77
import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool'
88
import { getClientTool } from '@/lib/copilot/tools/client/manager'
99
import { getRegisteredTools } from '@/lib/copilot/tools/client/registry'
10+
import { getBlock } from '@/blocks/registry'
1011
import { CLASS_TOOL_METADATA, useCopilotStore } from '@/stores/panel/copilot/store'
1112
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'
1215

1316
interface ToolCallProps {
1417
toolCall?: CopilotToolCall
@@ -231,7 +234,13 @@ function ShimmerOverlayText({
231234
/**
232235
* SubAgentToolCall renders a nested tool call from a subagent in a muted/thinking style.
233236
*/
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+
235244
const displayName = getDisplayNameForSubAgent(toolCall)
236245

237246
const isLoading =
@@ -360,15 +369,23 @@ function SubAgentToolCall({ toolCall }: { toolCall: CopilotToolCall }) {
360369
return null
361370
}
362371

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+
363376
return (
364377
<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+
)}
371387
{renderSubAgentTable()}
388+
<WorkflowEditSummary toolCall={toolCall} />
372389
{showButtons && <RunSkipButtons toolCall={toolCall} />}
373390
</div>
374391
)
@@ -584,6 +601,177 @@ function isSpecialToolCall(toolCall: CopilotToolCall): boolean {
584601
return workflowOperationTools.includes(toolCall.name)
585602
}
586603

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+
587775
/**
588776
* Checks if a tool is an integration tool (server-side executed, not a client tool)
589777
*/
@@ -1515,6 +1703,9 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
15151703
</Button>
15161704
</div>
15171705
) : null}
1706+
{/* Workflow edit summary - shows block changes after edit_workflow completes */}
1707+
<WorkflowEditSummary toolCall={toolCall} />
1708+
15181709
{/* Render subagent content (from debug tool or other subagents) */}
15191710
{toolCall.subAgentBlocks && toolCall.subAgentBlocks.length > 0 && (
15201711
<SubAgentContent

0 commit comments

Comments
 (0)