Skip to content

Conversation

@ComputelessComputer
Copy link
Collaborator

@ComputelessComputer ComputelessComputer commented Dec 4, 2025

Summary

Adds the ability for users to switch between 4 app icon variants (Dark, Light, Nightly, Pro) in the Settings panel. The icon change is applied immediately using macOS's NSApplication.setApplicationIconImage API and persists across app restarts via the store.

Changes:

  • Added icon assets for all 4 variants in apps/desktop/src-tauri/icons/dynamic/
  • Created Rust icon module in the windows plugin with set_app_icon, reset_app_icon, and get_available_icons commands
  • Added app_icon field to the store schema with persistence
  • Created AppIconSettings UI component in the general settings section
  • Wired up config registry with side effect to apply icon changes

Review & Testing Checklist for Human

  • Test on macOS: The icon switching uses NSApplication.setApplicationIconImage which is macOS-specific and could not be tested on my Linux environment. Verify the icon actually changes in the Dock when selecting different options.
  • Verify persistence: Change the icon, quit the app completely, and relaunch. Confirm the selected icon is restored on startup.
  • Main thread safety: The Rust code requires MainThreadMarker. If you see "Must be called from main thread" errors, the command may need to be wrapped with tauri::async_runtime::spawn_blocking or similar.
  • Check icon quality: Verify the 4 PNG icons display correctly at various sizes (Dock, app switcher, etc.)

Recommended test plan:

  1. Open Settings > General
  2. Scroll to "App Icon" section
  3. Click each icon variant and verify the Dock icon updates immediately
  4. Quit and relaunch the app to verify persistence
  5. (Optional) Test on non-macOS to ensure no crashes (should be a no-op)

Notes

  • Icon assets are duplicated in two locations (src-tauri/icons/dynamic/ for Rust include_bytes! and src/assets/icons/ for React UI previews)
  • The include_bytes! macro uses relative paths from the plugin directory which could be fragile if the project structure changes

Link to Devin run: https://app.devin.ai/sessions/41c336d8475546b29a6bbdc68d920b04
Requested by: [email protected] (@ComputelessComputer)

- Add icon assets (nightly, dark, light, pro) for dynamic switching
- Create Rust icon module with NSApplication.setApplicationIconImage
- Add set_app_icon, reset_app_icon, get_available_icons commands
- Update store schema with app_icon field for persistence
- Add AppIconSettings UI component in settings
- Wire up config registry with side effect to apply icon changes

Co-Authored-By: [email protected] <[email protected]>
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@netlify
Copy link

netlify bot commented Dec 4, 2025

Deploy Preview for hyprnote failed.

Name Link
🔨 Latest commit b02ea44
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/69312fc076e5a80008c3afdb

@netlify
Copy link

netlify bot commented Dec 4, 2025

Deploy Preview for hyprnote-storybook failed.

Name Link
🔨 Latest commit b02ea44
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/69312fc01a3d8d0008ce1c3f

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 4, 2025

📝 Walkthrough

Walkthrough

Adds an app icon feature: a new Desktop UI component for selecting app icons, configuration persistence via registry and store schema, Rust plugin commands and icon management (macOS implementation; non-macOS no-op), and Specta command exposure.

Changes

Cohort / File(s) Summary
Desktop Settings UI
apps/desktop/src/components/settings/general/app-icon.tsx, apps/desktop/src/components/settings/general/index.tsx
New AppIconSettings React component and integration into General settings form. Component computes available icons from build channel/billing, renders selectable icon grid, and updates form state via onChange.
Desktop Configuration
apps/desktop/src/config/registry.ts
Adds app_icon config key with default "dark" (typed AppIcon) and a side effect that calls windowsCommands.setAppIcon(value).
Store Schema
packages/store/src/schema-internal.ts
Adds appIconSchema enum ("dark","light","nightly","pro"), AppIcon type, and includes app_icon in generalSchema and Tinybase internal schema.
Windows Plugin Backend
plugins/windows/src/icon.rs, plugins/windows/src/commands.rs, plugins/windows/src/lib.rs
Introduces AppIcon enum and AppIcon::all(). Adds set_app_icon, reset_app_icon, get_available_icons, and get_build_channel commands. Implements macOS icon setting/reset via included PNGs and main-thread NSImage handling; non-macOS builds are no-ops. Exposes module and registers commands with Specta.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Desktop UI
    participant Form as Form State
    participant Registry as Config Registry
    participant CMD as windowsCommands
    participant Plugin as Windows Plugin
    participant Platform as Platform (macOS / Other)

    User->>UI: choose icon
    UI->>Form: onChange(selectedIcon)
    Form->>Form: update app_icon
    Form->>Registry: setPartialValues({ app_icon })
    Registry->>Registry: persist value & trigger side effect
    Registry->>CMD: windowsCommands.setAppIcon(app_icon)
    CMD->>Plugin: invoke set_app_icon(app_icon)
    alt macOS
        Plugin->>Plugin: load PNG (include_bytes)
        Plugin->>Plugin: create NSImage on main thread
        Plugin->>Platform: set application icon
        Platform-->>Plugin: success / error
    else Non-macOS
        Plugin->>Platform: no-op
        Platform-->>Plugin: ok
    end
    Plugin-->>CMD: Result<(), String>
    CMD-->>Registry: command result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review platform-specific macOS image loading and main-thread safety in plugins/windows/src/icon.rs
  • Verify Specta command registration and public API types in plugins/windows/src/lib.rs and plugins/windows/src/commands.rs
  • Confirm registry side-effect wiring and form integration in apps/desktop/src/config/registry.ts and apps/desktop/src/components/settings/general/index.tsx
  • Check schema additions and Tinybase mapping in packages/store/src/schema-internal.ts

Suggested reviewers

  • yujonglee

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add dynamic macOS app icon switching' directly describes the main feature added: the ability to dynamically switch macOS app icons, which is the primary change across all modified files.
Description check ✅ Passed The description thoroughly explains the feature additions, including icon assets, Rust modules, store schema changes, UI component, and config registry wiring, with detailed testing guidance that clearly relates to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch devin/1764829409-dynamic-mac-app-icon

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
plugins/windows/src/lib.rs (1)

5-5: Icon module export and command registration look correct — don’t forget codegen/permissions

The icon module is correctly added and re‑exported, and the three icon commands are wired into make_specta_builder, so they’ll be available to JS once bindings are regenerated.

Per the plugins guidelines, after changing commands in plugins/windows/src/lib.rs you should also:

  • Re‑run whatever codegen step generates @hypr/plugin-windows.
  • Update plugins/windows/permissions/default.toml.
  • Update apps/desktop/src-tauri/capabilities/default.json.

Based on learnings, this keeps the new commands usable and properly permissioned end‑to‑end.

Also applies to: 12-12, 59-62

apps/desktop/src/components/settings/general/app-icon.tsx (1)

1-52: Typed AppIconSettings UI looks good; minor cn style nit

The component is nicely typed against the plugin’s AppIcon union, the options array is explicit, and the selection state/visual feedback work as expected.

One small style nit: project guidelines prefer passing an array to cn for more complex class combinations. Here you could do something like:

className={cn([
  "flex flex-col items-center gap-2 p-3 rounded-lg border-2 transition-all",
  value === option.value
    ? "border-blue-500 bg-blue-50"
    : "border-neutral-200 hover:border-neutral-300 hover:bg-neutral-50",
])}

Functionally everything is correct; this is just for consistency.

apps/desktop/src/components/settings/general/index.tsx (1)

5-5: app_icon is correctly threaded through SettingsGeneral

Including "app_icon" in useConfigValues, wiring it into useForm’s defaultValues, and exposing it via a <form.Field name="app_icon"> bound to AppIconSettings lines up cleanly with the new config key and registry side effect.

Assuming useConfigValues always surfaces a valid AppIcon (via the Zod default), the as AppIcon cast is fine. If there’s any chance of value.app_icon being undefined on older installs, you might optionally default it here (e.g. value.app_icon ?? "dark" as AppIcon) to keep the UI selection deterministic.

Also applies to: 10-10, 24-24, 53-53, 154-161

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b77977 and 7ecf740.

⛔ Files ignored due to path filters (9)
  • apps/desktop/src-tauri/icons/dynamic/dark.png is excluded by !**/*.png
  • apps/desktop/src-tauri/icons/dynamic/light.png is excluded by !**/*.png
  • apps/desktop/src-tauri/icons/dynamic/nightly.png is excluded by !**/*.png
  • apps/desktop/src-tauri/icons/dynamic/pro.png is excluded by !**/*.png
  • apps/desktop/src/assets/icons/dark.png is excluded by !**/*.png
  • apps/desktop/src/assets/icons/light.png is excluded by !**/*.png
  • apps/desktop/src/assets/icons/nightly.png is excluded by !**/*.png
  • apps/desktop/src/assets/icons/pro.png is excluded by !**/*.png
  • plugins/windows/js/bindings.gen.ts is excluded by !**/*.gen.ts
📒 Files selected for processing (7)
  • apps/desktop/src/components/settings/general/app-icon.tsx (1 hunks)
  • apps/desktop/src/components/settings/general/index.tsx (4 hunks)
  • apps/desktop/src/config/registry.ts (3 hunks)
  • packages/store/src/schema-internal.ts (3 hunks)
  • plugins/windows/src/commands.rs (2 hunks)
  • plugins/windows/src/icon.rs (1 hunks)
  • plugins/windows/src/lib.rs (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Agent implementations should use TypeScript and follow the established architectural patterns defined in the agent framework
Agent communication should use defined message protocols and interfaces

Files:

  • packages/store/src/schema-internal.ts
  • apps/desktop/src/config/registry.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • packages/store/src/schema-internal.ts
  • apps/desktop/src/components/settings/general/app-icon.tsx
  • apps/desktop/src/config/registry.ts
  • apps/desktop/src/components/settings/general/index.tsx
plugins/*/src/lib.rs

📄 CodeRabbit inference engine (plugins/AGENTS.md)

After updating commands in plugins/<NAME>/src/lib.rs, run codegen, update plugins/<NAME>/permissions/default.toml, and apps/desktop/src-tauri/capabilities/default.json

Files:

  • plugins/windows/src/lib.rs
🧠 Learnings (2)
📚 Learning: 2025-11-27T11:40:22.782Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: plugins/AGENTS.md:0-0
Timestamp: 2025-11-27T11:40:22.782Z
Learning: Applies to plugins/*/src/lib.rs : After updating commands in `plugins/<NAME>/src/lib.rs`, run `codegen`, update `plugins/<NAME>/permissions/default.toml`, and `apps/desktop/src-tauri/capabilities/default.json`

Applied to files:

  • plugins/windows/src/lib.rs
  • plugins/windows/src/commands.rs
  • plugins/windows/src/icon.rs
📚 Learning: 2025-11-24T16:32:23.055Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T16:32:23.055Z
Learning: Applies to **/*.{ts,tsx} : Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.

Applied to files:

  • apps/desktop/src/components/settings/general/index.tsx
🧬 Code graph analysis (5)
apps/desktop/src/components/settings/general/app-icon.tsx (2)
packages/store/src/schema-internal.ts (1)
  • AppIcon (11-11)
packages/utils/src/cn.ts (1)
  • cn (20-22)
apps/desktop/src/config/registry.ts (1)
packages/store/src/schema-internal.ts (1)
  • AppIcon (11-11)
plugins/windows/src/lib.rs (2)
plugins/windows/src/commands.rs (3)
  • set_app_icon (111-113)
  • reset_app_icon (117-119)
  • get_available_icons (123-125)
plugins/windows/src/icon.rs (4)
  • set_app_icon (26-54)
  • set_app_icon (57-59)
  • reset_app_icon (62-74)
  • reset_app_icon (77-79)
plugins/windows/src/icon.rs (2)
packages/store/src/schema-internal.ts (1)
  • AppIcon (11-11)
plugins/windows/src/commands.rs (2)
  • set_app_icon (111-113)
  • reset_app_icon (117-119)
apps/desktop/src/components/settings/general/index.tsx (2)
packages/store/src/schema-internal.ts (1)
  • AppIcon (11-11)
apps/desktop/src/components/settings/general/app-icon.tsx (1)
  • AppIconSettings (21-52)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Devin
🔇 Additional comments (4)
packages/store/src/schema-internal.ts (1)

10-12: AppIcon schema and Tinybase mapping look consistent

The appIconSchema values match the Rust AppIcon’s snake_case serialization, and storing app_icon as a string in the Tinybase schema is appropriate given the Zod enum. The "dark" default provides a sane baseline for both new and migrated data.

Also applies to: 30-30, 71-71

apps/desktop/src/config/registry.ts (1)

8-11: app_icon registry entry is correctly typed and wired to the plugin

The new app_icon config key, its AppIcon-typed default, and the sideEffect delegating to windowsCommands.setAppIcon are all consistent with the plugin API and the store schema. Calling into the plugin on non‑macOS targets is safe given the no‑op stubs, so this wiring looks good.

Also applies to: 28-29, 163-169

plugins/windows/src/commands.rs (1)

1-1: Icon commands cleanly wrap the icon module API

The new set_app_icon, reset_app_icon, and get_available_icons commands are straightforward wrappers over the icon module and AppIcon::all(), matching the signatures exposed in lib.rs. This keeps the JS surface small and leverages the shared enum nicely.

Also applies to: 109-125

plugins/windows/src/icon.rs (1)

1-79: AppIcon enum and macOS implementation look sound and align with the TS side

The AppIcon enum (with snake_case serde) plus all() provides a clean, serializable surface that matches the Zod/TS AppIcon values consumed in the store and UI. The macOS set_app_icon/reset_app_icon implementations correctly guard on MainThreadMarker and use include_bytes! so the PNG assets are embedded at compile time, while the non‑macOS stubs safely no‑op.

This is a solid foundation for the dynamic icon feature.

- Add get_build_channel command to detect nightly/stable/staging/dev
- Update AppIconSettings to filter icons based on build channel and pro status
- Nightly builds: only nightly icon
- Stable builds: dark and light icons
- Pro users on stable: all icons (dark, light, nightly, pro)
- Auto-reset to valid icon if current selection becomes unavailable

Co-Authored-By: [email protected] <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
apps/desktop/src/components/settings/general/app-icon.tsx (5)

24-37: Confirm icon availability rules for nightly channel and section visibility

getAvailableIconsForTier returns only ["nightly"] when channel === "nightly" (Line 28–29), regardless of isPro, and visibleOptions.length <= 1 then hides the whole App Icon section (Lines 73–75). That means on nightly builds:

  • Pro users can’t select Dark/Light/Pro at all.
  • The settings UI disappears entirely instead of showing a read‑only “Nightly” preview.

If that’s intentional, this is fine; if Pro/nightly should still allow other icons or at least a visible preview, you may want to adjust either getAvailableIconsForTier or the visibleOptions.length guard.

Also applies to: 73-75


45-47: Guard against overwriting a persisted Pro icon while billing state is still loading

With availableIcons derived from channel + isPro and the effect that auto‑fixes invalid values (Lines 64–71), there’s a potential UX issue if isPro is initially false while billing is loading:

  • Suppose a Pro user has value === "pro" persisted.
  • On first mount, isPro defaults to false, so availableIcons is ["dark","light"].
  • The effect sees !availableIcons.includes("pro") and calls onChange(availableIcons[0]), resetting the icon (probably to "dark").
  • When isPro later flips true, value is already "dark", so the user never automatically gets their Pro icon back.

If useBillingAccess doesn’t guarantee a fully resolved isPro before this component mounts, consider gating the correction effect on a “billing loaded” flag or only auto‑downgrading when you know the user is definitively non‑Pro.

Also applies to: 54-57, 64-71


48-52: Handle getBuildChannel failures to avoid unhandled Promise rejections

windowsCommands.getBuildChannel() is used with .then only and no .catch (Lines 48–52). If that command ever rejects, you’ll get an unhandled rejection and channel will silently stay "dev".

Consider adding basic error handling while keeping "dev" as a safe default, for example:

   useEffect(() => {
-    windowsCommands.getBuildChannel().then((ch) => {
-      setChannel(ch as BuildChannel);
-    });
+    void windowsCommands
+      .getBuildChannel()
+      .then((ch) => {
+        setChannel(ch as BuildChannel);
+      })
+      .catch((err) => {
+        console.error("Failed to get build channel", err);
+      });
   }, []);

89-94: Align cn usage with project style (array-based grouping)

Per the coding guidelines, cn should be called with an array and grouped logically. Right now it’s using separate string arguments (Lines 89–94).

You could rewrite it along these lines:

-            className={cn(
-              "flex flex-col items-center gap-2 p-3 rounded-lg border-2 transition-all",
-              value === option.value
-                ? "border-blue-500 bg-blue-50"
-                : "border-neutral-200 hover:border-neutral-300 hover:bg-neutral-50",
-            )}
+            className={cn([
+              "flex flex-col items-center gap-2 p-3 rounded-lg border-2 transition-all",
+              value === option.value
+                ? "border-blue-500 bg-blue-50"
+                : "border-neutral-200 hover:border-neutral-300 hover:bg-neutral-50",
+            ])}

As per coding guidelines, this keeps conditionals and base styles clearly grouped.


22-26: Consider reusing a shared BuildChannel type from the plugin (if available)

You define a local BuildChannel union (Line 22) and then cast the result of windowsCommands.getBuildChannel() to it (Line 50). If @hypr/plugin-windows already exposes a typed build‑channel enum/union, importing that instead of maintaining a separate union here would avoid drift between the command’s return type and this file.

If no such type exists, this is fine as‑is.

Also applies to: 45-51

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ecf740 and b02ea44.

⛔ Files ignored due to path filters (1)
  • plugins/windows/js/bindings.gen.ts is excluded by !**/*.gen.ts
📒 Files selected for processing (3)
  • apps/desktop/src/components/settings/general/app-icon.tsx (1 hunks)
  • plugins/windows/src/commands.rs (2 hunks)
  • plugins/windows/src/lib.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugins/windows/src/commands.rs
  • plugins/windows/src/lib.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • apps/desktop/src/components/settings/general/app-icon.tsx
🔇 Additional comments (1)
apps/desktop/src/components/settings/general/app-icon.tsx (1)

39-42: Component structure and selection UX look solid

Prop surface (value + onChange), memoization of availableIcons/visibleOptions, and the grid‑based button UI (Lines 39–42 and 77–105) are all clear and straightforward. The selected‑state styling and copy make the behavior obvious to users.

Also applies to: 77-105

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