Skip to content

Commit adba14c

Browse files
committed
fix(chat): load favicons from the visited site
1 parent c730028 commit adba14c

2 files changed

Lines changed: 77 additions & 49 deletions

File tree

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

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
77
import type { AgentGroupItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view'
88
import {
99
BrowserAgentIcon,
10-
getBrowserAgentHostname,
10+
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'
1313

@@ -25,53 +25,72 @@ function tool(overrides: Partial<ToolCallData> = {}): AgentGroupItem {
2525
}
2626
}
2727

28-
describe('getBrowserAgentHostname', () => {
28+
describe('getBrowserAgentFaviconUrl', () => {
29+
it('keeps the observed origin and port without disclosing page details', () => {
30+
expect(
31+
getBrowserAgentFaviconUrl([
32+
tool({
33+
result: {
34+
success: true,
35+
output: {
36+
url: 'https://username:password@example.com:8443/document?token=private#part',
37+
},
38+
},
39+
}),
40+
])
41+
).toBe('https://example.com:8443/favicon.ico')
42+
})
43+
2944
it('uses the pending destination, then the observed redirect, and retains it during editing', () => {
3045
const navigating = tool({
3146
status: 'executing',
3247
params: { url: 'https://example.org/start' },
3348
result: undefined,
3449
})
35-
expect(getBrowserAgentHostname([tool(), navigating])).toBe('example.org')
50+
expect(getBrowserAgentFaviconUrl([tool(), navigating])).toBe('https://example.org/favicon.ico')
3651
const redirected = tool({ params: { url: 'https://example.org/start' } })
37-
expect(getBrowserAgentHostname([redirected])).toBe('example.com')
52+
expect(getBrowserAgentFaviconUrl([redirected])).toBe('https://example.com/favicon.ico')
3853
expect(
39-
getBrowserAgentHostname([
54+
getBrowserAgentFaviconUrl([
4055
redirected,
4156
tool({ toolName: 'browser_type', status: 'executing', result: undefined }),
4257
])
43-
).toBe('example.com')
58+
).toBe('https://example.com/favicon.ico')
4459
})
4560

4661
it.each(['error', 'cancelled', 'rejected', 'awaiting_approval'] as const)(
4762
'does not adopt a destination from a %s tool',
4863
(status) => {
4964
expect(
50-
getBrowserAgentHostname([
65+
getBrowserAgentFaviconUrl([
5166
tool(),
5267
tool({ status, params: { url: 'https://example.org' }, result: undefined }),
5368
])
54-
).toBe('example.com')
69+
).toBe('https://example.com/favicon.ico')
5570
}
5671
)
5772

58-
it.each(['', 'about:blank', 'file:///document', 'data:text/html,hello', 'not a URL'])(
59-
'clears the previous site for a page without an HTTP hostname: %s',
60-
(url) => {
61-
expect(
62-
getBrowserAgentHostname([
63-
tool(),
64-
tool({ toolName: 'browser_switch_tab', result: { success: true, output: { url } } }),
65-
])
66-
).toBeNull()
67-
}
68-
)
73+
it.each([
74+
'',
75+
'about:blank',
76+
'http://example.com',
77+
'file:///document',
78+
'data:text/html,hello',
79+
'not a URL',
80+
])('clears the previous site when the page cannot supply an allowed favicon: %s', (url) => {
81+
expect(
82+
getBrowserAgentFaviconUrl([
83+
tool(),
84+
tool({ toolName: 'browser_switch_tab', result: { success: true, output: { url } } }),
85+
])
86+
).toBeNull()
87+
})
6988

7089
it.each(['browser_open_tab', 'browser_switch_tab', 'browser_close_tab', 'browser_go_back'])(
7190
'clears an obsolete site while %s has no known destination',
7291
(toolName) => {
7392
expect(
74-
getBrowserAgentHostname([
93+
getBrowserAgentFaviconUrl([
7594
tool(),
7695
tool({ toolName, status: 'executing', result: undefined }),
7796
])
@@ -89,13 +108,13 @@ describe('getBrowserAgentHostname', () => {
89108
{ tabs, activeTabId: 'agent' },
90109
]) {
91110
expect(
92-
getBrowserAgentHostname([
111+
getBrowserAgentFaviconUrl([
93112
tool({ toolName: 'browser_list_tabs', result: { success: true, output } }),
94113
])
95-
).toBe('example.com')
114+
).toBe('https://example.com/favicon.ico')
96115
}
97116
expect(
98-
getBrowserAgentHostname([
117+
getBrowserAgentFaviconUrl([
99118
tool(),
100119
tool({
101120
toolName: 'browser_list_tabs',
@@ -118,14 +137,14 @@ describe('getBrowserAgentHostname', () => {
118137
['browser_extract', { page: { url: 'https://example.org' } }],
119138
] as const) {
120139
expect(
121-
getBrowserAgentHostname([tool(), tool({ toolName, result: { success: true, output } })])
122-
).toBe('example.org')
140+
getBrowserAgentFaviconUrl([tool(), tool({ toolName, result: { success: true, output } })])
141+
).toBe('https://example.org/favicon.ico')
123142
}
124143
})
125144

126145
it('clears a stale site when an action navigated without reporting its destination', () => {
127146
expect(
128-
getBrowserAgentHostname([
147+
getBrowserAgentFaviconUrl([
129148
tool(),
130149
tool({
131150
toolName: 'browser_click',
@@ -137,7 +156,7 @@ describe('getBrowserAgentHostname', () => {
137156

138157
it('ignores URLs from other tools and nested agent runs', () => {
139158
expect(
140-
getBrowserAgentHostname([
159+
getBrowserAgentFaviconUrl([
141160
tool(),
142161
tool({
143162
toolName: 'browser_read_text',
@@ -160,7 +179,7 @@ describe('getBrowserAgentHostname', () => {
160179
},
161180
},
162181
])
163-
).toBe('example.com')
182+
).toBe('https://example.com/favicon.ico')
164183
})
165184
})
166185

@@ -184,10 +203,11 @@ describe('BrowserAgentIcon', () => {
184203
)
185204
}
186205

187-
it('keeps the globe until load and resets image state when the hostname changes', () => {
188-
render('https://example.com/document?token=private#section')
206+
it('keeps the globe until load and resets image state when the page origin changes', () => {
207+
render('https://username:password@example.com/document?token=private#section')
189208
const firstImage = container.querySelector('img')!
190-
expect(firstImage.src).toBe('https://www.google.com/s2/favicons?domain=example.com&sz=32')
209+
expect(firstImage.src).toBe('https://example.com/favicon.ico')
210+
expect(firstImage.getAttribute('referrerpolicy')).toBe('no-referrer')
191211
expect(container.querySelector('svg')).not.toBeNull()
192212
act(() => firstImage.dispatchEvent(new Event('load')))
193213
expect(container.querySelector('svg')).toBeNull()

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

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,22 @@ 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 { browserTabHostname } from '@/lib/browser-agent/tab-label'
9-
import { faviconUrl } from '@/lib/core/utils/favicon'
108
import type { AgentGroupItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view'
119
import { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types'
1210

11+
function pageFaviconUrl(url: string): string | null {
12+
try {
13+
const page = new URL(url)
14+
/** External images must use HTTPS under the app's content security policy. */
15+
if (page.protocol !== 'https:') return null
16+
return `${page.origin}/favicon.ico`
17+
} catch {
18+
return null
19+
}
20+
}
21+
1322
/** Uses this run's page observations so historical icons never follow another run's live tab. */
14-
export function getBrowserAgentHostname(items: AgentGroupItem[]): string | null {
23+
export function getBrowserAgentFaviconUrl(items: AgentGroupItem[]): string | null {
1524
for (let index = items.length - 1; index >= 0; index--) {
1625
const item = items[index]
1726
if (item.type !== 'tool' || !isBrowserToolName(item.data.toolName)) continue
@@ -25,33 +34,33 @@ export function getBrowserAgentHostname(items: AgentGroupItem[]): string | null
2534
toolName === 'browser_open_url' ||
2635
toolName === 'browser_open_tab'
2736
if (status === ToolCallStatus.executing && navigatesToUrl) {
28-
return typeof params?.url === 'string' ? browserTabHostname(params.url) : null
37+
return typeof params?.url === 'string' ? pageFaviconUrl(params.url) : null
2938
}
3039

3140
const output = result?.success && isRecordLike(result.output) ? result.output : null
3241
if (output) {
3342
if (isRecordLike(output.activeTab) && typeof output.activeTab.url === 'string') {
34-
return browserTabHostname(output.activeTab.url)
43+
return pageFaviconUrl(output.activeTab.url)
3544
}
36-
if (typeof output.url === 'string') return browserTabHostname(output.url)
45+
if (typeof output.url === 'string') return pageFaviconUrl(output.url)
3746
if (
3847
toolName === 'browser_extract' &&
3948
isRecordLike(output.page) &&
4049
typeof output.page.url === 'string'
4150
) {
42-
return browserTabHostname(output.page.url)
51+
return pageFaviconUrl(output.page.url)
4352
}
4453
if (
4554
toolName === 'browser_screenshot' &&
4655
isRecordLike(output.viewport) &&
4756
typeof output.viewport.url === 'string'
4857
) {
49-
return browserTabHostname(output.viewport.url)
58+
return pageFaviconUrl(output.viewport.url)
5059
}
5160
if (toolName === 'browser_list_tabs' && Array.isArray(output.tabs)) {
5261
const tabId = 'automationTabId' in output ? output.automationTabId : output.activeTabId
5362
const tab = output.tabs.find((tab) => isRecordLike(tab) && tab.tabId === tabId)
54-
return isRecordLike(tab) && typeof tab.url === 'string' ? browserTabHostname(tab.url) : null
63+
return isRecordLike(tab) && typeof tab.url === 'string' ? pageFaviconUrl(tab.url) : null
5564
}
5665
/** A navigation without a destination invalidates the previous page observation. */
5766
if (
@@ -81,22 +90,23 @@ interface BrowserAgentIconProps {
8190
}
8291

8392
export function BrowserAgentIcon({ items }: BrowserAgentIconProps) {
84-
const hostname = getBrowserAgentHostname(items)
85-
return <BrowserAgentFavicon key={hostname} hostname={hostname} />
93+
const url = getBrowserAgentFaviconUrl(items)
94+
return <BrowserAgentFavicon key={url} url={url} />
8695
}
8796

8897
interface BrowserAgentFaviconProps {
89-
hostname: string | null
98+
url: string | null
9099
}
91100

92-
function BrowserAgentFavicon({ hostname }: BrowserAgentFaviconProps) {
101+
function BrowserAgentFavicon({ url }: BrowserAgentFaviconProps) {
93102
const [status, setStatus] = useState<'loading' | 'loaded' | 'failed'>('loading')
94103

95104
return (
96105
<span className='relative flex size-[16px] items-center justify-center' aria-hidden='true'>
97-
{hostname && status !== 'failed' && (
106+
{url && status !== 'failed' && (
98107
<img
99-
src={faviconUrl(hostname, 32)}
108+
src={url}
109+
referrerPolicy='no-referrer'
100110
alt=''
101111
className={cn(
102112
'size-[16px] rounded-[3px]',
@@ -106,9 +116,7 @@ function BrowserAgentFavicon({ hostname }: BrowserAgentFaviconProps) {
106116
onError={() => setStatus('failed')}
107117
/>
108118
)}
109-
{(!hostname || status !== 'loaded') && (
110-
<Globe className='size-[16px] text-[var(--text-icon)]' />
111-
)}
119+
{(!url || status !== 'loaded') && <Globe className='size-[16px] text-[var(--text-icon)]' />}
112120
</span>
113121
)
114122
}

0 commit comments

Comments
 (0)