diff --git a/apps/web/src/components/providers/ProviderTable/ProviderTable.module.scss b/apps/web/src/components/providers/ProviderTable/ProviderTable.module.scss index ce35296e4..43e15d1f6 100644 --- a/apps/web/src/components/providers/ProviderTable/ProviderTable.module.scss +++ b/apps/web/src/components/providers/ProviderTable/ProviderTable.module.scss @@ -173,9 +173,12 @@ $table-columns: 92px minmax(132px, 0.72fr) minmax(180px, 1fr) 96px 72px minmax(2 } .cellUrl { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; font-size: 12px; color: var(--text-secondary); - @include text-ellipsis; @include provider-card-layout { grid-area: url; @@ -184,6 +187,31 @@ $table-columns: 92px minmax(132px, 0.72fr) minmax(180px, 1fr) 96px 72px minmax(2 } } +.urlText { + min-width: 0; + @include text-ellipsis; + + @include provider-card-layout { + white-space: normal; + overflow-wrap: anywhere; + } +} + +.urlLink { + flex-shrink: 0; + display: inline-flex; + align-items: center; + padding: 2px; + color: var(--text-secondary); + border-radius: $radius-sm; + transition: color $transition-fast, background $transition-fast; + + &:hover { + color: var(--text-primary); + background: var(--surface-subtle); + } +} + .cellNumeric { text-align: right; diff --git a/apps/web/src/components/providers/ProviderTable/ProviderTable.test.tsx b/apps/web/src/components/providers/ProviderTable/ProviderTable.test.tsx index 2b1488bcd..5ac71deb0 100644 --- a/apps/web/src/components/providers/ProviderTable/ProviderTable.test.tsx +++ b/apps/web/src/components/providers/ProviderTable/ProviderTable.test.tsx @@ -378,4 +378,35 @@ describe('ProviderTable', () => { expect(getText(renderedRows[1])).toContain('status_bar.no_requests'); expect(renderedRows[1].findAllByType(ProviderStatusBar)).toHaveLength(0); }); + + it('renders an external homepage link derived from the base url', () => { + const rows = buildProviderRows({ + ...emptyInput, + codex: [{ apiKey: 'k1', baseUrl: 'https://low.example.com/v1' }], + }); + const renderer = renderTable(rows); + + const link = getRows(renderer)[0] + .findAll((node) => node.type === 'a') + .find((node) => node.props['aria-label'] === 'ai_providers.open_homepage'); + expect(link).toBeTruthy(); + expect(link?.props.href).toBe('https://low.example.com'); + expect(link?.props.target).toBe('_blank'); + expect(link?.props.rel).toBe('noopener noreferrer'); + }); + + it('skips the homepage link when the base url is missing or malformed', () => { + const rows = buildProviderRows({ + ...emptyInput, + codex: [ + { apiKey: 'k2', baseUrl: '' }, + { apiKey: 'k3', baseUrl: 'https://' }, + ], + }); + const renderer = renderTable(rows); + + for (const row of getRows(renderer)) { + expect(row.findAll((node) => node.type === 'a')).toHaveLength(0); + } + }); }); diff --git a/apps/web/src/components/providers/ProviderTable/ProviderTable.tsx b/apps/web/src/components/providers/ProviderTable/ProviderTable.tsx index e63869e5e..5adfff701 100644 --- a/apps/web/src/components/providers/ProviderTable/ProviderTable.tsx +++ b/apps/web/src/components/providers/ProviderTable/ProviderTable.tsx @@ -12,6 +12,7 @@ import { Button } from '@/components/ui/Button'; import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; import { IconCheck, + IconExternalLink, IconEye, IconPencil, IconTrash2, @@ -19,6 +20,7 @@ import { } from '@/components/ui/icons'; import { ProviderStatusBar } from '../ProviderStatusBar'; import { getProviderKindIcon, PROVIDER_KIND_LABELS } from './kindMeta'; +import { resolveProviderHomepage } from './homepage'; import type { ProviderRow } from './rowData'; import styles from './ProviderTable.module.scss'; @@ -211,6 +213,7 @@ export function ProviderTable({ {rows.map((row) => { const kindLabel = PROVIDER_KIND_LABELS[row.kind]; + const homepageUrl = resolveProviderHomepage(row.baseUrl); return (