Skip to content

Commit 6e06ce3

Browse files
committed
refactor(favicon): one site icon for the Platform and the legacy app
Favicon moves to components/favicon/favicon.tsx as a Platform host module (platformHostModules), drops the legacy EMCN cn import that kept it out of the Platform's seam closure, and owns its glyph-sized appearance in its own CSS module; legacy Tailwind classes still win from the later layer. - Links and table cells render Favicon with the globe fallback; LinkIcon, CellFavicon (a second miss LRU) and their .favicon CSS are deleted. links.ts exposes linkHost, the tables barrel siteHost, not a built src. - The browser agent icon and browser tab icon render Favicon instead of their own img + per-instance failure state, so they share the miss cache. - siteHost (lib/favicon/url.ts) replaces externalLinkHostname, browserTabHostname and the inline host parsing in links, cells and the browser agent icon. - check:source-conventions gains favicon-url: outside the favicon owners, no /api/favicon or third-party icon service string, and no import of faviconSrc or the favicon contract.
1 parent 8a1b530 commit 6e06ce3

24 files changed

Lines changed: 269 additions & 193 deletions

File tree

‎apps/sim/app/(product)/_components/links-page/link-icon.tsx‎

Lines changed: 0 additions & 21 deletions
This file was deleted.

‎apps/sim/app/(product)/_components/links-page/links-page.module.css‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,6 @@
2222
flex: 1 1 var(--spacing-128);
2323
min-inline-size: 0;
2424
}
25-
.favicon {
26-
flex: none;
27-
inline-size: var(--spacing-16);
28-
block-size: var(--spacing-16);
29-
border-radius: var(--radius-2);
30-
}
3125
.nameCell {
3226
display: grid;
3327
flex: 1;

‎apps/sim/app/(product)/_components/links-page/links-page.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ import { OverflowText } from '@sim/emcn/overflow-text'
1313
import type { LoadMoreState } from '@sim/emcn/scroll-area/use-load-more'
1414
import { SearchField } from '@sim/emcn/search-field'
1515
import { FailureNotice } from '@sim/emcn/status'
16+
import { Favicon } from '@/components/favicon/favicon'
1617
import { LinkEditor } from '@/app/(product)/_components/links-page/link-editor'
17-
import { LinkIcon } from '@/app/(product)/_components/links-page/link-icon'
1818
import styles from '@/app/(product)/_components/links-page/links-page.module.css'
1919
import { SectionToolbar } from '@/app/(product)/_components/settings-parts/settings-parts'
2020
import { useMinuteClock } from '@/platform/lib/data/clock'
2121
import {
2222
type LinkRecord,
23-
linkFaviconSrc,
23+
linkHost,
2424
openChromeStore,
2525
openLink,
2626
useLinksHosted,
@@ -158,7 +158,7 @@ const LinksTable = memo(function LinksTable({
158158
},
159159
cells: {
160160
name: (
161-
<TableLabel leading={<LinkIcon src={linkFaviconSrc(link)} />}>
161+
<TableLabel leading={<Favicon host={linkHost(link)} fallback={<Icon name='I041' />} />}>
162162
<span className={styles.nameCell}>
163163
<OverflowText text={link.name}>{link.name}</OverflowText>
164164
<LinkSummary link={link} updated={updated} />

‎apps/sim/app/(product)/_components/workspace/tables/table-grid/cell-render.tsx‎

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
'use client'
22

3-
import { type ReactElement, useEffect, useReducer, useRef, useState } from 'react'
3+
import { type ReactElement, useEffect, useRef, useState } from 'react'
44
import { Checkbox } from '@sim/emcn/checkbox'
55
import { Icon } from '@sim/emcn/icon'
66
import { StatusTag, StatusText } from '@sim/emcn/status'
77
import { Tooltip } from '@sim/emcn/tooltip'
8-
import { LRUCache } from 'lru-cache'
98
import Link from 'next/link'
109
import { parse } from 'tldts'
10+
import { Favicon } from '@/components/favicon/favicon'
1111
import { workspacePath } from '@/lib/navigation/paths'
1212
import { resolveSelectOptions } from '@/app/(product)/_components/workspace/tables/select-field/select-field'
1313
import { InlineEditor } from '@/app/(product)/_components/workspace/tables/table-grid/inline-editors'
@@ -16,10 +16,10 @@ import { useKnowledgeBasesQuery } from '@/platform/lib/data/knowledge'
1616
import {
1717
columnTypeOf,
1818
type DisplayColumn,
19-
faviconSrc,
2019
type RowExecutionMetadata,
2120
type SaveReason,
2221
type SelectOption,
22+
siteHost,
2323
storageToDisplay,
2424
type TimezoneState,
2525
useTablesList,
@@ -187,12 +187,8 @@ function extractUrlInfo(text: string): { href: string; domain: string } | null {
187187
const trimmed = text.trim()
188188
if (!trimmed) return null
189189
if (/^https?:\/\//i.test(trimmed)) {
190-
try {
191-
const url = new URL(trimmed)
192-
return { href: trimmed, domain: url.hostname }
193-
} catch {
194-
return null
195-
}
190+
const domain = siteHost(trimmed)
191+
return domain ? { href: trimmed, domain } : null
196192
}
197193
if (BARE_DOMAIN_RE.test(trimmed)) {
198194
const parsed = parse(trimmed)
@@ -430,7 +426,7 @@ function CellRender({ kind, isEditing }: CellRenderProps): ReactElement | null {
430426
case 'url':
431427
return (
432428
<span className={styles.linkCell} data-hidden={hidden}>
433-
<CellFavicon host={kind.domain} />
429+
<Favicon host={kind.domain} fallback={<Icon name='I041' />} />
434430
<a
435431
href={kind.href}
436432
target='_blank'
@@ -494,30 +490,6 @@ function StatusCell({ label, intent, tip, hidden }: StatusCellProps) {
494490
return tip ? <Tooltip label={tip}>{tag}</Tooltip> : tag
495491
}
496492

497-
/**
498-
* Hosts whose icon failed recently, so a remounted cell renders the globe at once instead of an
499-
* empty slot. An entry lasts only as long as the favicon route retries an outage.
500-
*/
501-
const knownFaviconMisses = new LRUCache<string, true>({ max: 500, ttl: 5 * 60 * 1000 })
502-
503-
function CellFavicon({ host }: { host: string }) {
504-
const [, rerender] = useReducer((failures: number) => failures + 1, 0)
505-
if (knownFaviconMisses.has(host)) return <Icon name='I041' />
506-
return (
507-
<img
508-
src={faviconSrc(host)}
509-
alt=''
510-
loading='lazy'
511-
decoding='async'
512-
className={styles.favicon}
513-
onError={() => {
514-
knownFaviconMisses.set(host, true)
515-
rerender()
516-
}}
517-
/>
518-
)
519-
}
520-
521493
const FALLBACK_LABEL: Record<SimResourceType, string> = {
522494
workflow: 'Workflow',
523495
table: 'Table',

‎apps/sim/app/(product)/_components/workspace/tables/table-grid/table-grid.module.css‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,12 +229,6 @@
229229
.link:hover {
230230
color: var(--ui-secondary);
231231
}
232-
.favicon {
233-
flex: none;
234-
inline-size: var(--spacing-12);
235-
block-size: var(--spacing-12);
236-
border-radius: var(--radius-2);
237-
}
238232
.statusCell {
239233
display: inline-flex;
240234
min-inline-size: 0;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* A site icon is a glyph: it takes the glyph size and leading slot of the icon it stands in for. */
2+
@layer components {
3+
.icon {
4+
display: inline-block;
5+
flex: none;
6+
inline-size: var(--ui-glyph-size);
7+
block-size: var(--ui-glyph-size);
8+
border-radius: var(--radius-2);
9+
}
10+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
6+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
7+
import { Favicon } from '@/components/favicon/favicon'
8+
9+
const fallback = <svg data-fallback='' />
10+
11+
describe('Favicon', () => {
12+
let container: HTMLDivElement
13+
let root: Root
14+
15+
beforeEach(() => {
16+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
17+
container = document.createElement('div')
18+
root = createRoot(container)
19+
})
20+
21+
afterEach(() => act(() => root.unmount()))
22+
23+
it('renders the fallback at once for every later icon of a host whose icon failed', () => {
24+
act(() => root.render(<Favicon host='no-icon.example' fallback={fallback} />))
25+
const failed = container.querySelector('img')
26+
expect(failed).not.toBeNull()
27+
act(() => failed?.dispatchEvent(new Event('error')))
28+
expect(container.querySelector('img')).toBeNull()
29+
30+
act(() =>
31+
root.render(
32+
<>
33+
<Favicon key='other-surface' host='no-icon.example' fallback={fallback} />
34+
<Favicon host='has-icon.example' fallback={fallback} />
35+
</>
36+
)
37+
)
38+
expect(container.querySelectorAll('[data-fallback]')).toHaveLength(1)
39+
expect(
40+
[...container.querySelectorAll('img')].map((image) => image.getAttribute('src'))
41+
).toEqual(['/api/favicon?host=has-icon.example'])
42+
})
43+
})
Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
'use client'
22

33
import { type ReactNode, useReducer } from 'react'
4-
import { cn } from '@sim/emcn-legacy'
54
import { LRUCache } from 'lru-cache'
5+
import styles from '@/components/favicon/favicon.module.css'
66
import { faviconSrc } from '@/lib/favicon/url'
77

88
/**
@@ -14,20 +14,23 @@ import { faviconSrc } from '@/lib/favicon/url'
1414
const knownMisses = new LRUCache<string, true>({ max: 500, ttl: 5 * 60 * 1000 })
1515

1616
interface FaviconProps {
17-
/** The site's hostname; without one the fallback renders. */
17+
/** The site's hostname (`siteHost` from `@/lib/favicon/url`); without one the fallback renders. */
1818
host: string | null | undefined
1919
/** Rendered when there is no host, or when neither the host nor a parent domain has an icon. */
2020
fallback: ReactNode
21-
/** Sizing for the icon; the fallback carries its own. */
21+
/**
22+
* Utility classes from a legacy (Tailwind) caller; they win over the glyph size, being a later
23+
* layer. Platform callers pass none and the icon takes the glyph size of the slot it fills.
24+
*/
2225
className?: string
2326
/** Reports the image painted, including one the browser had ready before hydration. */
2427
onLoad?: () => void
2528
}
2629

2730
/**
2831
* A site's icon, served by Sim's favicon route: the browser never contacts a third party, a
29-
* private tenant shows its platform's icon, and a site with none renders `fallback` instead
30-
* of a provider's placeholder.
32+
* private tenant shows its platform's icon, and a site with none renders `fallback` instead of a
33+
* provider's placeholder. The Platform and the legacy app both render site icons only through it.
3134
*/
3235
export function Favicon({ host, fallback, className, onLoad }: FaviconProps) {
3336
const [, rerender] = useReducer((failures: number) => failures + 1, 0)
@@ -41,7 +44,7 @@ export function Favicon({ host, fallback, className, onLoad }: FaviconProps) {
4144
alt=''
4245
loading='lazy'
4346
decoding='async'
44-
className={cn('rounded-sm', className)}
47+
className={className ? `${styles.icon} ${className}` : styles.icon}
4548
onLoad={onLoad}
4649
onError={() => {
4750
knownMisses.set(host, true)

‎apps/sim/components/mothership-chat/message-content/components/agent-group/browser-agent-icon.test.tsx‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
77
import type { AgentGroupItem } from '@/components/mothership-chat/message-content/components/agent-group/agent-group-view'
88
import {
99
BrowserAgentIcon,
10-
getBrowserAgentFaviconUrl,
10+
getBrowserAgentPageHost,
1111
} from '@/components/mothership-chat/message-content/components/agent-group/browser-agent-icon'
1212
import type { ToolCallData } from '@/lib/mothership/chat/types'
1313
import { useBrowserSessionStore } from '@/stores/browser-session/store'
@@ -46,10 +46,10 @@ function tool(overrides: Partial<ToolCallData> = {}): AgentGroupItem {
4646
}
4747
}
4848

49-
describe('getBrowserAgentFaviconUrl', () => {
49+
describe('getBrowserAgentPageHost', () => {
5050
it('keeps only the observed hostname, never credentials, port, path, or query', () => {
5151
expect(
52-
getBrowserAgentFaviconUrl([
52+
getBrowserAgentPageHost([
5353
tool({
5454
result: {
5555
success: true,
@@ -59,12 +59,12 @@ describe('getBrowserAgentFaviconUrl', () => {
5959
},
6060
}),
6161
])
62-
).toBe('/api/favicon?host=example.com')
62+
).toBe('example.com')
6363
})
6464

6565
it('ignores URLs from other tools and nested agent runs', () => {
6666
expect(
67-
getBrowserAgentFaviconUrl([
67+
getBrowserAgentPageHost([
6868
tool(),
6969
tool({
7070
toolName: 'browser_read_text',
@@ -87,7 +87,7 @@ describe('getBrowserAgentFaviconUrl', () => {
8787
},
8888
},
8989
])
90-
).toBe('/api/favicon?host=example.com')
90+
).toBe('example.com')
9191
})
9292
})
9393

0 commit comments

Comments
 (0)