feat(frontend): add missing resource type icons and show them in the resource picker (#10407)

* feat(frontend): add missing resource type icons and show them in the picker

Anthropic, DeepSeek, OpenRouter, Google AI (Gemini) and Ultravox resource
types rendered as a blank grey circle. Add their marks and wire up two icons
that already existed in the repo but were never imported.

Unmapped resource types now fall back to the generic resource glyph instead
of the grey circle, and the resource picker shows the icon before the path —
both in the dropdown rows and inside the input — omitting it entirely, with
no reserved padding, when the type has no icon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): correct gemini gradient units and reuse the icon resolver

The Gemini gradient omitted gradientUnits, so its 0..24 coordinates were read
as objectBoundingBox fractions and the whole mark collapsed onto a 4% slice of
the ramp — a flat purple. Pin it to userSpaceOnUse like every other gradient
icon in the directory.

Index the resource list by path once instead of scanning it per dropdown row,
fold the two remaining inline copies of the icon lookup into appIconComponent,
and drop the sendflake key — no resource type by that name exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style(frontend): shrink the resource picker icon to 14px

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style(frontend): drop the redundant frame around the ai providers list

SettingCard already supplies p-4, rounded-md and bg-surface-tertiary, so the
wrapper repeated all three and added a border, nesting a card inside a card.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): keep the unregistered snowflake mark, extend the card fix

SendflakeIcon is the Snowflake logo under a misleading filename, not a
different product; SnowflakeIcon is the same logo with a baked-in ® that
shrinks and off-centres the flake and turns to a smudge at 14-24px. Leave the
mapping where it was.

Apply the card-in-card removal to the two InstanceFallbackSettings cards that
stack directly above the AI providers one, so the frames stay consistent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(frontend): make the ansible mark legible on dark surfaces

Mapping `ansible` renders this icon for the first time, and its near-black disc
disappears into the dark surface, leaving only the white "A" counter floating.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-07-30 23:40:03 +02:00
committed by GitHub
parent a372ae0c04
commit 705c90debd
14 changed files with 202 additions and 33 deletions
@@ -1,6 +1,6 @@
<script>
import { FileText, FolderOpen } from 'lucide-svelte'
import { APP_TO_ICON_COMPONENT } from './icons'
import { Boxes, FileText, FolderOpen } from 'lucide-svelte'
import { appIconComponent } from './icons'
/**
* @typedef {Object} Props
* @property {any} name
@@ -27,13 +27,7 @@
isFileset = false
} = $props()
let iconComponent = $derived(
name
? name === 'teams'
? APP_TO_ICON_COMPONENT.ms_teams_webhook
: APP_TO_ICON_COMPONENT[name] || APP_TO_ICON_COMPONENT[name.split('_')[0]]
: undefined
)
let iconComponent = $derived(appIconComponent(name))
let widthInPixels = $derived(parseInt(width))
</script>
@@ -56,7 +50,9 @@
<FileText {height} {width} />
</span>
{:else}
<span style="width: {width}; height: {height}" class="bg-gray-100 rounded-full"></span>
<span class="text-hint/60">
<Boxes {height} {width} strokeWidth={1.5} />
</span>
{/if}
{#if !silent && after}
{name}
@@ -11,6 +11,7 @@
import Select from './select/Select.svelte'
import ExploreAssetButton, { assetCanBeExplored } from './ExploreAssetButton.svelte'
import DropdownV2 from './DropdownV2.svelte'
import { appIconComponent } from './icons'
interface Props {
initialValue?: string | undefined
@@ -186,6 +187,8 @@
let resourceEditor: ResourceEditorDrawer | undefined = $state()
let hovering = $state(false)
let isDatatableSelected = $derived(value?.startsWith('datatable://') ?? false)
let typeByPath = $derived(new Map(collection.map((x) => [x.value, x.type])))
let SelectedIcon = $derived(appIconComponent(typeByPath.get(value!)))
</script>
<AppConnect
@@ -212,6 +215,12 @@
}
}}
/>
{#snippet selectedIconSnippet()}
{#if SelectedIcon}
<SelectedIcon height="14px" width="14px" size={14} />
{/if}
{/snippet}
<!-- {JSON.stringify({ value, collection })} -->
<div class="flex flex-col w-full items-start {className}">
<div
@@ -244,7 +253,16 @@
placeholder={placeholder ?? `${resourceType ?? 'any'} resource`}
itemLabelWrapperClasses="flex-1"
id="resource-picker-select"
inputLeadingSnippet={SelectedIcon ? selectedIconSnippet : undefined}
>
{#snippet startSnippet({ item })}
{@const Icon = appIconComponent(typeByPath.get(item.value))}
{#if Icon}
<span class="shrink-0 text-secondary">
<Icon height="14px" width="14px" size={14} />
</span>
{/if}
{/snippet}
{#snippet endSnippet({ item, close })}
{#if !item.value?.startsWith('datatable://')}
<Button
@@ -1,7 +1,7 @@
<script lang="ts">
import { Folder, User } from 'lucide-svelte'
import { Badge } from '../common'
import { APP_TO_ICON_COMPONENT } from '../icons'
import { appIconComponent } from '../icons'
import { onDestroy, onMount } from 'svelte'
interface Props {
@@ -53,10 +53,6 @@
selectedFilter = queryValue
}
function getIconComponent(name: string) {
return APP_TO_ICON_COMPONENT[name] || APP_TO_ICON_COMPONENT[name.split('_')[0]]
}
export async function setQuery(url: URL, key: string, value: string | undefined): Promise<void> {
if (value != undefined) {
url.searchParams.set(key, value)
@@ -110,7 +106,7 @@
>
<span style="height: 12px" class="-mt-0.5">
{#if resourceType}
{@const SvelteComponent = getIconComponent(filter)}
{@const SvelteComponent = appIconComponent(filter)}
<SvelteComponent height="14px" width="14px" />
{:else if filter.startsWith('u/')}
<User class="mr-0.5" size={14} />
@@ -1,23 +1,20 @@
<script lang="ts">
import { Folder, User, Circle } from 'lucide-svelte'
import { APP_TO_ICON_COMPONENT } from '../icons'
import { appIconComponent } from '../icons'
import { createEventDispatcher } from 'svelte'
import { Button } from '../common'
interface Props {
filters: string[];
selectedFilter?:
| { kind: 'owner' | 'integrations'; name: string | undefined }
| undefined;
resourceType?: boolean;
filters: string[]
selectedFilter?: { kind: 'owner' | 'integrations'; name: string | undefined } | undefined
resourceType?: boolean
}
let { filters, selectedFilter = $bindable(undefined), resourceType = false }: Props = $props();
let { filters, selectedFilter = $bindable(undefined), resourceType = false }: Props = $props()
function getIconComponent(name: string, resourceType: boolean) {
if (resourceType) {
const icon = APP_TO_ICON_COMPONENT[name] || APP_TO_ICON_COMPONENT[name.split('_')[0]]
const icon = appIconComponent(name)
if (icon) {
return { icon: icon, props: { width: 13, height: 13 } }
} else {
@@ -32,7 +29,9 @@
}
const dispatch = createEventDispatcher()
let selectedAppFilter = $derived(selectedFilter?.kind === 'integrations' ? selectedFilter?.name : undefined)
let selectedAppFilter = $derived(
selectedFilter?.kind === 'integrations' ? selectedFilter?.name : undefined
)
</script>
{#if Array.isArray(filters) && filters.length > 0}
@@ -1,18 +1,21 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- The mark is a near-black disc knocked out with a white "A", so it needs inverting
rather than currentColor: recolouring the disc alone would leave white on light grey. -->
<svg
{height}
preserveAspectRatio="xMidYMid"
viewBox="0 0 256 256"
{width}
xmlns="http://www.w3.org/2000/svg"
class="dark:invert"
>
<g fill="#1a1918" transform="translate(0 281.86)">
<path
@@ -0,0 +1,14 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z"
/>
</svg>
@@ -0,0 +1,14 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="#4D6BFE" xmlns="http://www.w3.org/2000/svg">
<path
d="M23.748 4.651c-.254-.124-.364.113-.512.233-.051.04-.094.09-.137.137-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.155-.708-.311-.955-.65-.172-.24-.219-.509-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.094.172.187.129.323-.082.28-.18.553-.266.833-.055.179-.137.218-.328.14a5.5 5.5 0 0 1-1.737-1.179c-.857-.828-1.631-1.743-2.597-2.46a12 12 0 0 0-.689-.47c-.985-.957.13-1.743.387-1.836.27-.098.094-.433-.778-.428-.872.003-1.67.295-2.687.685a3 3 0 0 1-.465.136 9.6 9.6 0 0 0-2.883-.101c-1.885.21-3.39 1.1-4.497 2.622C.082 8.776-.231 10.854.152 13.02c.403 2.284 1.568 4.175 3.36 5.653 1.857 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.132-.284 4.994-1.86.47.234.962.328 1.78.398.629.058 1.235-.031 1.705-.129.735-.155.684-.836.418-.961-2.155-1.004-1.682-.595-2.112-.926 1.095-1.295 2.768-3.598 3.284-6.733.05-.346.115-.834.108-1.114-.004-.171.035-.238.23-.257a4.2 4.2 0 0 0 1.545-.475c1.397-.763 1.96-2.016 2.093-3.517.02-.23-.004-.467-.247-.588M11.58 18.168c-2.088-1.642-3.101-2.183-3.52-2.16-.39.024-.32.472-.234.763.09.288.207.487.371.74.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.168-1.361-.801-2.5-1.86-3.301-3.306-.775-1.393-1.225-2.888-1.299-4.482-.02-.385.094-.522.477-.592a4.7 4.7 0 0 1 1.53-.038c2.131.311 3.946 1.264 5.467 2.774.868.86 1.525 1.887 2.202 2.89.72 1.066 1.494 2.082 2.48 2.915.348.291.626.513.892.677-.802.09-2.14.109-3.055-.615zm1.001-6.44a.306.306 0 0 1 .415-.287.3.3 0 0 1 .113.074.3.3 0 0 1 .086.214c0 .17-.136.307-.308.307a.303.303 0 0 1-.306-.307m3.11 1.596c-.2.081-.4.151-.591.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.551-.758a1.7 1.7 0 0 1 .015-.588c.07-.327-.007-.537-.238-.727-.188-.156-.426-.199-.689-.199a.6.6 0 0 1-.254-.078.253.253 0 0 1-.114-.358 1 1 0 0 1 .192-.21c.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.392.451.462.576.685.915.176.264.336.536.446.848.066.194-.02.353-.25.45"
/>
</svg>
@@ -0,0 +1,29 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient
id="googleai-gemini-gradient"
x1="0"
y1="24"
x2="24"
y2="0"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#4285F4" />
<stop offset="0.52" stop-color="#9B72CB" />
<stop offset="1" stop-color="#D96570" />
</linearGradient>
</defs>
<path
d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"
fill="url(#googleai-gemini-gradient)"
/>
</svg>
@@ -0,0 +1,14 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M16.778 1.844v1.919q-.569-.026-1.138-.032-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237-2.911 2.066-2.731 1.95-4.14 2.75-.396.223-1.342.574-2.185.798-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799 1.41.798 1.228.683 4.14 2.75 2.126 1.509 4.22 2.11 6.148 2.236.88.058 1.716.041 2.555.005v1.918l7.222-4.168-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021-1.364-.09-2.417-.357-3.979-1.465-2.244-1.593-2.866-2.027-3.68-2.508.889-.518 1.449-.906 3.822-2.59 1.56-1.109 2.614-1.377 3.978-1.466.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z"
/>
</svg>
@@ -0,0 +1,48 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 340 340" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient
id="ultravox-gradient-a"
x1="170.76"
x2="334.06"
y1="98.27"
y2="261.65"
gradientTransform="matrix(1 0 0 -1 0 342)"
gradientUnits="userSpaceOnUse"
>
<stop offset=".19" stop-color="#bb3b57" />
<stop offset=".55" stop-color="#ff4f68" />
<stop offset="1" stop-color="#ffbf61" />
</linearGradient>
<linearGradient
id="ultravox-gradient-b"
x1="203.91"
x2="-34.3"
y1="63.62"
y2="301.83"
gradientTransform="matrix(1 0 0 -1 0 342)"
gradientUnits="userSpaceOnUse"
>
<stop offset=".1" stop-color="#ef3254" />
<stop offset=".25" stop-color="#ff4f68" />
<stop offset="1" stop-color="#ffbf61" />
</linearGradient>
</defs>
<path
d="m245.04 238.33 25-12.96a3.992 3.992 0 0 0 2.09-3.51V65.92c0-1.76-2.09-2.59-3.34-1.42l-96.82 96.82 49.16 53.09"
fill="url(#ultravox-gradient-a)"
/>
<path
d="M245.04 238.33 71.21 64.5c-1.25-1.25-3.34-.33-3.34 1.42v155.94c0 1.51.84 2.84 2.09 3.51l95.57 49.58a9.566 9.566 0 0 0 9.03 0"
fill="url(#ultravox-gradient-b)"
/>
<path d="m172.05 161.32 49.58 53.51-51.67-51.59 2.09-1.92Z" fill="#a22f4b" />
</svg>
@@ -200,6 +200,12 @@ import RaindropIcon from './RaindropIcon.svelte'
import MagentoIcon from './MagentoIcon.svelte'
import DeelIcon from './DeelIcon.svelte'
import GroqIcon from './GroqIcon.svelte'
import AnthropicIcon from './AnthropicIcon.svelte'
import DeepSeekIcon from './DeepSeekIcon.svelte'
import OpenRouterIcon from './OpenRouterIcon.svelte'
import GoogleAiIcon from './GoogleAiIcon.svelte'
import UltravoxIcon from './UltravoxIcon.svelte'
import AnsibleIcon from './AnsibleIcon.svelte'
import TogetherAiIcon from './TogetherAiIcon.svelte'
import RunPodIcon from './RunPodIcon.svelte'
import SigNozIcon from './SigNozIcon.svelte'
@@ -430,6 +436,13 @@ export const APP_TO_ICON_COMPONENT = {
magento: MagentoIcon,
deel: DeelIcon,
groqai: GroqIcon,
groq: GroqIcon,
anthropic: AnthropicIcon,
deepseek: DeepSeekIcon,
openrouter: OpenRouterIcon,
googleai: GoogleAiIcon,
ultravox: UltravoxIcon,
ansible: AnsibleIcon,
togetherai: TogetherAiIcon,
runpod: RunPodIcon,
signoz: SigNozIcon,
@@ -455,6 +468,13 @@ export const APP_TO_ICON_COMPONENT = {
wiz: WizIcon
} as unknown as Record<string, Component> // to generate correct svelte package types
// Resource types are named `<app>` or `<app>_<variant>` (e.g. `airtable_table`), so an
// unmatched name retries on the leading segment before giving up.
export function appIconComponent(name: string | undefined): Component | undefined {
if (!name) return undefined
return APP_TO_ICON_COMPONENT[name] ?? APP_TO_ICON_COMPONENT[name.split('_')[0]]
}
export {
AirtableIcon,
DbIcon,
@@ -501,6 +521,12 @@ export {
ZammadIcon,
NextcloudIcon,
SendflakeIcon,
AnsibleIcon,
AnthropicIcon,
DeepSeekIcon,
OpenRouterIcon,
GoogleAiIcon,
UltravoxIcon,
SurrealdbIcon,
ActivitypubIcon,
AwsEcrIcon,
@@ -57,7 +57,8 @@
useContentEditable = false,
startSnippet,
endSnippet,
bottomSnippet
bottomSnippet,
inputLeadingSnippet
}: {
items?: Item[]
value: Value | undefined
@@ -95,6 +96,9 @@
startSnippet?: Snippet<[{ item: ProcessedItem<Value>; close: () => void }]>
endSnippet?: Snippet<[{ item: ProcessedItem<Value>; close: () => void }]>
bottomSnippet?: Snippet<[{ close: () => void }]>
// Adornment pinned inside the input, left of the text. Pass `undefined` rather than a
// snippet that renders nothing — its presence is what reserves the leading padding.
inputLeadingSnippet?: Snippet
} = $props()
let disabled = $derived(_disabled || (loading && !value))
@@ -177,6 +181,12 @@
</div>
{/if}
{#if inputLeadingSnippet}
<div class="absolute z-10 left-2 h-full flex items-center pointer-events-none text-secondary">
{@render inputLeadingSnippet()}
</div>
{/if}
{#if tooltip}
<div class="absolute z-10 right-2 h-full flex items-center">
<Tooltip>{tooltip}</Tooltip>
@@ -204,6 +214,7 @@
'focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0',
open ? '' : 'cursor-pointer',
loading || (clearable && !disabled && value) || RightIcon ? 'pr-7' : '',
inputLeadingSnippet ? 'pl-7' : '',
'empty:before:content-[attr(data-placeholder)]',
!value ? 'empty:before:text-hint' : 'empty:before:text-primary',
disabled && '!bg-surface-disabled !border-transparent !text-disabled pointer-events-none',
@@ -244,6 +255,7 @@
// Show value as placeholder when opening the dropdown and the search is empty
!value ? 'placeholder-hint' : '!placeholder-primary',
loading || (clearable && !disabled && value) || RightIcon ? 'pr-8' : '',
inputLeadingSnippet ? 'pl-7' : '',
inputClass ?? ''
)}
autocomplete="off"
@@ -387,7 +387,7 @@
{/if}
{#if showWorkspaceOverrideEditor}
<SettingCard label="AI Providers">
<div class="flex flex-col gap-4 p-4 rounded-md border bg-surface-tertiary">
<div class="flex flex-col gap-4">
{#each Object.entries(AI_PROVIDERS) as [provider, details] (provider)}
<div class="flex flex-col">
<div class="flex flex-row gap-2">
@@ -30,7 +30,7 @@
{#if instanceAiSummary}
<SettingCard label="Active instance AI">
<div class="flex flex-col gap-4 p-4 rounded-md border bg-surface-tertiary">
<div class="flex flex-col gap-4">
<p class="text-xs text-secondary">
This workspace is currently using the instance AI defaults shown below.
</p>
@@ -91,7 +91,7 @@
{/if}
<SettingCard label="Workspace override">
<div class="flex flex-col gap-3 p-4 rounded-md border bg-surface-tertiary">
<div class="flex flex-col gap-3">
<p class="text-xs text-secondary">
Create workspace-specific AI settings only if this workspace needs to override the active
instance defaults.