Diff view in chat

This commit is contained in:
Siddharth Ganesan
2026-01-07 12:12:05 -08:00
parent 9d23a3fd1b
commit 6750024991
2 changed files with 157 additions and 69 deletions

View File

@@ -7,7 +7,7 @@ import { ChevronUp } from 'lucide-react'
/**
* Max height for thinking content before internal scrolling kicks in
*/
const THINKING_MAX_HEIGHT = 125
const THINKING_MAX_HEIGHT = 200
/**
* Interval for auto-scroll during streaming (ms)

View File

@@ -385,7 +385,7 @@ function SubAgentToolCall({ toolCall: toolCallProp }: { toolCall: CopilotToolCal
/>
)}
{renderSubAgentTable()}
<WorkflowEditSummary toolCall={toolCall} />
{/* WorkflowEditSummary is rendered outside SubAgentContent for edit subagent */}
{showButtons && <RunSkipButtons toolCall={toolCall} />}
</div>
)
@@ -406,7 +406,7 @@ function getDisplayNameForSubAgent(toolCall: CopilotToolCall): string {
/**
* Max height for subagent content before internal scrolling kicks in
*/
const SUBAGENT_MAX_HEIGHT = 125
const SUBAGENT_MAX_HEIGHT = 200
/**
* Interval for auto-scroll during streaming (ms)
@@ -582,6 +582,16 @@ function SubAgentContent({
})}
</div>
)}
{/* Render WorkflowEditSummary outside the collapsible container for edit_workflow tool calls */}
{blocks
.filter((block) => block.type === 'subagent_tool_call' && block.toolCall?.name === 'edit_workflow')
.map((block, index) => (
<WorkflowEditSummary
key={`edit-summary-${block.toolCall?.id || index}`}
toolCall={block.toolCall!}
/>
))}
</div>
)
}
@@ -607,16 +617,25 @@ function isSpecialToolCall(toolCall: CopilotToolCall): boolean {
* Expands inline on click to show individual blocks with their icons.
*/
function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
const [isExpanded, setIsExpanded] = useState(false)
// Get workflow name from registry
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
const workflows = useWorkflowRegistry((s) => s.workflows)
const workflowName = activeWorkflowId ? workflows[activeWorkflowId]?.name : undefined
// Get block data from current workflow state
const blocks = useWorkflowStore((s) => s.blocks)
// Cache block info on first render (before diff is applied) so we can show
// deleted blocks properly even after they're removed from the workflow
const cachedBlockInfoRef = useRef<Record<string, { name: string; type: string }>>({})
// Update cache with current block info (only add, never remove)
useEffect(() => {
for (const [blockId, block] of Object.entries(blocks)) {
if (!cachedBlockInfoRef.current[blockId]) {
cachedBlockInfoRef.current[blockId] = {
name: block.name || '',
type: block.type || '',
}
}
}
}, [blocks])
// Show for edit_workflow regardless of state
if (toolCall.name !== 'edit_workflow') {
return null
@@ -632,10 +651,19 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
}
// Group operations by type with block info
interface SubBlockPreview {
id: string
value: any
}
interface BlockChange {
blockId: string
blockName: string
blockType: string
/** All subblocks for add operations */
subBlocks?: SubBlockPreview[]
/** Only changed subblocks for edit operations */
changedSubBlocks?: SubBlockPreview[]
}
const addedBlocks: BlockChange[] = []
@@ -646,10 +674,11 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
const blockId = op.block_id
if (!blockId) continue
// Get block info from current workflow state or operation params
// Get block info from current workflow state, cached state, or operation params
const currentBlock = blocks[blockId]
let blockName = currentBlock?.name || ''
let blockType = currentBlock?.type || ''
const cachedBlock = cachedBlockInfoRef.current[blockId]
let blockName = currentBlock?.name || cachedBlock?.name || ''
let blockType = currentBlock?.type || cachedBlock?.type || ''
// For add operations, get info from params (type is stored as params.type)
if (op.operation_type === 'add' && op.params) {
@@ -662,11 +691,45 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
blockType = op.params.type || ''
}
// Skip edge-only edit operations (like how we don't highlight blocks on canvas for edge changes)
// An edit is edge-only if params only contains 'connections' and nothing else meaningful
if (op.operation_type === 'edit' && op.params) {
const paramKeys = Object.keys(op.params)
const isEdgeOnlyEdit = paramKeys.length === 1 && paramKeys[0] === 'connections'
if (isEdgeOnlyEdit) {
continue
}
}
// For delete operations, check if block info was provided in operation
if (op.operation_type === 'delete') {
// Some delete operations may include block_name and block_type
blockName = blockName || op.block_name || ''
blockType = blockType || op.block_type || ''
}
// Fallback name to type or ID
if (!blockName) blockName = blockType || blockId
const change: BlockChange = { blockId, blockName, blockType }
// Extract subblock info from operation params
if (op.params?.inputs && typeof op.params.inputs === 'object') {
const subBlocks: SubBlockPreview[] = []
for (const [id, value] of Object.entries(op.params.inputs)) {
// Skip empty values and connections
if (value === null || value === undefined || value === '') continue
subBlocks.push({ id, value })
}
if (subBlocks.length > 0) {
if (op.operation_type === 'add') {
change.subBlocks = subBlocks
} else if (op.operation_type === 'edit') {
change.changedSubBlocks = subBlocks
}
}
}
switch (op.operation_type) {
case 'add':
addedBlocks.push(change)
@@ -691,8 +754,40 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
return getBlock(blockType)
}
// Render a single block row (toolbar style: colored square with white icon)
const renderBlockRow = (
// Format subblock value for display
const formatSubBlockValue = (value: any): string => {
if (value === null || value === undefined) return ''
if (typeof value === 'string') {
// Truncate long strings
return value.length > 60 ? `${value.slice(0, 60)}...` : value
}
if (typeof value === 'boolean') return value ? 'true' : 'false'
if (typeof value === 'number') return String(value)
if (Array.isArray(value)) {
if (value.length === 0) return '[]'
return `[${value.length} items]`
}
if (typeof value === 'object') {
const keys = Object.keys(value)
if (keys.length === 0) return '{}'
return `{${keys.length} fields}`
}
return String(value)
}
// Format subblock ID to readable label
const formatSubBlockLabel = (id: string): string => {
return id
.replace(/([A-Z])/g, ' $1')
.replace(/[_-]/g, ' ')
.trim()
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ')
}
// Render a single block item with action icon and details
const renderBlockItem = (
change: BlockChange,
type: 'add' | 'edit' | 'delete'
) => {
@@ -700,74 +795,67 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
const Icon = blockConfig?.icon
const bgColor = blockConfig?.bgColor || '#6B7280'
const symbols = {
const actionIcons = {
add: { symbol: '+', color: 'text-[#22c55e]' },
edit: { symbol: '', color: 'text-[#f97316]' },
edit: { symbol: '~', color: 'text-[#f97316]' },
delete: { symbol: '-', color: 'text-[#ef4444]' },
}
const { symbol, color } = symbols[type]
const { symbol, color } = actionIcons[type]
const subBlocksToShow = type === 'add' ? change.subBlocks : type === 'edit' ? change.changedSubBlocks : undefined
return (
<div
key={`${type}-${change.blockId}`}
className='flex items-center gap-2 px-2.5 py-1.5'
className='rounded-md border border-[var(--border-1)] bg-[var(--surface-1)] overflow-hidden'
>
<span className={`font-mono text-[11px] font-medium ${color} w-3`}>{symbol}</span>
{/* 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' />}
{/* 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>
</div>
{/* Action icon in top right */}
<span className={`font-mono text-[14px] font-bold ${color}`}>{symbol}</span>
</div>
<span
className={`font-season text-[12px] ${type === 'delete' ? 'text-[var(--text-tertiary)] line-through' : 'text-[var(--text-secondary)]'}`}
>
{change.blockName}
</span>
{/* Subblock details */}
{subBlocksToShow && subBlocksToShow.length > 0 && (
<div className='border-t border-[var(--border-1)] bg-[var(--surface-2)] px-2.5 py-1.5'>
{subBlocksToShow.map((sb) => (
<div
key={sb.id}
className='flex items-start gap-1.5 py-0.5 text-[11px]'
>
<span className={`font-medium ${type === 'edit' ? 'text-[#f97316]' : 'text-[var(--text-tertiary)]'}`}>
{formatSubBlockLabel(sb.id)}:
</span>
<span className='text-[var(--text-muted)] line-clamp-1 break-all'>
{formatSubBlockValue(sb.value)}
</span>
</div>
))}
</div>
)}
</div>
)
}
return (
<div className='mt-2 w-full overflow-hidden rounded-md border border-[var(--border-1)] bg-[var(--surface-1)]'>
{/* Header row - always visible */}
<button
type='button'
onClick={() => setIsExpanded(!isExpanded)}
className='flex w-full items-center justify-between px-2.5 py-2 transition-colors hover:bg-[var(--surface-2)]'
>
<div className='flex items-center gap-2'>
<span className='font-medium font-season text-[12px] text-[var(--text-primary)]'>
{workflowName || 'Workflow'}
</span>
<span className='flex items-center gap-1.5'>
{addedBlocks.length > 0 && (
<span className='font-mono text-[11px] font-medium text-[#22c55e]'>+{addedBlocks.length}</span>
)}
{editedBlocks.length > 0 && (
<span className='font-mono text-[11px] font-medium text-[#f97316]'>{editedBlocks.length}</span>
)}
{deletedBlocks.length > 0 && (
<span className='font-mono text-[11px] font-medium text-[#ef4444]'>-{deletedBlocks.length}</span>
)}
</span>
</div>
{isExpanded ? (
<ChevronUp className='h-3.5 w-3.5 text-[var(--text-tertiary)]' />
) : (
<ChevronDown className='h-3.5 w-3.5 text-[var(--text-tertiary)]' />
)}
</button>
{/* Expanded block list */}
{isExpanded && (
<div className='border-t border-[var(--border-1)]'>
{addedBlocks.map((change) => renderBlockRow(change, 'add'))}
{editedBlocks.map((change) => renderBlockRow(change, 'edit'))}
{deletedBlocks.map((change) => renderBlockRow(change, 'delete'))}
</div>
)}
<div className='mt-2 flex flex-col gap-1.5'>
{addedBlocks.map((change) => renderBlockItem(change, 'add'))}
{editedBlocks.map((change) => renderBlockItem(change, 'edit'))}
{deletedBlocks.map((change) => renderBlockItem(change, 'delete'))}
</div>
)
}