Skip to content

Commit 913dd20

Browse files
committed
fix(copilot): refresh custom block metadata after hydration
1 parent 5584a31 commit 913dd20

4 files changed

Lines changed: 138 additions & 10 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
/**
2-
* @vitest-environment node
2+
* @vitest-environment jsdom
33
*/
4-
import type { ReactNode, SVGProps } from 'react'
4+
import { act, type ReactNode, type SVGProps } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
56
import { renderToStaticMarkup } from 'react-dom/server'
6-
import { describe, expect, it, vi } from 'vitest'
7-
import { getBlockByToolName } from '@/blocks/registry'
7+
import { beforeEach, describe, expect, it, vi } from 'vitest'
8+
import { notifyBlockOverlayChanged } from '@/blocks/custom/client-overlay'
9+
import { getBlock, getBlockByToolName } from '@/blocks/registry'
810
import { ToolCallItem } from './tool-call-item'
911

1012
vi.mock('@/components/ui', () => ({
1113
ShimmerText: ({ children }: { children: ReactNode }) => <span>{children}</span>,
1214
}))
1315

1416
describe('ToolCallItem', () => {
17+
beforeEach(() => {
18+
vi.clearAllMocks()
19+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
20+
})
21+
1522
it.each(['executing', 'success', 'error', 'cancelled'] as const)(
1623
'renders the %s tool row without an icon',
1724
(status) => {
@@ -115,4 +122,34 @@ describe('ToolCallItem', () => {
115122
expect(markup).toContain('<svg')
116123
expect(markup).toContain('Read recent emails')
117124
})
125+
126+
it('refreshes the read icon when custom blocks hydrate after mount', () => {
127+
vi.mocked(getBlock).mockReturnValue(undefined)
128+
const container = document.createElement('div')
129+
const root: Root = createRoot(container)
130+
131+
act(() => {
132+
root.render(
133+
<ToolCallItem
134+
toolName='read'
135+
displayTitle='Read Custom block invoice parser'
136+
status='success'
137+
params={{
138+
path: 'organization/custom-blocks/custom_block_invoice_parser.json',
139+
}}
140+
/>
141+
)
142+
})
143+
expect(container.querySelector('[data-testid="custom-block-icon"]')).toBeNull()
144+
145+
vi.mocked(getBlock).mockReturnValue({
146+
type: 'custom_block_invoice_parser',
147+
name: 'Invoice Parser',
148+
icon: (props: SVGProps<SVGSVGElement>) => <svg {...props} data-testid='custom-block-icon' />,
149+
} as ReturnType<typeof getBlock>)
150+
act(() => notifyBlockOverlayChanged())
151+
152+
expect(container.querySelector('[data-testid="custom-block-icon"]')).not.toBeNull()
153+
act(() => root.unmount())
154+
})
118155
})

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { RETIRED_BROWSER_REQUEST_TAKEOVER_ID } from '@/lib/copilot/tools/retired
1313
import { extractStreamingStringArgument } from '@/lib/copilot/tools/streaming-args'
1414
import { getToolStatusDisplayTitle, getWaitCountdownTitle } from '@/lib/copilot/tools/tool-display'
1515
import { BrandIcon } from '@/blocks/brand-icon'
16+
import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay'
1617
import { getBlockByToolName } from '@/blocks/registry'
1718
import type { ToolCallData, ToolCallStatus } from '../../../../types'
1819
import { resolveToolDisplayState } from '../../utils'
@@ -122,11 +123,12 @@ export function ToolCallItem({
122123
toolCallId,
123124
startedAt,
124125
}: ToolCallItemProps) {
125-
const readBlock = useMemo(() => {
126-
if (toolName !== ReadTool.id) return undefined
127-
const path = params?.path
128-
return typeof path === 'string' ? getReadTargetBlock(path) : undefined
129-
}, [toolName, params])
126+
useCustomBlockOverlayVersion()
127+
const readPath = params?.path
128+
const readBlock =
129+
toolName === ReadTool.id && typeof readPath === 'string'
130+
? getReadTargetBlock(readPath)
131+
: undefined
130132

131133
// Like read's VFS-target resolution above, the gateway uses its exact
132134
// discovered toolId only as a deterministic registry lookup. This renders
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
6+
import { beforeEach, describe, expect, it, vi } from 'vitest'
7+
8+
const { mockGetBlock } = vi.hoisted(() => ({
9+
mockGetBlock: vi.fn(),
10+
}))
11+
12+
vi.mock('@/blocks/registry', () => ({
13+
getBlock: mockGetBlock,
14+
getBlockByToolName: vi.fn(),
15+
getLatestBlock: vi.fn(),
16+
}))
17+
18+
vi.mock('@/lib/auth/auth-client', () => ({
19+
useSession: vi.fn(() => ({ data: null, isPending: false })),
20+
}))
21+
22+
interface MockAgentGroupItem {
23+
type: string
24+
data?: { id: string; displayTitle: string }
25+
}
26+
27+
vi.mock('./components', () => ({
28+
AgentGroup: ({ items }: { items: MockAgentGroupItem[] }) => (
29+
<div>
30+
{items.map((item) => item.data && <span key={item.data.id}>{item.data.displayTitle}</span>)}
31+
</div>
32+
),
33+
ChatContent: () => null,
34+
CircleStop: () => null,
35+
Options: () => null,
36+
PendingTagIndicator: () => null,
37+
}))
38+
39+
import type { ContentBlock } from '@/app/workspace/[workspaceId]/home/types'
40+
import { notifyBlockOverlayChanged } from '@/blocks/custom/client-overlay'
41+
import { MessageContent } from './message-content'
42+
43+
describe('MessageContent custom-block hydration', () => {
44+
beforeEach(() => {
45+
vi.clearAllMocks()
46+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
47+
})
48+
49+
it('refreshes a read title when the custom-block registry hydrates after mount', () => {
50+
mockGetBlock.mockReturnValue(undefined)
51+
const blocks: ContentBlock[] = [
52+
{
53+
type: 'tool_call',
54+
toolCall: {
55+
id: 'read-custom-block',
56+
name: 'read',
57+
status: 'success',
58+
params: {
59+
path: 'organization/custom-blocks/custom_block_invoice_parser.json',
60+
},
61+
},
62+
timestamp: 1,
63+
},
64+
]
65+
const container = document.createElement('div')
66+
const root: Root = createRoot(container)
67+
68+
act(() => {
69+
root.render(<MessageContent blocks={blocks} fallbackContent='' isStreaming={false} />)
70+
})
71+
expect(container.textContent).toContain('Read Custom block invoice parser')
72+
73+
mockGetBlock.mockReturnValue({
74+
type: 'custom_block_invoice_parser',
75+
name: 'Invoice Parser',
76+
icon: () => null,
77+
})
78+
act(() => notifyBlockOverlayChanged())
79+
80+
expect(container.textContent).toContain('Read Invoice Parser')
81+
expect(container.textContent).not.toContain('Read Custom block invoice parser')
82+
act(() => root.unmount())
83+
})
84+
})

apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from '@/lib/copilot/tools/tool-display'
2323
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
2424
import type { CredentialSubmissionPayload } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
25+
import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay'
2526
import type { ContentBlock, OptionItem, ToolCallData } from '../../types'
2627
import { SUBAGENT_LABELS } from '../../types'
2728
import type { AgentGroupItem } from './components'
@@ -851,7 +852,11 @@ function MessageContentInner({
851852
actions,
852853
}: MessageContentProps) {
853854
const { onWorkspaceResourceSelect } = useChatSurface()
854-
const parsed = useMemo(() => (blocks.length > 0 ? parseBlocks(blocks) : []), [blocks])
855+
const blockOverlayVersion = useCustomBlockOverlayVersion()
856+
const parsed = useMemo(
857+
() => (blocks.length > 0 ? parseBlocks(blocks) : []),
858+
[blocks, blockOverlayVersion]
859+
)
855860

856861
const [trailingRevealing, setTrailingRevealing] = useState(false)
857862
const handleTrailingRevealChange = useCallback((revealing: boolean) => {

0 commit comments

Comments
 (0)