Skip to content

feat: add support for custom icons#2497

Open
LitoMore wants to merge 4 commits intofarion1231:mainfrom
LitoMore:add-support-custom-icons
Open

feat: add support for custom icons#2497
LitoMore wants to merge 4 commits intofarion1231:mainfrom
LitoMore:add-support-custom-icons

Conversation

@LitoMore
Copy link
Copy Markdown

@LitoMore LitoMore commented Apr 30, 2026

Summary / 概述

Add support for custom provider icons via image URLs in the icon picker.

Users can now paste an image URL into the existing icon search input. Built-in icon search continues to work as before, while valid HTTPS image URLs are rendered as provider icons with <img>. URL detection is debounced to avoid updating the icon on every keystroke.

Related Issue / 关联 Issue

None

Screenshots / 截图

image image
Before / 修改前 After / 修改后
Icon picker only supported selecting from built-in icons. Icon picker supports searching built-in icons and pasting an image URL in the same input.

Checklist / 检查清单

  • pnpm typecheck passes / 通过 TypeScript 类型检查
  • pnpm format:check passes / 通过代码格式检查
  • cargo clippy passes (if Rust code changed) / 通过 Clippy 检查(如修改了 Rust 代码)
  • Updated i18n files if user-facing text changed / 如修改了用户可见文本,已更新国际化文件

@farion1231
Copy link
Copy Markdown
Owner

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f3759a3ad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/components/IconPicker.tsx
@farion1231
Copy link
Copy Markdown
Owner

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b9bddeb4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/components/IconPicker.tsx Outdated
@farion1231
Copy link
Copy Markdown
Owner

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented May 1, 2026

Claude finished @farion1231's task in 2m 18s —— View job


Code Review

  • Read and analyzed all changed files
  • Reviewed for bugs, security, UX, and code quality

This is a clean, well-structured feature. The architecture is sound and the fix commit correctly addressed the debounce-loss issue flagged by Codex. Here are my findings:


Bug: No error feedback for broken image URLs

src/components/ProviderIcon.tsx:88-101

When a user pastes a URL that doesn't resolve to an image (404, wrong content type, CORS block, etc.), the <img> silently renders a broken-image icon with no user feedback. The user has no way to know their icon failed to load.

// Current — silent failure
<img
  src={iconUrl}
 
  ...
  loading="lazy"
/>

Suggested fix — add onError to fall through to the initials fallback:

const [imgError, setImgError] = useState(false);

if (iconUrl && !imgError) {
  return (
    <img
      src={iconUrl}
     
      onError={() => setImgError(true)}
      ...
    />
  );
}
// falls through to the initials fallback

Fix this →


UX: Brief "no results" flash during debounce window

src/components/IconPicker.tsx:31-35

filteredIcons uses isImageUrl (debounced) to short-circuit to [], but the guard condition in the "no results" message also uses isImageUrl:

const filteredIcons = useMemo(() => {
  if (isImageUrl) return [];     // debounced — false for 250ms after paste
  if (!searchQuery) return iconList;
  return searchIcons(searchQuery); // runs with URL string during those 250ms
}, [isImageUrl, searchQuery]);

// ...
{!isImageUrl && filteredIcons.length === 0 && (
  <div>No matching icons found</div>   // shown briefly after paste
)}

When a valid URL is pasted, for ~250ms: isImageUrl is false, filteredIcons is [] (no icon matches a URL string), and the "no results" message appears. Then the debounce fires and the URL preview button replaces it. It's a minor flicker but visible.

A simple fix is to derive isImageUrl from the non-debounced searchQuery for the icon-list logic, and only use the debounced value for the image preview rendering (which benefits from the debounce to avoid unnecessary <img> re-renders while typing).

Fix this →


Security / Privacy note

src/components/ProviderIcon.tsx:39 / src/lib/schemas/provider.ts:57-71

The HTTPS-only constraint in the Zod schema is a good call — it prevents loading mixed-content and avoids file:// or javascript: schemes. One thing worth documenting: rendering user-supplied external URLs as <img> means the browser will make a GET request to the image host on every render, which leaks the user's IP to that host. This is the standard trade-off for any "paste an image URL" feature, but it's worth being aware of if users are privacy-conscious.

There's no validation that the URL actually resolves to an image MIME type. This is expected for a client-only feature (checking MIME type would require a proxy request), but the onError fix above partially mitigates the bad-UX side of this.


Minor: color prop defined but unused

src/components/IconPicker.tsx:14-15

interface IconPickerProps {
  // ...
  color?: string; // 预览颜色
}

export const IconPicker: React.FC<IconPickerProps> = ({
  value,
  onValueChange,
  // color is not destructured or used
}) => {

color appears in the interface and in the JSDoc comment but is never destructured or forwarded to ProviderIcon. This is pre-existing behavior but the new icon URL feature makes it more visible — if a user selects a custom URL icon, there's no color customization at all (which is probably correct for raster/external images). Consider either removing the dead prop from the interface or forwarding it to ProviderIcon for built-in icon previews.


What's working well

  • isHttpsIconUrl utility is clean and correctly handles the URL parse edge cases.
  • The fix commit's approach of calling onValueChange immediately on URL input (while keeping debounce for display state) correctly solves the dialog-close race condition flagged by Codex. The dual-track design is correct.
  • Zod schema validation for HTTPS-only icon URLs is in the right place and correctly keyed to the icon field.
  • i18n keys added consistently across all three locale files.
  • Initial state seeding from value when it's an HTTPS URL correctly restores the URL to the input when the picker is reopened.

Summary: Two actionable issues — the missing onError handler (bug) and the brief "no results" flash (minor UX). The security/privacy note is informational. Everything else is clean.

@farion1231
Copy link
Copy Markdown
Owner

@claude review

(Re-running with the updated review prompt for comparison.)

@claude
Copy link
Copy Markdown

claude Bot commented May 2, 2026

Claude finished @farion1231's task in 2m 12s —— View job


Code Review (re-run, high-signal-only)

  • Read changed files
  • Analyze for high-signal issues only
  • Post review

No blocking issues. LGTM.

I went through the four code files (IconPicker.tsx, ProviderIcon.tsx, provider.ts, iconUtils.ts) and the three locale files looking for compile/runtime breakage, definite-wrong-result bugs, security issues with concrete triggers, and cross-platform problems. Nothing in those buckets.

A few notes on items that prior reviews raised, just to confirm they're resolved on the current head (9656e34):

  • onError fallback — present at src/components/ProviderIcon.tsx:105, with imgError reset on iconUrl change (:50-52). Broken URLs fall through to the initials fallback as expected.
  • Codex's debounce-loss / commit-on-keystroke concernonValueChange is now driven exclusively from the debounced value via the useEffect at src/components/IconPicker.tsx:32-43, with a value !== debouncedTrimmedQuery guard. No keystroke-level form rewrites; no lost-on-close race, since the user clicks the preview button (which calls onValueChange synchronously) to commit.
  • "No results" flash during debounce — the guard at src/components/IconPicker.tsx:126 and the filteredIcons short-circuit at :47 both use isSearchImageUrl (non-debounced), so the message no longer flashes when a URL is pasted.
  • Scheme safetyisHttpsIconUrl correctly rejects javascript:, data:, file:, http:, etc. via new URL().protocol === "https:". The schema's superRefine (src/lib/schemas/provider.ts:60-71) only triggers the HTTPS check when the value looks like a URL (^[a-z][a-z0-9+.-]*:\/\//i), so plain icon names still validate.

Privacy trade-off (rendering external <img> URLs leaks the user's IP to the image host) is inherent to any "paste an image URL" feature and not something to block on.
• Branch: add-support-custom-icons

@LitoMore
Copy link
Copy Markdown
Author

LitoMore commented May 3, 2026

All issues have been resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants