Skip to content

Commit d228a18

Browse files
committed
fix(chat): limit favicon loads to the local agent browser
1 parent adba14c commit d228a18

2 files changed

Lines changed: 86 additions & 4 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.test.tsx

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,34 @@
33
*/
44
import { act } from 'react'
55
import { createRoot, type Root } from 'react-dom/client'
6-
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
77
import type { AgentGroupItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view'
88
import {
99
BrowserAgentIcon,
1010
getBrowserAgentFaviconUrl,
1111
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon'
1212
import type { ToolCallData } from '@/app/workspace/[workspaceId]/home/types'
13+
import { useBrowserSessionStore } from '@/stores/browser-session/store'
14+
15+
const { browserAvailable, chatIdentity } = vi.hoisted(() => ({
16+
browserAvailable: vi.fn(() => true),
17+
chatIdentity: { chatId: 'chat-1' },
18+
}))
19+
vi.mock('@/lib/browser-agent/transport', () => ({ isBrowserAgentAvailable: browserAvailable }))
20+
vi.mock('@/app/workspace/[workspaceId]/home/components/chat-surface-context', () => ({
21+
useChatSurface: () => chatIdentity,
22+
}))
23+
24+
function openPage(url: string, scopeId = 'chat-1', loading = false) {
25+
act(() =>
26+
useBrowserSessionStore.getState().setTabsState({
27+
scopeId,
28+
activeTabId: 'tab-1',
29+
automationTabId: 'tab-1',
30+
tabs: [{ tabId: 'tab-1', title: '', url, loading, active: true }],
31+
})
32+
)
33+
}
1334

1435
function tool(overrides: Partial<ToolCallData> = {}): AgentGroupItem {
1536
return {
@@ -189,6 +210,9 @@ describe('BrowserAgentIcon', () => {
189210

190211
beforeEach(() => {
191212
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
213+
browserAvailable.mockReturnValue(true)
214+
chatIdentity.chatId = 'chat-1'
215+
useBrowserSessionStore.setState({ sessions: {}, activeScopeId: null })
192216
container = document.createElement('div')
193217
root = createRoot(container)
194218
})
@@ -203,7 +227,45 @@ describe('BrowserAgentIcon', () => {
203227
)
204228
}
205229

230+
it('does not contact sites from history, other chats, or pending navigation', () => {
231+
render('https://example.com/document')
232+
expect(container.querySelector('img')).toBeNull()
233+
openPage('https://example.com', 'another-chat')
234+
expect(container.querySelector('img')).toBeNull()
235+
openPage('https://example.com', 'chat-1', true)
236+
expect(container.querySelector('img')).toBeNull()
237+
openPage('https://example.org')
238+
expect(container.querySelector('img')).toBeNull()
239+
openPage('https://example.com')
240+
expect(container.querySelector('img')).not.toBeNull()
241+
})
242+
243+
it('requires the local desktop browser and retains an already loaded favicon after closing it', () => {
244+
browserAvailable.mockReturnValue(false)
245+
openPage('https://example.com')
246+
render('https://example.com')
247+
expect(container.querySelector('img')).toBeNull()
248+
browserAvailable.mockReturnValue(true)
249+
render('https://example.com')
250+
const img = container.querySelector('img')!
251+
act(() => img.dispatchEvent(new Event('load')))
252+
act(() => useBrowserSessionStore.getState().discardScope('chat-1'))
253+
expect(container.querySelector('img')).toBe(img)
254+
expect(container.querySelector('svg')).toBeNull()
255+
})
256+
257+
it('does not carry loaded state into another chat with the same destination', () => {
258+
openPage('https://example.com')
259+
render('https://example.com')
260+
act(() => container.querySelector('img')!.dispatchEvent(new Event('load')))
261+
chatIdentity.chatId = 'chat-2'
262+
render('https://example.com')
263+
expect(container.querySelector('img')).toBeNull()
264+
expect(container.querySelector('svg')).not.toBeNull()
265+
})
266+
206267
it('keeps the globe until load and resets image state when the page origin changes', () => {
268+
openPage('https://example.com/document')
207269
render('https://username:password@example.com/document?token=private#section')
208270
const firstImage = container.querySelector('img')!
209271
expect(firstImage.src).toBe('https://example.com/favicon.ico')
@@ -216,6 +278,7 @@ describe('BrowserAgentIcon', () => {
216278
expect(container.querySelector('img')).toBe(firstImage)
217279
expect(container.querySelector('svg')).toBeNull()
218280

281+
openPage('https://example.org/document')
219282
render('https://example.org/document')
220283
expect(container.querySelector('img')).not.toBe(firstImage)
221284
expect(container.querySelector('svg')).not.toBeNull()
@@ -225,6 +288,7 @@ describe('BrowserAgentIcon', () => {
225288
expect(container.querySelector('img')).toBeNull()
226289
expect(container.querySelector('svg')).not.toBeNull()
227290

291+
openPage('https://example.com')
228292
render('https://example.com')
229293
expect(container.querySelector('img')).not.toBeNull()
230294
render('about:blank')

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import { isBrowserToolName } from '@sim/browser-protocol'
55
import { cn } from '@sim/emcn'
66
import { Globe } from '@sim/emcn/icons'
77
import { isRecordLike } from '@sim/utils/object'
8+
import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport'
9+
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
810
import type { AgentGroupItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view'
911
import { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types'
12+
import { useBrowserSessionStore } from '@/stores/browser-session/store'
1013

1114
function pageFaviconUrl(url: string): string | null {
1215
try {
@@ -90,20 +93,35 @@ interface BrowserAgentIconProps {
9093
}
9194

9295
export function BrowserAgentIcon({ items }: BrowserAgentIconProps) {
96+
const { chatId } = useChatSurface()
9397
const url = getBrowserAgentFaviconUrl(items)
94-
return <BrowserAgentFavicon key={url} url={url} />
98+
const pageIsOpen = useBrowserSessionStore((state) => {
99+
const session = chatId ? state.sessions[chatId] : undefined
100+
if (!url || !session?.sessionAlive || session.suspended) return false
101+
const tab = session.tabs.find((tab) => tab.tabId === session.automationTabId)
102+
return Boolean(tab && !tab.loading && !tab.issue && pageFaviconUrl(tab.url) === url)
103+
})
104+
return (
105+
<BrowserAgentFavicon
106+
key={`${chatId ?? ''}:${url ?? ''}`}
107+
url={url}
108+
canLoad={isBrowserAgentAvailable() && pageIsOpen}
109+
/>
110+
)
95111
}
96112

97113
interface BrowserAgentFaviconProps {
98114
url: string | null
115+
canLoad: boolean
99116
}
100117

101-
function BrowserAgentFavicon({ url }: BrowserAgentFaviconProps) {
118+
function BrowserAgentFavicon({ url, canLoad }: BrowserAgentFaviconProps) {
102119
const [status, setStatus] = useState<'loading' | 'loaded' | 'failed'>('loading')
103120

104121
return (
105122
<span className='relative flex size-[16px] items-center justify-center' aria-hidden='true'>
106-
{url && status !== 'failed' && (
123+
{/** History alone must not contact a site; keep an already loaded image after the tab closes. */}
124+
{url && status !== 'failed' && (canLoad || status === 'loaded') && (
107125
<img
108126
src={url}
109127
referrerPolicy='no-referrer'

0 commit comments

Comments
 (0)