Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export function AgentGroup({
toolName={item.data.toolName}
displayTitle={item.data.displayTitle}
status={item.data.status}
params={item.data.params}
streamingArgs={item.data.streamingArgs}
/>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useMemo } from 'react'
import { ShimmerText } from '@/components/ui'
import { WorkspaceFile } from '@/lib/copilot/generated/tool-catalog-v1'
import { Read as ReadTool, WorkspaceFile } from '@/lib/copilot/generated/tool-catalog-v1'
import { getReadTargetBlock } from '@/lib/copilot/tools/client/read-block'
import { getToolCompletedTitle } from '@/lib/copilot/tools/tool-display'
import { getBareIconStyle } from '@/blocks/icon-color'
import type { ToolCallStatus } from '../../../../types'
import { resolveToolDisplayState } from '../../utils'

Expand All @@ -25,6 +27,7 @@ interface ToolCallItemProps {
toolName: string
displayTitle: string
status: ToolCallStatus
params?: Record<string, unknown>
streamingArgs?: string
}

Expand All @@ -33,8 +36,22 @@ interface ToolCallItemProps {
* static label once terminal. For `workspace_file` the title is derived live
* from the streaming args; because that path bypasses the completed-title
* rewrite in `toToolData`, the past-tense flip is applied here on success.
* A `read` of a block or integration schema shows the block's brand icon
* inline next to its display name (e.g. the Gmail logo before "Read Gmail").
*/
export function ToolCallItem({ toolName, displayTitle, status, streamingArgs }: ToolCallItemProps) {
export function ToolCallItem({
toolName,
displayTitle,
status,
params,
streamingArgs,
}: ToolCallItemProps) {
const readBlock = useMemo(() => {
if (toolName !== ReadTool.id) return undefined
const path = params?.path
return typeof path === 'string' ? getReadTargetBlock(path) : undefined
}, [toolName, params])

const liveWorkspaceFileTitle = useMemo(() => {
if (toolName !== WorkspaceFile.id || !streamingArgs) return null
const titleMatch = streamingArgs.match(/"title"\s*:\s*"([^"]+)"/)
Expand Down Expand Up @@ -71,8 +88,13 @@ export function ToolCallItem({ toolName, displayTitle, status, streamingArgs }:
? (getToolCompletedTitle(liveTitle) ?? liveTitle)
: liveTitle

const BlockIcon = readBlock?.icon

return (
<div className='flex items-center pl-6'>
<div className='flex items-center gap-[6px] pl-6'>
{BlockIcon && (
<BlockIcon className='size-[14px] flex-shrink-0' style={getBareIconStyle(BlockIcon)} />
)}
{isExecuting ? (
<ShimmerText className='text-[13px] [--shimmer-rest:var(--text-secondary)]'>
{title}
Expand Down
30 changes: 30 additions & 0 deletions apps/sim/lib/copilot/tools/client/read-block.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @vitest-environment node
*/
import { describe, expect, it, vi } from 'vitest'
import { getReadTargetBlock } from '@/lib/copilot/tools/client/read-block'

const gmailBlock = { type: 'gmail_v2', name: 'Gmail', icon: () => null }

vi.mock('@/blocks/registry', () => ({
getBlock: vi.fn((type: string) => (type === 'gmail_v2' ? gmailBlock : undefined)),
getLatestBlock: vi.fn((baseType: string) => (baseType === 'gmail' ? gmailBlock : undefined)),
}))

describe('getReadTargetBlock', () => {
it('resolves a block schema read to its block', () => {
expect(getReadTargetBlock('components/blocks/gmail_v2.json')?.name).toBe('Gmail')
})

it('resolves integration operation and service-directory reads to the latest service block', () => {
expect(getReadTargetBlock('components/integrations/gmail/send.json')?.name).toBe('Gmail')
expect(getReadTargetBlock('components/integrations/gmail')?.name).toBe('Gmail')
})

it('returns undefined for unknown blocks and non-component paths', () => {
expect(getReadTargetBlock('components/blocks/unknown_block.json')).toBeUndefined()
expect(getReadTargetBlock('workflows/My Workflow/meta.json')).toBeUndefined()
expect(getReadTargetBlock('files/gmail_v2.json')).toBeUndefined()
expect(getReadTargetBlock(undefined)).toBeUndefined()
})
})
23 changes: 23 additions & 0 deletions apps/sim/lib/copilot/tools/client/read-block.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getBlock, getLatestBlock } from '@/blocks/registry'
import type { BlockConfig } from '@/blocks/types'

/**
* Resolves the block a copilot `read` call targets when the path is a
* component schema — `components/blocks/{type}.json` or
* `components/integrations/{service}/{operation}.json` — so tool rows can show
* the block's display name and brand icon instead of the raw type id
* (e.g. "Gmail" instead of `gmail_v2`). Returns undefined for every other
* path, leaving the generic read-target labeling untouched.
*/
export function getReadTargetBlock(path: string | undefined): BlockConfig | undefined {
if (!path) return undefined
const segments = path.trim().split('/').filter(Boolean)
if (segments[0] !== 'components' || segments.length < 3) return undefined
if (segments[1] === 'blocks' && segments.length === 3) {
return getBlock(segments[2].replace(/\.json$/, ''))
}
if (segments[1] === 'integrations') {
return getLatestBlock(segments[2])
}
Comment thread
waleedlatif1 marked this conversation as resolved.
return undefined
}
29 changes: 28 additions & 1 deletion apps/sim/lib/copilot/tools/client/store-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@
* @vitest-environment node
*/

import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { Read as ReadTool } from '@/lib/copilot/generated/tool-catalog-v1'
import { resolveToolDisplay } from './store-utils'
import { ClientToolCallState } from './tool-call-state'

const gmailBlock = { type: 'gmail_v2', name: 'Gmail', icon: () => null }

vi.mock('@/blocks/registry', () => ({
getBlock: vi.fn((type: string) => (type === 'gmail_v2' ? gmailBlock : undefined)),
getLatestBlock: vi.fn((baseType: string) => (baseType === 'gmail' ? gmailBlock : undefined)),
}))

describe('resolveToolDisplay', () => {
it('uses a friendly label for internal respond tools', () => {
expect(resolveToolDisplay('respond', ClientToolCallState.executing)?.text).toBe(
Expand Down Expand Up @@ -120,6 +127,26 @@ describe('resolveToolDisplay', () => {
).toBe('Read style details for deck.pptx')
})

it('shows the block display name for block and integration schema reads', () => {
expect(
resolveToolDisplay(ReadTool.id, ClientToolCallState.success, {
path: 'components/blocks/gmail_v2.json',
})?.text
).toBe('Read Gmail')

expect(
resolveToolDisplay(ReadTool.id, ClientToolCallState.executing, {
path: 'components/integrations/gmail/send.json',
})?.text
).toBe('Reading Gmail')

expect(
resolveToolDisplay(ReadTool.id, ClientToolCallState.success, {
path: 'components/blocks/unknown_block.json',
})?.text
).toBe('Read unknown_block')
})

it('falls back to a humanized tool label for generic tools', () => {
expect(resolveToolDisplay('deploy_api', ClientToolCallState.success)?.text).toBe(
'Executed Deploy Api'
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/lib/copilot/tools/client/store-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FileText } from 'lucide-react'
import { Read as ReadTool } from '@/lib/copilot/generated/tool-catalog-v1'
import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resources/types'
import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools'
import { getReadTargetBlock } from '@/lib/copilot/tools/client/read-block'
import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state'
import { decodeVfsSegment } from '@/lib/copilot/vfs/path-utils'

Expand Down Expand Up @@ -98,6 +99,9 @@ function decodeVfsSegmentSafe(segment: string): string {
function describeReadTarget(path: string | undefined): string | undefined {
if (!path) return undefined

const block = getReadTargetBlock(path)
if (block) return block.name

const segments = path
.split('/')
.map((segment) => segment.trim())
Expand Down
1 change: 1 addition & 0 deletions apps/sim/vitest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ vi.mock('@/blocks/registry', () => ({
outputs: {},
})),
getAllBlocks: vi.fn(() => ({})),
getLatestBlock: vi.fn(() => undefined),
}))

vi.mock('@trigger.dev/sdk', () => ({
Expand Down
Loading