Conversation
WalkthroughThis change migrates the application's icon system from Lucide to Phosphor Icons across both Electron and Vite codebases. It updates icon handling logic, imports, and rendering components to use Phosphor Icons, including reading SVGs directly and updating icon caching and retrieval. React components and icon pickers are refactored to work with the new icon set and metadata. Supporting utility functions and dependencies related to Lucide are removed, and new dependencies for Phosphor Icons are added. The changes streamline icon management, improve type safety, and simplify the logic for menu and UI icon rendering. Additionally, menu update mechanisms are enhanced to respond to window focus changes. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as React Component
participant PhosphorIcons as Phosphor Icon Library
participant PhosphorIcon as PhosphorIcon Component
UI->>PhosphorIcon: Render with id (icon name)
PhosphorIcon->>PhosphorIcons: Lookup icon by id
alt Icon found
PhosphorIcon-->>UI: Render icon component
else Icon not found & fallbackId provided
PhosphorIcon->>PhosphorIcons: Lookup fallback icon
PhosphorIcon-->>UI: Render fallback icon
else No icon found
PhosphorIcon-->>UI: Render null
end
Suggested labels
Poem
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
vite/src/lib/phosphor-icons.tsx (1)
6-6: Unnecessary double cast – rely on the library’s own typings.
iconsis already exported asIconEntry[]by@phosphor-icons/core; theas unknown as IconEntry[]cast is redundant and may hide real typing issues.-export const PhosphorIcons = icons as unknown as IconEntry[]; +export const PhosphorIcons = icons;vite/src/components/settings/sections/spaces/icon-picker.tsx (1)
18-25: State duplication can be avoided – useuseMemoinstead of storing a second copy.
iconListis static (PhosphorIcons), yet it is stored in state only to be copied again intofilteredIcons. This triggers an extra render on mount and is unnecessary work. DeriveiconListwithuseMemo(or just referencePhosphorIconsdirectly) and initialisefilteredIconsfrom that.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (13)
electron/browser/utility/menu/items/spaces.ts(2 hunks)electron/package.json(1 hunks)vite/package.json(1 hunks)vite/src/components/browser-ui/sidebar/content/space-title.tsx(2 hunks)vite/src/components/browser-ui/sidebar/spaces-switcher.tsx(2 hunks)vite/src/components/main/dynamic-lucide-icon.tsx(0 hunks)vite/src/components/onboarding/stages/initial-space/icon.tsx(3 hunks)vite/src/components/settings/sections/spaces/icon-picker.tsx(5 hunks)vite/src/components/settings/sections/spaces/space-card.tsx(2 hunks)vite/src/components/settings/sections/spaces/theme-editors/background-gradient.tsx(2 hunks)vite/src/components/settings/sections/spaces/theme-editors/space-icon.tsx(2 hunks)vite/src/lib/phosphor-icons.tsx(1 hunks)vite/src/lib/utils.ts(0 hunks)
💤 Files with no reviewable changes (2)
- vite/src/components/main/dynamic-lucide-icon.tsx
- vite/src/lib/utils.ts
🧰 Additional context used
🧬 Code Graph Analysis (7)
vite/src/components/settings/sections/spaces/theme-editors/space-icon.tsx (1)
vite/src/components/settings/sections/spaces/icon-picker.tsx (1)
SpaceIconPicker(16-80)
vite/src/components/onboarding/stages/initial-space/icon.tsx (2)
vite/src/lib/phosphor-icons.tsx (1)
SpaceIcon(31-33)vite/src/components/settings/sections/spaces/icon-picker.tsx (1)
SpaceIconPicker(16-80)
vite/src/components/settings/sections/spaces/theme-editors/background-gradient.tsx (1)
vite/src/lib/phosphor-icons.tsx (1)
SpaceIcon(31-33)
vite/src/components/browser-ui/sidebar/spaces-switcher.tsx (1)
vite/src/lib/phosphor-icons.tsx (1)
SpaceIcon(31-33)
vite/src/components/browser-ui/sidebar/content/space-title.tsx (1)
vite/src/lib/phosphor-icons.tsx (1)
SpaceIcon(31-33)
vite/src/components/settings/sections/spaces/space-card.tsx (1)
vite/src/lib/phosphor-icons.tsx (1)
SpaceIcon(31-33)
vite/src/components/settings/sections/spaces/icon-picker.tsx (1)
vite/src/lib/phosphor-icons.tsx (2)
PhosphorIcons(6-6)SpaceIcon(31-33)
🪛 Biome (1.9.4)
vite/src/lib/phosphor-icons.tsx
[error] 15-15: Don't use 'String' as a type.
Use lowercase primitives for consistency.
Safe fix: Use 'string' instead
(lint/complexity/noBannedTypes)
🔇 Additional comments (15)
vite/src/components/settings/sections/spaces/theme-editors/space-icon.tsx (2)
2-2: Updated icon picker importThe import was correctly updated from
LucideIconPickertoSpaceIconPickeras part of the migration to Phosphor Icons.
19-22: Updated component to use SpaceIconPickerThe component usage was correctly updated to use
SpaceIconPickerwhile maintaining the same prop interface (selectedIconandonSelectIcon), which ensures backward compatibility and consistent behavior.electron/package.json (1)
22-22: Added Phosphor Icons core dependencyThe Phosphor Icons core dependency was added while removing the Lucide package, which is consistent with the migration strategy. This ensures that the electron part of the application has the necessary resources to work with Phosphor Icons.
vite/src/components/browser-ui/sidebar/spaces-switcher.tsx (2)
6-6: Updated icon importThe import was correctly updated to use the new
SpaceIconcomponent from the Phosphor icons module.
17-19: Updated icon component usageThe component usage was correctly updated from
DynamicLucideIcontoSpaceIconwith the prop name change fromiconIdtoid. This maintains the same functionality while adapting to the new icon system.Looking at the relevant code snippet,
SpaceIconwrapsPhosphorIconwith sensible defaults likefallbackId="DotOutline"andweight="duotone", which ensures consistent icon styling across the application.vite/src/components/settings/sections/spaces/theme-editors/background-gradient.tsx (2)
7-7: Import for newSpaceIconcomponent added correctly.The import statement correctly brings in the new Phosphor-based icon component from the central location.
103-103: Icon rendering successfully migrated to Phosphor.The component has been properly updated to use the new
SpaceIconcomponent with the correct props:
idproperty replaces the previousiconId- Appropriate styling classes are applied with
className="size-6 text-white"This change correctly implements the migration from Lucide to Phosphor icons while maintaining the same visual appearance.
vite/src/components/browser-ui/sidebar/content/space-title.tsx (2)
3-3: Import forSpaceIconcomponent added correctly.The import statement correctly brings in the new Phosphor-based icon component.
13-13: Icon component properly replaced with explicit fallback override.The
DynamicLucideIconhas been correctly replaced withSpaceIcon, with appropriate property adjustments:
iconIdchanged toidto match the new component's APIfallbackId={undefined}explicitly overrides the default fallback iconSetting
fallbackId={undefined}means no fallback icon will be shown if the specified icon ID is invalid, which differs from the default behavior of showing a "DotOutline" icon.Is this intentional? In other components you're using the default fallback behavior. Consider whether you want consistent fallback behavior across all icon usages.
vite/src/components/onboarding/stages/initial-space/icon.tsx (3)
5-5: Imports correctly updated for both icon components.The imports have been properly updated:
SpaceIconPickerreplaces the previousLucideIconPickerSpaceIconreplaces the previousDynamicLucideIconThis ensures consistent use of the new Phosphor-based icon system.
Also applies to: 8-8
131-131: Icon component correctly updated with appropriate props.The implementation now uses
SpaceIconwith the correct props:
idreplaces the previousiconIdproperty- Class names and styling are preserved
This change maintains the visual appearance while using the new icon system.
142-142: Icon picker component updated consistently.The
SpaceIconPickercomponent is properly used with the same props as the previousLucideIconPicker:
selectedIcon- Passes the current selected icononSelectIcon- Passes the callback to update the selected iconThis ensures the component continues to function as before while using the new icon system.
vite/src/components/settings/sections/spaces/space-card.tsx (2)
3-3: Import forSpaceIconcomponent added correctly.The import statement correctly brings in the new Phosphor-based icon component.
31-31: Icon rendering successfully simplified with Phosphor component.The implementation has been simplified by using the new
SpaceIconcomponent:
- Removes the previous dynamic loading pattern with state management
- Uses a direct, synchronous rendering approach
- Maintains the same visual styling with
className="size-6 text-white"This change improves code readability and maintainability while ensuring consistent icon rendering.
electron/browser/utility/menu/items/spaces.ts (1)
41-47:fillreplacement is incomplete for duotone / stroked icons.Many Phosphor duotone icons have multiple
<path>elements and use bothfillandstroke. Replacing only the firstfill="…"leaves secondary layers untouched and produces half‑colored icons. Consider a more robust approach such as injectingstyle="*{fill:white;stroke:white}"or usingsharp’s tinting.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
electron/browser/utility/menu/items/spaces.ts (1)
30-59: Hard-coded asset path is fragile inside ASAR and when package layout changes.Deriving the SVG file path with path joins assumes the package structure never changes and that Electron can read outside the ASAR bundle at runtime.
Instead of reading SVG files from the filesystem, consider using the icon metadata directly from the Phosphor icon package:
function getPhosphorIconPath(pascalName: string): string | null { const name = getIconNameFromPascalCase(pascalName); if (!name) return null; - try { - // Find the icon directly from node_modules in dev, or from app.asar in production - let iconPath: string; - - // Use the packaged path if the app is packaged - if (app.isPackaged) { - const pkgPath = path.join(process.resourcesPath, "duotone", `${name}-duotone.svg`); - iconPath = pkgPath; - } else { - const devPath = path.join( - require.resolve("@phosphor-icons/core"), - "..", - "..", - "assets", - "duotone", - `${name}-duotone.svg` - ); - iconPath = devPath; - } - - return iconPath; - } catch (error) { - console.error("Failed to resolve phosphor-icons path:", error); - return null; - } + try { + // Find the icon directly from the Phosphor icon metadata + const icon = PhosphorIcons.find(icon => icon.name === name); + if (!icon) return null; + + // Use the SVG content directly if available + if (icon.svg) return icon.svg; + + // Or use the file_path if provided + if (icon.file_path) return icon.file_path; + + return null; + } catch (error) { + console.error("Failed to resolve phosphor icon:", error); + return null; + } }
🧹 Nitpick comments (2)
electron/browser/utility/menu/items/spaces.ts (2)
20-20: Type assertion needs validation to ensure runtime safety.The type assertion
as unknown as IconEntry[]is a workaround that bypasses TypeScript's type checking. This could lead to runtime errors if the actual structure doesn't match the expectedIconEntrytype.Consider adding validation or a more type-safe approach:
-const PhosphorIcons = icons as unknown as IconEntry[]; +// Validate the structure matches IconEntry before casting +const PhosphorIcons = Array.isArray(icons) && icons.every(icon => + 'name' in icon && 'pascal_name' in icon +) ? icons as IconEntry[] : [];
87-105: Icon retrieval function lacks memory management.The icon cache grows unbounded, which could lead to memory leaks if many different icons are used over time.
Consider implementing a size limit or LRU cache:
// Icon cache type IconCacheKey = `${string}-${number | undefined}`; +const MAX_CACHE_SIZE = 100; // Limit cache size const iconCache = new Map<IconCacheKey, NativeImage>(); async function getIconAsNativeImage(name: string, padding?: number): Promise<NativeImage | null> { const cacheKey = `${name}-${padding}` as IconCacheKey; // Check cache first if (iconCache.has(cacheKey)) { return iconCache.get(cacheKey) as NativeImage; } // Create new icon if not in cache const iconPath = getPhosphorIconPath(name); if (!iconPath) return null; const image = await createSvgFromIconPath(iconPath); if (image) { iconCache.set(cacheKey, image); + // Basic cache size management + if (iconCache.size > MAX_CACHE_SIZE) { + const oldestKey = iconCache.keys().next().value; + iconCache.delete(oldestKey); + } } return image; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
electron/browser/utility/menu/items/spaces.ts(2 hunks)electron/forge.config.ts(1 hunks)vite/src/lib/phosphor-icons.tsx(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- electron/forge.config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- vite/src/lib/phosphor-icons.tsx
🔇 Additional comments (3)
electron/browser/utility/menu/items/spaces.ts (3)
133-134: This code simplification improves readability.The combined null check is more concise than the previous version.
143-195: Excellent error handling and robustness improvements.The refactored function includes several important improvements:
- Proper try-catch blocks for error handling
- Using Promise.allSettled to handle potential failures with individual menu items
- Fallback menu when spaces can't be loaded
- Type narrowing with the filter for better type safety
These changes make the code more reliable and maintainable.
25-28: 🛠️ Refactor suggestionError handling for missing icons is suboptimal.
The function falls back to "dot-outline" when
iconis undefined, but it returnsicon?.name || "dot-outline"which would return an empty string ificonexists but has no name property.function getIconNameFromPascalCase(pascalCaseName: string): string { const icon = PhosphorIcons.find((icon) => icon.pascal_name === pascalCaseName); - return icon?.name || "dot-outline"; + return icon?.name || "dot-outline"; // Return default if icon not found or name is empty }Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
electron/browser/utility/menu/items/spaces.ts (2)
30-59: Hard-coded asset path is fragile inside ASAR and when package layout changes.This implementation still uses hardcoded paths and file system operations, which is fragile when the package structure changes or when accessing files within an ASAR archive.
Consider using the official API approach mentioned in the past review - each
IconEntrymight already contain ansvgfield that could be used directly, eliminating file system IO and path brittleness.If you must use file paths, implement more robust path resolution with fallbacks:
function getPhosphorIconPath(pascalName: string): string | null { const name = getIconNameFromPascalCase(pascalName); if (!name) return null; try { - // Find the icon directly from node_modules in dev, or from app.asar in production - let iconPath: string; - - // Use the packaged path if the app is packaged - if (app.isPackaged) { - const pkgPath = path.join(process.resourcesPath, "duotone", `${name}-duotone.svg`); - iconPath = pkgPath; - } else { - const devPath = path.join( - require.resolve("@phosphor-icons/core"), - "..", - "..", - "assets", - "duotone", - `${name}-duotone.svg` - ); - iconPath = devPath; - } + // Define potential paths in order of preference + const possiblePaths = []; + + // Try to get icon SVG directly from the IconEntry if available + const iconEntry = PhosphorIcons.find(icon => icon.name === name); + if (iconEntry?.svg) { + // If the icon has an SVG string, no need for file path + return null; // Will handle inline SVG in getIconAsNativeImage + } + + // Add packaged path for production + if (app.isPackaged) { + possiblePaths.push(path.join(process.resourcesPath, "duotone", `${name}-duotone.svg`)); + // Add alternative locations as fallbacks + possiblePaths.push(path.join(process.resourcesPath, "assets", "duotone", `${name}-duotone.svg`)); + } else { + // Development paths + try { + const basePath = path.dirname(require.resolve("@phosphor-icons/core/package.json")); + possiblePaths.push(path.join(basePath, "assets", "duotone", `${name}-duotone.svg`)); + } catch (err) { + console.warn("Could not resolve @phosphor-icons/core package location"); + } + } + + // Return the first path that exists (would need to check synchronously) + // For simplicity in this example, just return the first path + return possiblePaths[0] || null; return iconPath; } catch (error) { console.error("Failed to resolve phosphor-icons path:", error); return null; } }Additionally, if
IconEntryhas file path information, consider using that directly.
62-92: SVG fill color modification is brittle and may not work with complex SVGs.While the approach has been improved with a more robust regex replacement, it's still string-based manipulation which can be fragile with complex SVGs.
Consider using an XML/SVG parser like
jsdomorcheeriofor more reliable SVG manipulation:async function createSvgFromIconPath(iconPath: string): Promise<NativeImage | null> { if (!svgPlatforms.includes(process.platform)) { return null; } try { let svgString = await readFile(iconPath, "utf8"); - // Make SVG white using a more robust approach - // 1. Handle existing fill attributes on the SVG root - svgString = svgString.replace(/<svg([^>]*)>/, (match, attributes) => { - // Remove any existing fill attribute - const cleanedAttributes = attributes.replace(/\s*fill="[^"]*"\s*/g, " "); - return `<svg${cleanedAttributes} fill="white">`; - }); - - // 2. Add style to ensure all elements inherit the white color - svgString = svgString.replace(/<svg([^>]*)>/, (match) => { - return `${match}<style>* { fill: white; stroke: white; }</style>`; - }); + // Simplify by injecting an outer SVG with a filter + // This approach avoids complex parsing but ensures all nested elements are white + svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"> + <defs> + <filter id="whitefill"> + <feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/> + </filter> + </defs> + <g filter="url(#whitefill)"> + ${svgString} + </g> + </svg>`; // Convert to native image const iconBuffer = await sharp(Buffer.from(svgString)).png().resize(16, 16).toBuffer(); return nativeImage.createFromBuffer(iconBuffer); } catch (error) { console.error("Error creating SVG from path:", error); return null; } }This approach uses an SVG filter to reliably convert all colors to white instead of complex string replacements.
🧹 Nitpick comments (3)
electron/browser/utility/menu/items/spaces.ts (3)
20-20: Consider using a more type-safe approach than double type assertion.The double type assertion (
as unknown as IconEntry[]) is potentially unsafe and bypasses TypeScript's type checking. If the structure of the importediconsdoesn't matchIconEntry[], it could lead to runtime errors.-const PhosphorIcons = icons as unknown as IconEntry[]; +// Verify the structure matches IconEntry[] before casting +const PhosphorIcons = Array.isArray(icons) && + icons.every(icon => 'pascal_name' in icon && 'name' in icon) + ? icons as IconEntry[] + : []; + +// Log warning if the icons array is empty after validation +if (PhosphorIcons.length === 0) { + console.warn('PhosphorIcons array is empty or has incorrect structure'); +}
25-28: Improve resilience with better fallback handling.The function returns a default icon "dot-outline" when the icon isn't found, but doesn't verify if this fallback icon actually exists in the Phosphor icon set.
function getIconNameFromPascalCase(pascalCaseName: string): string { const icon = PhosphorIcons.find((icon) => icon.pascal_name === pascalCaseName); - return icon?.name || "dot-outline"; + // Return the found icon name, a verified fallback, or null + if (icon?.name) return icon.name; + + // Verify fallback exists + const fallbackIcon = PhosphorIcons.find((icon) => icon.name === "dot-outline"); + if (fallbackIcon) return "dot-outline"; + + // Last resort fallback to the first available icon + return PhosphorIcons[0]?.name || ""; }
98-116: Add handling for inline SVG content.The function assumes icons are always loaded from files, but if the previous function is modified to use
IconEntry.svgproperty, this function should handle inline SVG strings as well.-async function getIconAsNativeImage(name: string, padding?: number): Promise<NativeImage | null> { +async function getIconAsNativeImage( + name: string, + padding?: number, + inlineSvg?: string +): Promise<NativeImage | null> { const cacheKey = `${name}-${padding}` as IconCacheKey; // Check cache first if (iconCache.has(cacheKey)) { return iconCache.get(cacheKey) as NativeImage; } - // Create new icon if not in cache - const iconPath = getPhosphorIconPath(name); - if (!iconPath) return null; - - const image = await createSvgFromIconPath(iconPath); + let image: NativeImage | null = null; + + // Handle inline SVG if provided + if (inlineSvg) { + // Process inline SVG similarly to file content + try { + // Similar SVG modifications as in createSvgFromIconPath + let svgString = inlineSvg; + svgString = svgString.replace(/<svg([^>]*)>/, (match, attributes) => { + const cleanedAttributes = attributes.replace(/\s*fill="[^"]*"\s*/g, " "); + return `<svg${cleanedAttributes} fill="white">`; + }); + + svgString = svgString.replace(/<svg([^>]*)>/, (match) => { + return `${match}<style>* { fill: white; stroke: white; }</style>`; + }); + + const iconBuffer = await sharp(Buffer.from(svgString)).png().resize(16, 16).toBuffer(); + image = nativeImage.createFromBuffer(iconBuffer); + } catch (error) { + console.error("Error processing inline SVG:", error); + } + } else { + // Fall back to file path approach + const iconPath = getPhosphorIconPath(name); + if (iconPath) { + image = await createSvgFromIconPath(iconPath); + } + } + if (image) { iconCache.set(cacheKey, image); } return image; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
electron/browser/utility/menu/items/spaces.ts(2 hunks)
🔇 Additional comments (4)
electron/browser/utility/menu/items/spaces.ts (4)
129-135: Improved error handling in createSpaceMenuItem.The try-catch block for loading icons is well-implemented, allowing the menu item creation to proceed even if the icon fails to load.
This is a good approach to ensure menu creation remains robust even when individual icon loading fails.
144-145: Improved null-checking with early return.The simplification of the null check with an early return improves code readability and removes unnecessary nesting.
This clean approach to null checking helps maintain code clarity and follows good TypeScript practices.
173-180: Excellent use of Promise.allSettled for robust space menu creation.Using
Promise.allSettledcombined with proper filtering of results ensures that the menu creation continues even if individual space menu items fail to be created.This is a robust pattern that ensures partial failures don't prevent the menu from being created. The type narrowing with the filter predicate is also well-implemented.
154-206: Well-structured async function with comprehensive error handling.The main
createSpacesMenufunction is well-organized with proper error handling, fallbacks for edge cases, and graceful degradation when errors occur.This implementation ensures the menu always appears, even in error conditions, which is crucial for application robustness. The fallback menu provides essential functionality, and the error logging will help with debugging.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
electron/browser/utility/menu/items/spaces.ts (1)
30-59: 🛠️ Refactor suggestionHard-coded asset path remains fragile.
While you've improved the path resolution by handling both packaged and development environments, this approach still has issues: it relies on hard-coded relative paths that could break if package structures change, and it performs unnecessary filesystem IO.
Consider using the
IconEntrydata directly from Phosphor:function getPhosphorIconPath(pascalName: string): string | null { const name = getIconNameFromPascalCase(pascalName); if (!name) return null; try { - // Find the icon directly from node_modules in dev, or from app.asar in production - let iconPath: string; - - // Use the packaged path if the app is packaged - if (app.isPackaged) { - const pkgPath = path.join(process.resourcesPath, "duotone", `${name}-duotone.svg`); - iconPath = pkgPath; - } else { - const devPath = path.join( - require.resolve("@phosphor-icons/core"), - "..", - "..", - "assets", - "duotone", - `${name}-duotone.svg` - ); - iconPath = devPath; - } - - return iconPath; + // Find the icon directly from the icon data + const icon = PhosphorIcons.find(icon => icon.name === name); + return icon?.svg ? icon.svg : null; } catch (error) { console.error("Failed to resolve phosphor-icons path:", error); return null; } }
🧹 Nitpick comments (3)
electron/browser/utility/menu/items/spaces.ts (3)
20-20: Type casting viaunknownshould be avoided.The type casting
icons as unknown as IconEntry[]is a TypeScript anti-pattern that bypasses type safety. This could lead to runtime errors if the actual structure doesn't match the expected interface.Consider using a more type-safe approach:
-const PhosphorIcons = icons as unknown as IconEntry[]; +// Define an explicit type guard to verify structure +function isIconEntryArray(obj: any): obj is IconEntry[] { + return Array.isArray(obj) && obj.length > 0 && 'pascal_name' in obj[0] && 'name' in obj[0]; +} + +const PhosphorIcons = isIconEntryArray(icons) ? icons : [];
62-92: SVG manipulation has been improved but could be more robust.Your improved SVG manipulation is better than the previous approach, but still uses regular expressions to modify XML/SVG which can be brittle with complex SVGs. This addresses part of the previous review comment, but could be further improved.
For even more robust handling of SVGs, consider using a proper XML parser:
async function createSvgFromIconPath(iconPath: string): Promise<NativeImage | null> { if (!svgPlatforms.includes(process.platform)) { return null; } try { - let svgString = await readFile(iconPath, "utf8"); - - // Make SVG white using a more robust approach - // 1. Handle existing fill attributes on the SVG root - svgString = svgString.replace(/<svg([^>]*)>/, (match, attributes) => { - // Remove any existing fill attribute - const cleanedAttributes = attributes.replace(/\s*fill="[^"]*"\s*/g, " "); - return `<svg${cleanedAttributes} fill="white">`; - }); - - // 2. Add style to ensure all elements inherit the white color - svgString = svgString.replace(/<svg([^>]*)>/, (match) => { - return `${match}<style>* { fill: white; stroke: white; }</style>`; - }); + // If using the direct SVG string approach: + let svgString = typeof iconPath === 'string' && iconPath.startsWith('<svg') + ? iconPath + : await readFile(iconPath, "utf8"); + + // Use a lightweight XML/SVG parser like 'svgson' if added as a dependency + // For now, continue with the improved regex approach: + // 1. Handle existing fill attributes on the SVG root + svgString = svgString.replace(/<svg([^>]*)>/, (match, attributes) => { + // Remove any existing fill attribute + const cleanedAttributes = attributes.replace(/\s*fill="[^"]*"\s*/g, " "); + return `<svg${cleanedAttributes} fill="white">`; + }); + + // 2. Add style to ensure all elements inherit the white color + svgString = svgString.replace(/<svg([^>]*)>/, (match) => { + return `${match}<style>* { fill: white; stroke: white; }</style>`; + }); // Convert to native image const iconBuffer = await sharp(Buffer.from(svgString)).png().resize(16, 16).toBuffer(); return nativeImage.createFromBuffer(iconBuffer); } catch (error) { console.error("Error creating SVG from path:", error); return null; } }
95-116: Icon cache may grow unbounded.The current implementation of icon caching uses a Map that grows indefinitely as new icons are loaded, which could potentially lead to memory issues over time.
Consider adding a size limit or an LRU cache implementation:
// Icon cache type IconCacheKey = `${string}-${number | undefined}`; -const iconCache = new Map<IconCacheKey, NativeImage>(); +const MAX_CACHE_SIZE = 100; // Adjust based on expected icon usage +class LimitedIconCache { + private cache = new Map<IconCacheKey, { image: NativeImage, lastUsed: number }>(); + + has(key: IconCacheKey): boolean { + return this.cache.has(key); + } + + get(key: IconCacheKey): NativeImage | undefined { + const entry = this.cache.get(key); + if (entry) { + entry.lastUsed = Date.now(); + return entry.image; + } + return undefined; + } + + set(key: IconCacheKey, image: NativeImage): void { + // Evict least recently used if cache is full + if (this.cache.size >= MAX_CACHE_SIZE) { + let oldestKey: IconCacheKey | null = null; + let oldestTime = Infinity; + + for (const [k, v] of this.cache.entries()) { + if (v.lastUsed < oldestTime) { + oldestTime = v.lastUsed; + oldestKey = k; + } + } + + if (oldestKey) this.cache.delete(oldestKey); + } + + this.cache.set(key, { image, lastUsed: Date.now() }); + } +} + +const iconCache = new LimitedIconCache();
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
electron/browser/browser.ts(2 hunks)electron/browser/utility/menu.ts(2 hunks)electron/browser/utility/menu/items/spaces.ts(1 hunks)electron/browser/window.ts(3 hunks)electron/modules/windows.ts(2 hunks)electron/package.json(2 hunks)vite/package.json(2 hunks)
✅ Files skipped from review due to trivial changes (1)
- electron/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
- vite/package.json
🧰 Additional context used
🧬 Code Graph Analysis (3)
electron/browser/window.ts (1)
electron/modules/windows.ts (1)
windowEvents(34-37)
electron/browser/browser.ts (1)
electron/browser/utility/menu.ts (1)
setupMenu(15-38)
electron/browser/utility/menu.ts (1)
electron/modules/windows.ts (1)
windowEvents(34-37)
🔇 Additional comments (14)
electron/browser/window.ts (3)
11-12: Good addition of window event importsThese imports properly support the new focus handling functionality added below.
116-118: Well implemented window focus event propagationThis new event listener correctly propagates the native window focus event to the global window events system, enabling other components to react when a window receives focus.
174-174: Good addition of menu update on space changeUpdating the menu immediately after the space changes ensures the UI remains consistent with the application state. This is a good practice for maintaining a responsive user experience.
electron/browser/browser.ts (2)
27-27: Good addition of public updateMenu methodAdding this public property provides a clean interface for other components to trigger menu updates when needed.
42-42: Well implemented menu update initializationStoring the returned function from setupMenu provides a consistent way to update menus throughout the application lifecycle.
electron/browser/utility/menu.ts (3)
13-13: Good addition of window events importsThese imports properly support the new focus-triggered menu updates.
35-35: Well implemented focus-triggered menu updatesListening for the FOCUSED event ensures the menu stays up-to-date when the user switches between windows, improving the user experience.
37-37: Good design returning the craftMenu functionReturning the craftMenu function allows external components to trigger menu updates when needed, providing a clean API for menu management.
electron/modules/windows.ts (3)
24-24: Good addition of FOCUSED event typeAdding this event type properly extends the window event system to support focus-related functionality.
31-31: Well typed event handlerThe type definition correctly specifies that the FOCUSED event handler receives the BrowserWindow instance, ensuring type safety throughout the application.
84-86: Well implemented focus event handlingThis event handler properly emits the FOCUSED event when a window gains focus, enabling other components to react accordingly.
electron/browser/utility/menu/items/spaces.ts (3)
129-136: Good error handling for icon loading.The try-catch block around icon loading is a good practice that ensures a failing icon doesn't prevent a menu item from being created. The error is properly logged and execution continues gracefully.
146-148: Cleaner conditional check.You've simplified the previous code by combining null checks into a single conditional statement, which improves readability.
157-211: Robust async menu creation with proper error handling.The rewritten
createSpacesMenufunction is well-structured with comprehensive error handling. UsingPromise.allSettledensures that failures in individual menu items don't cause the entire menu creation to fail. The fallback menu is a good practice to ensure the application remains functional even if there are issues.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
electron/browser/utility/menu/items/spaces.ts (1)
30-59: 🛠️ Refactor suggestionHard-coded asset path is fragile inside ASAR and when package layout changes.
Deriving the SVG file path with
path.join(require.resolve(...), "..", "..", "assets", "duotone", ...)assumes the package structure never changes. This approach is improved from the previous implementation by usingprocess.resourcesPathfor packaged apps, but it's still brittle.Consider using the icon data directly from the
IconEntryobject:function getPhosphorIconPath(pascalName: string): string | null { const name = getIconNameFromPascalCase(pascalName); if (!name) return null; try { - // Find the icon directly from node_modules in dev, or from app.asar in production - let iconPath: string; - - // Use the packaged path if the app is packaged - if (app.isPackaged) { - const pkgPath = path.join(process.resourcesPath, "duotone", `${name}-duotone.svg`); - iconPath = pkgPath; - } else { - const devPath = path.join( - require.resolve("@phosphor-icons/core"), - "..", - "..", - "assets", - "duotone", - `${name}-duotone.svg` - ); - iconPath = devPath; - } - - return iconPath; + // Find the icon in the PhosphorIcons array + const icon = PhosphorIcons.find((icon) => icon.name === name); + if (!icon) return null; + + // Use the file_path if available, otherwise use the svg content directly + if (icon.file_path) { + return icon.file_path; + } else if (icon.svg) { + // If we have SVG content directly, we can skip file reading + // Save to a temporary file or pass directly to createSvgFromSvgString + // This would require modifying createSvgFromIconPath to accept SVG strings + const tempPath = path.join(app.getPath('temp'), `${name}-duotone.svg`); + fs.writeFileSync(tempPath, icon.svg); + return tempPath; + } + + return null; } catch (error) { console.error("Failed to resolve phosphor-icons path:", error); return null; } }This would also require adding a complementary function:
async function createSvgFromSvgString(svgString: string): Promise<NativeImage | null> { if (!svgPlatforms.includes(process.platform)) { return null; } try { // Apply the same SVG modifications as in createSvgFromIconPath // [...] const iconBuffer = await sharp(Buffer.from(svgString)).png().resize(16, 16).toBuffer(); return nativeImage.createFromBuffer(iconBuffer); } catch (error) { console.error("Error creating SVG from string:", error); return null; } }
🧹 Nitpick comments (4)
electron/browser/utility/menu/items/spaces.ts (4)
20-20: Consider using proper TypeScript typing for icons.Using
as unknown as IconEntry[]type casting is problematic as it bypasses TypeScript's type checking. This could lead to runtime errors if the structure of icons doesn't match what you're expecting.-const PhosphorIcons = icons as unknown as IconEntry[]; +const PhosphorIcons: IconEntry[] = icons as IconEntry[];
95-95: Simplify IconCacheKey type definition.The template literal type
IconCacheKey = \${string}`is unnecessarily complex for this use case since it's functionally equivalent to just usingstring`.-type IconCacheKey = `${string}`; +type IconCacheKey = string;
140-140: Menu item ID changes based on checked state.Including the checked state in the ID (
id: \space-${space.id}-${checked ? "checked" : "unchecked"}``) makes the ID unstable across state changes. If any code relies on consistent IDs, this could cause issues.-id: `space-${space.id}-${checked ? "checked" : "unchecked"}`, +id: `space-${space.id}`,If the checked state needs to be identifiable from the ID for some reason, consider adding it as a separate data attribute instead.
156-156: Unused Browser parameter.The
_browserparameter increateSpacesMenuis prefixed with an underscore indicating it's not used, but still required by the function signature. This suggests a potential misalignment with the function's intended usage.If the parameter is truly not needed, consider updating the API where this function is called to remove the parameter.
Alternatively, if it should be used (e.g., to get the focused window through Browser's methods), update the implementation to utilize it properly:
-export async function createSpacesMenu(_browser: Browser): Promise<MenuItemConstructorOptions> { +export async function createSpacesMenu(browser: Browser): Promise<MenuItemConstructorOptions> { try { const spaces = await getSpaces(); - const focusedWindow = getFocusedWindow(); + const focusedWindow = browser.getFocusedWindow();
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
electron/browser/utility/menu/items/spaces.ts(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
electron/browser/utility/menu/items/spaces.ts (5)
vite/src/lib/phosphor-icons.tsx (1)
PhosphorIcons(6-6)electron/browser/browser.ts (2)
Browser(21-187)getFocusedWindow(112-114)electron/sessions/spaces.ts (1)
getSpaces(231-255)electron/modules/windows.ts (1)
getFocusedWindow(62-65)electron/settings/main.ts (1)
settings(40-76)
🔇 Additional comments (3)
electron/browser/utility/menu/items/spaces.ts (3)
61-92: Improved SVG color modification handling.Great job implementing a more robust approach to SVG color modification! This handles complex SVGs with nested elements by:
- Properly cleaning existing fill attributes
- Setting a white fill on the SVG root element
- Adding a CSS style that ensures consistent coloring across all elements
This is a significant improvement from the previous string replacement approach.
177-184: Excellent use of Promise.allSettled for robust menu creation.Using
Promise.allSettledinstead ofPromise.allis a great choice here as it ensures the menu is created even if some space items fail to load. Combined with proper filtering of rejected promises, this approach ensures users always get a functional menu.
197-209: Good error handling with fallback menu.The comprehensive error handling with a fallback menu ensures that users will always see a functional Spaces menu, even if there are underlying issues with fetching spaces or creating menu items. This improves reliability and user experience.
Summary by CodeRabbit