diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx
index 18b3e767a9a..0b8d1f3886e 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx
@@ -13,6 +13,7 @@ import { ChevronDown, cn, Expandable, ExpandableContent, OverflowText } from '@s
import { ShimmerText } from '@/components/ui'
import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport'
import { RETIRED_BROWSER_REQUEST_TAKEOVER_ID } from '@/lib/copilot/tools/retired-tools'
+import { BrowserAgentIcon } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon'
import { renderInlineMarkdown } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/inline-markdown'
import { getVisibleMainAgentItems } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/main-agent-activity'
import type { ToolCallItemProps } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item'
@@ -157,6 +158,12 @@ export function AgentGroupView({
renderBrowserTakeover,
}: AgentGroupViewProps) {
const AgentIcon = getAgentIcon(agentName)
+ const agentIcon =
+ agentName === 'browser' ? (
+
+ ) : (
+
+ )
const isMainAgent = agentName === 'mothership'
// Collapsed status line: the latest tool call, always in its RUNNING
// phrasing — it never flips to the completed rewrite (that lives in the
@@ -261,9 +268,7 @@ export function AgentGroupView({
onClick={toggleExpanded}
className='group/agent flex w-full min-w-0 cursor-pointer items-center gap-2 text-left'
>
-
+ {agentIcon}
{isWorking ? (
{headerText}
) : (
@@ -278,9 +283,7 @@ export function AgentGroupView({
) : (
-
+
{agentIcon}
{isWorking ? (
{headerText}
) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.test.tsx
new file mode 100644
index 00000000000..70b38e2bf03
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.test.tsx
@@ -0,0 +1,298 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import type { AgentGroupItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view'
+import {
+ BrowserAgentIcon,
+ getBrowserAgentFaviconUrl,
+} from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon'
+import type { ToolCallData } from '@/app/workspace/[workspaceId]/home/types'
+import { useBrowserSessionStore } from '@/stores/browser-session/store'
+
+const { browserAvailable, chatIdentity } = vi.hoisted(() => ({
+ browserAvailable: vi.fn(() => true),
+ chatIdentity: { chatId: 'chat-1' },
+}))
+vi.mock('@/lib/browser-agent/transport', () => ({ isBrowserAgentAvailable: browserAvailable }))
+vi.mock('@/app/workspace/[workspaceId]/home/components/chat-surface-context', () => ({
+ useChatSurface: () => chatIdentity,
+}))
+
+function openPage(url: string, scopeId = 'chat-1', loading = false) {
+ act(() =>
+ useBrowserSessionStore.getState().setTabsState({
+ scopeId,
+ activeTabId: 'tab-1',
+ automationTabId: 'tab-1',
+ tabs: [{ tabId: 'tab-1', title: '', url, loading, active: true }],
+ })
+ )
+}
+
+function tool(overrides: Partial
= {}): AgentGroupItem {
+ return {
+ type: 'tool',
+ data: {
+ id: 'browser-tool',
+ toolName: 'browser_navigate',
+ displayTitle: 'Opening page',
+ status: 'success',
+ result: { success: true, output: { url: 'https://example.com/document' } },
+ ...overrides,
+ },
+ }
+}
+
+describe('getBrowserAgentFaviconUrl', () => {
+ it('keeps the observed origin and port without disclosing page details', () => {
+ expect(
+ getBrowserAgentFaviconUrl([
+ tool({
+ result: {
+ success: true,
+ output: {
+ url: 'https://username:password@example.com:8443/document?token=private#part',
+ },
+ },
+ }),
+ ])
+ ).toBe('https://example.com:8443/favicon.ico')
+ })
+
+ it('uses the pending destination, then the observed redirect, and retains it during editing', () => {
+ const navigating = tool({
+ status: 'executing',
+ params: { url: 'https://example.org/start' },
+ result: undefined,
+ })
+ expect(getBrowserAgentFaviconUrl([tool(), navigating])).toBe('https://example.org/favicon.ico')
+ const redirected = tool({ params: { url: 'https://example.org/start' } })
+ expect(getBrowserAgentFaviconUrl([redirected])).toBe('https://example.com/favicon.ico')
+ expect(
+ getBrowserAgentFaviconUrl([
+ redirected,
+ tool({ toolName: 'browser_type', status: 'executing', result: undefined }),
+ ])
+ ).toBe('https://example.com/favicon.ico')
+ })
+
+ it.each(['error', 'cancelled', 'rejected', 'awaiting_approval'] as const)(
+ 'does not adopt a destination from a %s tool',
+ (status) => {
+ expect(
+ getBrowserAgentFaviconUrl([
+ tool(),
+ tool({ status, params: { url: 'https://example.org' }, result: undefined }),
+ ])
+ ).toBe('https://example.com/favicon.ico')
+ }
+ )
+
+ it.each([
+ '',
+ 'about:blank',
+ 'http://example.com',
+ 'file:///document',
+ 'data:text/html,hello',
+ 'not a URL',
+ ])('clears the previous site when the page cannot supply an allowed favicon: %s', (url) => {
+ expect(
+ getBrowserAgentFaviconUrl([
+ tool(),
+ tool({ toolName: 'browser_switch_tab', result: { success: true, output: { url } } }),
+ ])
+ ).toBeNull()
+ })
+
+ it.each(['browser_open_tab', 'browser_switch_tab', 'browser_close_tab', 'browser_go_back'])(
+ 'clears an obsolete site while %s has no known destination',
+ (toolName) => {
+ expect(
+ getBrowserAgentFaviconUrl([
+ tool(),
+ tool({ toolName, status: 'executing', result: undefined }),
+ ])
+ ).toBeNull()
+ }
+ )
+
+ it('uses the agent tab from a tab list, including legacy results', () => {
+ const tabs = [
+ { tabId: 'visible', url: 'https://example.org' },
+ { tabId: 'agent', url: 'https://example.com' },
+ ]
+ for (const output of [
+ { tabs, activeTabId: 'visible', automationTabId: 'agent' },
+ { tabs, activeTabId: 'agent' },
+ ]) {
+ expect(
+ getBrowserAgentFaviconUrl([
+ tool({ toolName: 'browser_list_tabs', result: { success: true, output } }),
+ ])
+ ).toBe('https://example.com/favicon.ico')
+ }
+ expect(
+ getBrowserAgentFaviconUrl([
+ tool(),
+ tool({
+ toolName: 'browser_list_tabs',
+ result: {
+ success: true,
+ output: { tabs, activeTabId: 'visible', automationTabId: null },
+ },
+ }),
+ ])
+ ).toBeNull()
+ })
+
+ it('follows a click into a new tab and screenshot page metadata', () => {
+ for (const [toolName, output] of [
+ [
+ 'browser_click',
+ { activeTab: { url: 'https://example.org' }, effect: { tabChanged: true } },
+ ],
+ ['browser_screenshot', { viewport: { url: 'https://example.org' } }],
+ ['browser_extract', { page: { url: 'https://example.org' } }],
+ ] as const) {
+ expect(
+ getBrowserAgentFaviconUrl([tool(), tool({ toolName, result: { success: true, output } })])
+ ).toBe('https://example.org/favicon.ico')
+ }
+ })
+
+ it('clears a stale site when an action navigated without reporting its destination', () => {
+ expect(
+ getBrowserAgentFaviconUrl([
+ tool(),
+ tool({
+ toolName: 'browser_click',
+ result: { success: true, output: { effect: { urlChanged: true } } },
+ }),
+ ])
+ ).toBeNull()
+ })
+
+ it('ignores URLs from other tools and nested agent runs', () => {
+ expect(
+ getBrowserAgentFaviconUrl([
+ tool(),
+ tool({
+ toolName: 'browser_read_text',
+ params: { elementId: 4 },
+ result: { success: true, output: { url: 'https://example.org/embedded' } },
+ }),
+ tool({
+ toolName: 'web_search',
+ result: { success: true, output: { url: 'https://example.org' } },
+ }),
+ {
+ type: 'agent_group',
+ group: {
+ id: 'other-run',
+ agentName: 'browser',
+ agentLabel: 'Browser',
+ isOpen: true,
+ isDelegating: true,
+ items: [tool({ result: { success: true, output: { url: 'https://example.org' } } })],
+ },
+ },
+ ])
+ ).toBe('https://example.com/favicon.ico')
+ })
+})
+
+describe('BrowserAgentIcon', () => {
+ let container: HTMLDivElement
+ let root: Root
+
+ beforeEach(() => {
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
+ browserAvailable.mockReturnValue(true)
+ chatIdentity.chatId = 'chat-1'
+ useBrowserSessionStore.setState({ sessions: {}, activeScopeId: null })
+ container = document.createElement('div')
+ root = createRoot(container)
+ })
+
+ afterEach(() => act(() => root.unmount()))
+
+ const render = (url: string) => {
+ act(() =>
+ root.render(
+
+ )
+ )
+ }
+
+ it('does not contact sites from history, other chats, or pending navigation', () => {
+ render('https://example.com/document')
+ expect(container.querySelector('img')).toBeNull()
+ openPage('https://example.com', 'another-chat')
+ expect(container.querySelector('img')).toBeNull()
+ openPage('https://example.com', 'chat-1', true)
+ expect(container.querySelector('img')).toBeNull()
+ openPage('https://example.org')
+ expect(container.querySelector('img')).toBeNull()
+ openPage('https://example.com')
+ expect(container.querySelector('img')).not.toBeNull()
+ })
+
+ it('requires the local desktop browser and retains an already loaded favicon after closing it', () => {
+ browserAvailable.mockReturnValue(false)
+ openPage('https://example.com')
+ render('https://example.com')
+ expect(container.querySelector('img')).toBeNull()
+ browserAvailable.mockReturnValue(true)
+ render('https://example.com')
+ const img = container.querySelector('img')!
+ act(() => img.dispatchEvent(new Event('load')))
+ act(() => useBrowserSessionStore.getState().discardScope('chat-1'))
+ expect(container.querySelector('img')).toBe(img)
+ expect(container.querySelector('svg')).toBeNull()
+ })
+
+ it('does not carry loaded state into another chat with the same destination', () => {
+ openPage('https://example.com')
+ render('https://example.com')
+ act(() => container.querySelector('img')!.dispatchEvent(new Event('load')))
+ chatIdentity.chatId = 'chat-2'
+ render('https://example.com')
+ expect(container.querySelector('img')).toBeNull()
+ expect(container.querySelector('svg')).not.toBeNull()
+ })
+
+ it('keeps the globe until load and resets image state when the page origin changes', () => {
+ openPage('https://example.com/document')
+ render('https://username:password@example.com/document?token=private#section')
+ const firstImage = container.querySelector('img')!
+ expect(firstImage.src).toBe('https://example.com/favicon.ico')
+ expect(firstImage.getAttribute('referrerpolicy')).toBe('no-referrer')
+ expect(container.querySelector('svg')).not.toBeNull()
+ act(() => firstImage.dispatchEvent(new Event('load')))
+ expect(container.querySelector('svg')).toBeNull()
+
+ render('https://example.com/another-document')
+ expect(container.querySelector('img')).toBe(firstImage)
+ expect(container.querySelector('svg')).toBeNull()
+
+ openPage('https://example.org/document')
+ render('https://example.org/document')
+ expect(container.querySelector('img')).not.toBe(firstImage)
+ expect(container.querySelector('svg')).not.toBeNull()
+ act(() => firstImage.dispatchEvent(new Event('error')))
+ expect(container.querySelector('img')).not.toBeNull()
+ act(() => container.querySelector('img')!.dispatchEvent(new Event('error')))
+ expect(container.querySelector('img')).toBeNull()
+ expect(container.querySelector('svg')).not.toBeNull()
+
+ openPage('https://example.com')
+ render('https://example.com')
+ expect(container.querySelector('img')).not.toBeNull()
+ render('about:blank')
+ expect(container.querySelector('img')).toBeNull()
+ expect(container.querySelector('svg')).not.toBeNull()
+ })
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx
new file mode 100644
index 00000000000..bc698497999
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx
@@ -0,0 +1,140 @@
+'use client'
+
+import { useState } from 'react'
+import { isBrowserToolName } from '@sim/browser-protocol'
+import { cn } from '@sim/emcn'
+import { Globe } from '@sim/emcn/icons'
+import { isRecordLike } from '@sim/utils/object'
+import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport'
+import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
+import type { AgentGroupItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view'
+import { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types'
+import { useBrowserSessionStore } from '@/stores/browser-session/store'
+
+function pageFaviconUrl(url: string): string | null {
+ try {
+ const page = new URL(url)
+ /** External images must use HTTPS under the app's content security policy. */
+ if (page.protocol !== 'https:') return null
+ return `${page.origin}/favicon.ico`
+ } catch {
+ return null
+ }
+}
+
+/** Uses this run's page observations so historical icons never follow another run's live tab. */
+export function getBrowserAgentFaviconUrl(items: AgentGroupItem[]): string | null {
+ for (let index = items.length - 1; index >= 0; index--) {
+ const item = items[index]
+ if (item.type !== 'tool' || !isBrowserToolName(item.data.toolName)) continue
+ const { toolName, status, params, result } = item.data
+ if (status !== ToolCallStatus.executing && status !== ToolCallStatus.success) continue
+ /** Element-scoped reads can report an iframe URL rather than the browser page. */
+ if (toolName === 'browser_read_text' && params?.elementId !== undefined) continue
+
+ const navigatesToUrl =
+ toolName === 'browser_navigate' ||
+ toolName === 'browser_open_url' ||
+ toolName === 'browser_open_tab'
+ if (status === ToolCallStatus.executing && navigatesToUrl) {
+ return typeof params?.url === 'string' ? pageFaviconUrl(params.url) : null
+ }
+
+ const output = result?.success && isRecordLike(result.output) ? result.output : null
+ if (output) {
+ if (isRecordLike(output.activeTab) && typeof output.activeTab.url === 'string') {
+ return pageFaviconUrl(output.activeTab.url)
+ }
+ if (typeof output.url === 'string') return pageFaviconUrl(output.url)
+ if (
+ toolName === 'browser_extract' &&
+ isRecordLike(output.page) &&
+ typeof output.page.url === 'string'
+ ) {
+ return pageFaviconUrl(output.page.url)
+ }
+ if (
+ toolName === 'browser_screenshot' &&
+ isRecordLike(output.viewport) &&
+ typeof output.viewport.url === 'string'
+ ) {
+ return pageFaviconUrl(output.viewport.url)
+ }
+ if (toolName === 'browser_list_tabs' && Array.isArray(output.tabs)) {
+ const tabId = 'automationTabId' in output ? output.automationTabId : output.activeTabId
+ const tab = output.tabs.find((tab) => isRecordLike(tab) && tab.tabId === tabId)
+ return isRecordLike(tab) && typeof tab.url === 'string' ? pageFaviconUrl(tab.url) : null
+ }
+ /** A navigation without a destination invalidates the previous page observation. */
+ if (
+ isRecordLike(output.effect) &&
+ (output.effect.urlChanged === true ||
+ output.effect.topUrlChanged === true ||
+ output.effect.tabChanged === true)
+ ) {
+ return null
+ }
+ }
+ if (
+ navigatesToUrl ||
+ toolName === 'browser_switch_tab' ||
+ toolName === 'browser_close_tab' ||
+ toolName === 'browser_go_back' ||
+ toolName === 'browser_go_forward'
+ ) {
+ return null
+ }
+ }
+ return null
+}
+
+interface BrowserAgentIconProps {
+ items: AgentGroupItem[]
+}
+
+export function BrowserAgentIcon({ items }: BrowserAgentIconProps) {
+ const { chatId } = useChatSurface()
+ const url = getBrowserAgentFaviconUrl(items)
+ const pageIsOpen = useBrowserSessionStore((state) => {
+ const session = chatId ? state.sessions[chatId] : undefined
+ if (!url || !session?.sessionAlive || session.suspended) return false
+ const tab = session.tabs.find((tab) => tab.tabId === session.automationTabId)
+ return Boolean(tab && !tab.loading && !tab.issue && pageFaviconUrl(tab.url) === url)
+ })
+ return (
+
+ )
+}
+
+interface BrowserAgentFaviconProps {
+ url: string | null
+ canLoad: boolean
+}
+
+function BrowserAgentFavicon({ url, canLoad }: BrowserAgentFaviconProps) {
+ const [status, setStatus] = useState<'loading' | 'loaded' | 'failed'>('loading')
+
+ return (
+
+ {/** History alone must not contact a site; keep an already loaded image after the tab closes. */}
+ {url && status !== 'failed' && (canLoad || status === 'loaded') && (
+
setStatus('loaded')}
+ onError={() => setStatus('failed')}
+ />
+ )}
+ {(!url || status !== 'loaded') && }
+
+ )
+}