feishin Documentation

A modern self-hosted music player.

10,215 stars TypeScript TypeScript 16 files Β· ~15,425 tokens #jellyfin#music-player#navidrome#self-hosted#subsonic#subsonic-client
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy feishin on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name feishin -p 9180:9180 --restart always ghcr.io/jeffvli/feishin:latest
Configured Services & Ports
βš™οΈ feishin
image: ghcr.io/jeffvli/feishin:latest
πŸ”Œ Ports: 9180:9180
docker-compose.yml
yaml (docker-compose.yml)
services:
    feishin:
        container_name: feishin
        image: "ghcr.io/jeffvli/feishin:latest"
        restart: unless-stopped
        environment:
            - SERVER_NAME=jellyfin # pre-defined server name
            - SERVER_LOCK=false # When true AND name/type/url are set, only username/password can be toggled
            - SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive
            - SERVER_URL=http://localhost:8096 # http://address:port or https://address:port
            # - REMOTE_URL=http://share.localhost # Used for compatibility with external functionality, such as custom sharing URLs on Navidrome
            - LEGACY_AUTHENTICATION=false # When SERVER_LOCK is true, sets the legacyauth flag for server authentication (true or false)
            - ANALYTICS_DISABLED=true # Set to true to disable Umami analytics tracking
        ports:
            - 9180:9180
            # Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190

Technical Documentation: jeffvli/feishin

ℹ️ Provenance: Multi-Tier Ingestion Β· CodeWiki Reference

Overview (`README.md`)

Feishin


Rewrite of Sonixd.

Features

  • MPV player backend
  • Web player backend
  • Modern UI
  • Scrobble playback to your server
  • Smart playlist editor (Navidrome)
  • Synchronized and unsynchronized lyrics support
  • Request a feature or view taskboard

Screenshots

Getting Started

Download the latest desktop client. The desktop client is the recommended way to use Feishin. It supports both the MPV and web player backends, as well as includes built-in fetching for lyrics.

macOS Notes

If you're using a device running macOS 12 (Monterey) or higher, check here for instructions on how to remove the app from quarantine.

For media keys to work, you will be prompted to allow Feishin to be a Trusted Accessibility Client. After allowing, you will need to restart Feishin for the privacy settings to take effect.

Linux Notes

Feishin is available in Flathub.

Alternatively, you can install it as an Appimage. We provide a small install script to download the latest .AppImage, make it executable, and also download the icons required by Desktop Environments. Finally, it generates a .desktop file to add Feishin to your Application Launcher.

Simply run the installer like this:

sh
dir=/your/application/directory
curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir"

The script also has an option to add launch arguments to run Feishin in native Wayland mode. Note that this is experimental in Electron and therefore not officially supported. If you want to use it, run this instead:

sh
dir=/your/application/directory
curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir" wayland-native

It also provides a simple uninstall routine, removing the downloaded files:

sh
dir=/your/application/directory
curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir" remove

The entry should show up in your Application Launcher immediately. If it does not, simply log out, wait 10 seconds, and log back in. Your Desktop Environment may alternatively provide a way to reload entries.

Web and Docker

Visit https://feishin.vercel.app to use the hosted web version of Feishin. The web client only supports the web player backend.

Feishin is also available as a Docker image. The images are hosted via ghcr.io and are available to view here. You can run the container using the following commands:

bash
# Run the latest version
docker run --name feishin -p 9180:9180 ghcr.io/jeffvli/feishin:latest

# Build the image locally
docker build -t feishin .
docker run --name feishin -p 9180:9180 feishin

Docker Compose

To install via Docker Compose, use the following snippet. This also works on Portainer.

yaml
services:
    feishin:
        container_name: feishin
        image: 'ghcr.io/jeffvli/feishin:latest'
        restart: unless-stopped
        environment:
            - SERVER_NAME=jellyfin # pre-defined server name
            - SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
            - SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive
            - SERVER_URL= # http://address:port or https://address:port
            - REMOTE_URL= # http://address or https://address
            - LEGACY_AUTHENTICATION=false # When SERVER_LOCK is true, sets the legacy (plaintext) authentication flag for Subsonic/OpenSubsonic servers
            - ANALYTICS_DISABLED=true # Set to true to disable Umami analytics tracking
        ports:
            - 9180:9180
            # Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190

Configuration

  1. Upon startup you will be greeted with a prompt to select the path to your MPV binary. If you do not have MPV installed, you can download it here or install it using any package manager supported by your OS. After inputting the path, restart the app.

  2. After restarting the app, you will be prompted to select a server. Click the Open menu button and select Manage servers. Click the Add server button in the popup and fill out all applicable details. You will need to enter the full URL to your server, including the protocol and port if applicable (e.g. https://navidrome.my-server.com or http://192.168.0.1:4533).

  • Navidrome - For the best experience, select "Save password" when creating the server and configure the SessionTimeout setting in your Navidrome config to a larger value (e.g. 72h).
    • Linux users - The default password store uses libsecret. kwallet4/5/6 are also supported, but must be explicitly set in Settings > Window > Passwords/secret store.
  1. Optional - If you want to host Feishin on a subpath (not /), then pass in the following environment variable: PUBLIC_PATH=PATH. For example, to host on /feishin, pass in PUBLIC_PATH=/feishin.

  2. Optional - To hard code the server url, pass the following environment variables: SERVER_NAME, SERVER_TYPE (one of jellyfin or navidrome or subsonic), SERVER_URL. To prevent users from changing these settings, pass SERVER_LOCK=true. This can only be set if all three of the previous values are set. When SERVER_LOCK=true, you can also set LEGACY_AUTHENTICATION=true or LEGACY_AUTHENTICATION=false to configure the legacy authentication flag for the server (only applicable for Subsonic/OpenSubsonic servers).

  3. Optional - If your server uses a separate public-facing URL than what integrating applications use internally to communicate with your server, such as a separate Navidrome ShareURL, set REMOTE_URL to said public-facing URL.

  4. Optional - To disable Umami analytics tracking in the Docker/web version, set the environment variable ANALYTICS_DISABLED=true. When enabled, the analytics script will not be loaded and all tracking will be disabled.

  5. Optional - App settings (theme, language, sidebar options, etc.) can be overridden with environment variables on first run. The variables use the FS_ prefix (e.g. FS_GENERAL_THEME=defaultDark, FS_GENERAL_LANGUAGE=de). See the settings environment variable documentation for the full list.

FAQ

MPV is either not working or is rapidly switching between pause/play states

First thing to do is check that your MPV binary path is correct. Navigate to the settings page and re-set the path and restart the app. If your issue still isn't resolved, try reinstalling MPV. Known working versions include v0.35.x and v0.36.x. v0.34.x is a known broken version.

What music servers does Feishin support?

Feishin supports any music server that implements a Navidrome, Jellyfin, or OpenSubsonic compatible API.

I have the issue "The SUID sandbox helper binary was found, but is not configured correctly" on Linux

This happens when you have user (unprivileged) namespaces disabled (sysctl kernel.unprivileged_userns_clone returns 0). You can fix this by either enabling unprivileged namespaces, or by making the chrome-sandbox Setuid.

bash
chmod 4755 chrome-sandbox
sudo chown root:root chrome-sandbox

Ubuntu 24.04 specifically introduced breaking changes that affect how namespaces work. Please see https://discourse.ubuntu.com/t/ubuntu-24-04-lts-noble-numbat-release-notes/39890#:~:text=security%20improvements%20 for possible fixes.

How can I add custom themes?

On the desktop app, you can add custom themes by dropping JSON files into the Themes folder (Settings β†’ General β†’ Theme β†’ Open Folder). See the custom themes documentation for the file format and examples.

Development

Built and tested using Node v23.11.0.

This project is built off of electron-vite

  • pnpm run dev - Start the development server
  • pnpm run dev:watch - Start the development server in watch mode (for main / preload HMR)
  • pnpm run start - Starts the app in production preview mode
  • pnpm run build - Builds the app for desktop
  • pnpm run build:electron - Build the electron app (main, preload, and renderer)
  • pnpm run build:remote - Build the remote app (remote)
  • pnpm run build:web - Build the standalone web app (renderer)
  • pnpm run package - Package the project
  • pnpm run package:dev - Package the project for development locally
  • pnpm run package:linux - Package the project for Linux locally
  • pnpm run package:mac - Package the project for Mac locally
  • pnpm run package:win - Package the project for Windows locally
  • pnpm run publish:linux - Publish the project for Linux
  • pnpm run publish:linux:beta - Publish the project for Linux (beta channel)
  • pnpm run publish:linux-arm64 - Publish the project for Linux ARM64
  • pnpm run publish:linux-arm64:beta - Publish the project for Linux ARM64 (beta channel)
  • pnpm run publish:mac - Publish the project for Mac
  • pnpm run publish:mac:beta - Publish the project for Mac (beta channel)
  • pnpm run publish:win - Publish the project for Windows
  • pnpm run publish:win:beta - Publish the project for Windows (beta channel)
  • pnpm run typecheck - Type check the project
  • pnpm run typecheck:node - Type check the project with tsconfig.node.json
  • pnpm run typecheck:web - Type check the project with tsconfig.web.json
  • pnpm run lint - Lint the project
  • pnpm run lint:fix - Lint the project and fix linting errors
  • pnpm run i18next - Generate i18n files

Translation

This project uses Weblate for translations. If you would like to contribute, please visit the link and submit a translation.

License

GNU General Public License v3.0 Β©


Overview (`.agents/skills/caveman-commit/README.md`)

caveman-commit

Terse Conventional Commits. Why over what.

What it does

Generates commit messages in Conventional Commits format. Subject ≀50 chars, hard cap 72. Imperative mood. Body only when the why is non-obvious or there are breaking changes. No AI attribution, no "this commit does X", no emoji unless the project uses them. Body always required for breaking changes, security fixes, data migrations, and reverts β€” future debuggers need the context.

Outputs only the message. Does not stage, commit, or amend.

How to invoke

text
/caveman-commit

Also triggers on phrases like "write a commit", "commit message", "generate commit".

Example output

Diff: new endpoint for user profile.

text
feat(api): add GET /users/:id/profile

Mobile client needs profile data without the full user payload
to reduce LTE bandwidth on cold-launch screens.

Closes #128

Diff: breaking API rename.

text
feat(api)!: rename /v1/orders to /v1/checkout

BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout
before 2026-06-01. Old route returns 410 after that date.

See also


Overview (`.agents/skills/caveman-review/README.md`)

caveman-review

One-line PR comments. Location, problem, fix. No throat-clearing.

What it does

Generates code review comments in L<line>: <severity> . <fix>. format. One line per finding. Severity emoji: πŸ”΄ bug, 🟑 risk, πŸ”΅ nit, ❓ question. Drops "I noticed that...", hedging, and restating what the diff already shows. Keeps exact line numbers, backticked symbols, and concrete fixes.

Auto-clarity: drops terse mode for CVE-class security findings, architectural disagreements, and onboarding contexts where the author needs the why. Resumes terse for the rest.

Output only β€” does not approve, request changes, or run linters.

How to invoke

text
/caveman-review

Also triggers on "review this PR", "code review", "review the diff".

Example output

text
L42: πŸ”΄ bug: user can be null after .find(). Add guard before .email.
L88-140: πŸ”΅ nit: 50-line fn does 4 things. Extract validate/normalize/persist.
L23: 🟑 risk: no retry on 429. Wrap in withBackoff(3).
L107: ❓ q: why drop the cache here? Reads on next request will miss.

See also


Overview (`.claude/skills/caveman-commit/README.md`)

caveman-commit

Terse Conventional Commits. Why over what.

What it does

Generates commit messages in Conventional Commits format. Subject ≀50 chars, hard cap 72. Imperative mood. Body only when the why is non-obvious or there are breaking changes. No AI attribution, no "this commit does X", no emoji unless the project uses them. Body always required for breaking changes, security fixes, data migrations, and reverts β€” future debuggers need the context.

Outputs only the message. Does not stage, commit, or amend.

How to invoke

text
/caveman-commit

Also triggers on phrases like "write a commit", "commit message", "generate commit".

Example output

Diff: new endpoint for user profile.

text
feat(api): add GET /users/:id/profile

Mobile client needs profile data without the full user payload
to reduce LTE bandwidth on cold-launch screens.

Closes #128

Diff: breaking API rename.

text
feat(api)!: rename /v1/orders to /v1/checkout

BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout
before 2026-06-01. Old route returns 410 after that date.

See also


Overview (`.claude/skills/caveman-review/README.md`)

caveman-review

One-line PR comments. Location, problem, fix. No throat-clearing.

What it does

Generates code review comments in L<line>: <severity> . <fix>. format. One line per finding. Severity emoji: πŸ”΄ bug, 🟑 risk, πŸ”΅ nit, ❓ question. Drops "I noticed that...", hedging, and restating what the diff already shows. Keeps exact line numbers, backticked symbols, and concrete fixes.

Auto-clarity: drops terse mode for CVE-class security findings, architectural disagreements, and onboarding contexts where the author needs the why. Resumes terse for the rest.

Output only β€” does not approve, request changes, or run linters.

How to invoke

text
/caveman-review

Also triggers on "review this PR", "code review", "review the diff".

Example output

text
L42: πŸ”΄ bug: user can be null after .find(). Add guard before .email.
L88-140: πŸ”΅ nit: 50-line fn does 4 things. Extract validate/normalize/persist.
L23: 🟑 risk: no retry on 429. Wrap in withBackoff(3).
L107: ❓ q: why drop the cache here? Reads on next request will miss.

See also


CUSTOM THEMES (`docs/CUSTOM_THEMES.md`)

Custom Themes

Custom themes let you add your own light and dark themes to the desktop app without rebuilding Feishin. Drop JSON theme files into the Themes folder; Feishin watches that folder and reloads themes when files change.

Custom themes are Electron / desktop only. They are not available in the web or Docker builds.


Getting started

  1. Open Settings β†’ General β†’ Theme.
  2. Under Custom Themes, click Open Folder to reveal the Themes directory.
  3. Add a .json file (for example my-theme.json).
  4. Select the theme from the Theme dropdown (it appears under Dark or Light based on mode).

You can also click Reload in Settings if a change was not picked up automatically.

Themes folder location

Platform Typical path
Windows %APPDATA%\feishin\Themes
macOS ~/Library/Application Support/feishin/Themes
Linux ~/.config/feishin/Themes

In development builds the folder name is under feishin-dev instead of feishin.

Theme files must be .json files in the root of the Themes folder (not nested in subfolders). Linked stylesheets may live next to them or in subfolders, as long as they stay inside Themes.


Theme file format

Each theme is a single JSON object.

json
{
  "mode": "dark",
  "extends": "defaultDark",
  "colors": {
    "primary": "rgb(53, 116, 252)",
    "background": "rgb(12, 12, 12)",
    "foreground": "rgb(225, 225, 225)"
  },
  "app": {
    "scrollbar-size": "9px"
  },
  "mantineOverride": {
    "primaryShade": {
      "dark": 6
    }
  },
  "stylesheets": ["my-theme.css"]
}

Identity

Derived from Behavior
id Filename without .json (e.g. rose-pine-custom.json β†’ rose-pine-custom)
label Title-cased id with - / _ replaced by spaces (e.g. Rose Pine Custom)

Use a unique filename. The id is what appears in settings and what you use when another theme extends this one.

Fields

Field Required Description
mode No (default dark) "dark" or "light". Controls which theme group the theme appears in and the app color scheme.
extends No Built-in theme id or another custom theme’s id (filename without .json). Your theme is merged on top of the base.
colors No Palette overrides. Invalid CSS colors are dropped and shown as a warning in Settings.
app No App chrome / layout CSS variable overrides.
mantineOverride No Partial Mantine theme override.
stylesheets No Array of CSS file paths relative to the Themes folder. Contents are inlined when the theme is active. Paths that escape the Themes folder are ignored.

Unspecified color and app values fall back to Feishin’s default theme (and then to anything provided by extends).


Colors

Supported colors keys:

Key Typical use
background Main app background
background-alternate Alternate / nested background
surface Cards, inputs, elevated surfaces
surface-foreground Text on surfaces
foreground Primary text
foreground-muted Secondary / muted text
primary Accent / primary action color (also drives generated primary shades)
black Black token
white White token
state-error Error state
state-info Info state
state-success Success state
state-warning Warning state

Values must be valid CSS colors recognized by Feishin’s color validator (for example rgb(...), rgba(...), #rrggbb, #rgb). Named CSS colors like red are rejected. Invalid entries are ignored and listed under Custom Themes warnings in Settings.


App variables

Supported app keys (CSS values as strings):

Key Description
content-max-width Max width of main content
root-font-size Root font size
overlay-header Header overlay background
overlay-subheader Subheader overlay background
scrollbar-size Scrollbar thickness
scrollbar-handle-background Scrollbar handle
scrollbar-handle-hover-background Handle on hover
scrollbar-handle-active-background Handle when active
scrollbar-handle-border-radius Handle corner radius
scrollbar-track-background Scrollbar track
scrollbar-track-hover-background Track on hover
scrollbar-track-active-background Track when active
scrollbar-track-border-radius Track corner radius

Extending themes

Use extends to start from a built-in or another custom theme and only override what you need.

json
{
  "mode": "dark",
  "extends": "nord",
  "colors": {
    "primary": "#88c0d0"
  }
}

Rules:

  • Custom theme fields always win over the theme they extend.
  • Custom β†’ custom chains are flattened when loading (depth limit of 10; cycles are ignored).
  • If extends is a built-in id, merging with that built-in’s defaults happens in the renderer.
  • If extends is omitted, the theme still merges onto Feishin’s shared default palette.

Built-in theme ids

Use these as extends values (same ids as in Settings):

ayuDark, ayuLight, catppuccinLatte, catppuccinMocha, defaultDark, defaultLight, dracula, everforestDark, everforestLight, githubDark, githubLight, glassyDark, gruvboxDark, gruvboxLight, highContrastDark, highContrastLight, materialDark, materialLight, monokai, nightOwl, nord, oneDark, rosePine, rosePineDawn, rosePineMoon, shadesOfPurple, solarizedDark, solarizedLight, tokyoNight, vscodeDarkPlus, vscodeLightPlus, zenburn


Linked stylesheets

For larger visual overrides (glass effects, layout tweaks, etc.), point stylesheets at CSS files inside the Themes folder:

json
{
  "mode": "dark",
  "extends": "defaultDark",
  "stylesheets": ["overrides/my-theme.css"]
}
text
Themes/
  my-theme.json
  overrides/
    my-theme.css

Notes:

  • Feishin watches JSON theme files for automatic reload. After editing only a CSS file, click Reload in Settings (or touch/save the .json file) so stylesheets are re-read.
  • Empty or unreadable stylesheet paths are skipped with a console warning.

Examples

Minimal accent-only dark theme

accent-blue.json:

json
{
  "mode": "dark",
  "extends": "defaultDark",
  "colors": {
    "primary": "rgb(80, 160, 255)"
  }
}

Full light palette

paper-light.json:

json
{
  "mode": "light",
  "colors": {
    "background": "rgb(250, 249, 246)",
    "background-alternate": "rgb(242, 240, 235)",
    "surface": "rgb(255, 255, 255)",
    "surface-foreground": "rgb(40, 40, 40)",
    "foreground": "rgb(30, 30, 30)",
    "foreground-muted": "rgb(110, 110, 110)",
    "primary": "rgb(180, 83, 9)",
    "black": "rgb(0, 0, 0)",
    "white": "rgb(255, 255, 255)",
    "state-error": "rgb(185, 28, 28)",
    "state-info": "rgb(37, 99, 235)",
    "state-success": "rgb(22, 163, 74)",
    "state-warning": "rgb(217, 119, 6)"
  },
  "app": {
    "overlay-header": "linear-gradient(rgb(250 249 246 / 50%) 0%, rgb(250 249 246 / 80%))",
    "scrollbar-handle-background": "rgba(120, 120, 120, 30%)"
  },
  "mantineOverride": {
    "primaryShade": {
      "light": 5
    }
  }
}

Theme that extends another custom theme

nord-soft.json:

json
{
  "mode": "dark",
  "extends": "nord",
  "colors": {
    "background": "rgb(36, 41, 51)",
    "surface": "rgb(46, 52, 64)"
  }
}

Troubleshooting

Symptom What to check
Theme missing from the list File must be .json in the Themes root; use Reload.
Theme shows an error in Settings JSON is invalid or not a top-level object. Fix the file and reload.
Theme loads with a warning One or more colors values were invalid and ignored.
CSS changes not applying Edit/save the .json or click Reload; stylesheet watch is tied to JSON changes.
Extending does nothing / looks wrong Confirm the extends id matches a built-in id or another custom filename (without .json). Avoid circular chains.

Broken themes still appear in Settings with their error message so you can fix them without digging through logs.


ENV SETTINGS (`docs/ENV_SETTINGS.md`)

Environment variables for settings (web / Docker)

These variables override app settings on first run when no persisted settings exist. They are injected via settings.js (from settings.js.template) and only apply to the web build.

Format: All values are strings; booleans use true/false, numbers are numeric strings. Leave unset or empty to use the default.


General

Setting Default Env variable Available values / Description
general.accent rgb(53, 116, 252) FS_GENERAL_ACCENT CSS rgb(r, g, b) string (e.g. rgb(53, 116, 252)). Invalid values are ignored.
general.albumBackground false FS_GENERAL_ALBUM_BACKGROUND true / false β€” Show album background image.
general.albumBackgroundBlur 3 FS_GENERAL_ALBUM_BACKGROUND_BLUR Blur amount for album background (number).
general.artistBackground true FS_GENERAL_ARTIST_BACKGROUND true / false β€” Show artist background image.
general.artistBackgroundBlur 3 FS_GENERAL_ARTIST_BACKGROUND_BLUR Blur amount for artist background (number).
general.blurExplicitImages false FS_GENERAL_BLUR_EXPLICIT_IMAGES true / false β€” Blur explicit images.
general.combinedLyricsAndVisualizer false FS_GENERAL_COMBINED_LYRICS_AND_VISUALIZER true / false β€” Combine lyrics and visualizer panel.
general.enableGridMultiSelect false FS_GENERAL_ENABLE_GRID_MULTI_SELECT true / false β€” Enable multi-select in grid views.
general.externalLinks true FS_GENERAL_EXTERNAL_LINKS true / false β€” Show external links in UI.
general.followCurrentSong true FS_GENERAL_FOLLOW_CURRENT_SONG true / false β€” Follow current song in list.
general.followSystemTheme false FS_GENERAL_FOLLOW_SYSTEM_THEME true / false β€” Use OS light/dark preference.
general.homeFeature true FS_GENERAL_HOME_FEATURE true / false β€” Show home featured carousel.
general.homeFeatureStyle single FS_GENERAL_HOME_FEATURE_STYLE multiple / single - Home featured carousel style.
general.imagePlaceholderPriority thumbhash FS_GENERAL_IMAGE_PLACEHOLDER_PRIORITY thumbhash / blurhash / dominantColor / off - Which image placeholder shows first while art loads; off disables placeholders.
general.language en FS_GENERAL_LANGUAGE UI language code (e.g. en, de, fr).
general.theme defaultDark FS_GENERAL_THEME One of: ayuDark, ayuLight, catppuccinLatte, catppuccinMocha, defaultDark, defaultLight, dracula, githubDark, githubLight, glassyDark, gruvboxDark, gruvboxLight, highContrastDark, highContrastLight, materialDark, materialLight, monokai, nightOwl, nord, oneDark, rosePine, rosePineDawn, rosePineMoon, shadesOfPurple, solarizedDark, solarizedLight, tokyoNight, vscodeDarkPlus, vscodeLightPlus.
general.themeDark defaultDark FS_GENERAL_THEME_DARK Same as theme (used when system is dark).
general.themeLight defaultLight FS_GENERAL_THEME_LIGHT Same as theme (used when system is light).
general.lastfmApiKey (empty) FS_GENERAL_LASTFM_API_KEY Last.fm API key.
general.lastFM true FS_GENERAL_LAST_FM true / false β€” Enable Last.fm.
general.listenBrainz true FS_GENERAL_LISTEN_BRAINZ true / false β€” ListenBrainz links.
general.musicBrainz true FS_GENERAL_MUSIC_BRAINZ true / false β€” MusicBrainz links.
general.nativeAspectRatio false FS_GENERAL_NATIVE_ASPECT_RATIO true / false β€” Use native cover art aspect ratio.
general.pathReplace (empty) FS_GENERAL_PATH_REPLACE Path pattern to replace (e.g. server path in Docker).
general.pathReplaceWith (empty) FS_GENERAL_PATH_REPLACE_WITH Replacement path.
general.playerbarOpenDrawer false FS_GENERAL_PLAYERBAR_OPEN_DRAWER true / false β€” Open queue/lyrics as drawer from player bar.
general.primaryShade 6 FS_GENERAL_PRIMARY_SHADE Mantine primary shade 0–9 (number).
general.qobuz true FS_GENERAL_QOBUZ true / false β€” Qobuz links.
general.resume true FS_GENERAL_RESUME true / false β€” Resume playback on load.
general.showLyricsInSidebar true FS_GENERAL_SHOW_LYRICS_IN_SIDEBAR true / false β€” Show lyrics in sidebar.
general.showRatings true FS_GENERAL_SHOW_RATINGS true / false β€” Show star ratings.
general.showVisualizerInSidebar true FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR true / false β€” Show visualizer in sidebar.
general.sidebarCollapsedNavigation true FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION true / false β€” Start with collapsed sidebar nav.
general.sidebarCollapseShared false FS_GENERAL_SIDEBAR_COLLAPSE_SHARED true / false β€” Share sidebar collapse state.
general.sidebarImageExpand false FS_GENERAL_SIDEBAR_IMAGE_EXPAND true / false β€” Show the player-bar album-art button that expands art into the sidebar.
general.sidebarPlaylistFolders true FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS true / false β€” Group playlists into folders by name separator.
general.sidebarPlaylistFolderSeparator / FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR Character or string that separates folder levels in a playlist name. Empty = use default.
general.sidebarPlaylistFolderTreeIndent 16 FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT Pixels each tree level is indented (0–64).
general.sidebarPlaylistFolderTreeLineColor (empty) FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR CSS color for tree connecting lines. Empty = theme default.
general.sidebarPlaylistFolderView tree FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW single / tree / navigation β€” How folders are displayed in the sidebar.
general.sidebarPlaylistList true FS_GENERAL_SIDEBAR_PLAYLIST_LIST true / false β€” Show playlist list in sidebar.
general.sidebarPlaylistMode expanded FS_GENERAL_SIDEBAR_PLAYLIST_MODE compact / expanded β€” Sidebar playlist row layout.
general.sidebarPlaylistSorting false FS_GENERAL_SIDEBAR_PLAYLIST_SORTING true / false β€” Enable playlist sorting in sidebar.
general.sideQueueType sideQueue FS_GENERAL_SIDE_QUEUE_TYPE sideDrawerQueue / sideQueue β€” Side play queue style.
general.sideQueueLayout horizontal FS_GENERAL_SIDE_QUEUE_LAYOUT horizontal / vertical β€” Attached side queue layout orientation.
general.useThemeAccentColor false FS_GENERAL_USE_THEME_ACCENT_COLOR true / false β€” Use theme’s accent color instead of custom.
general.useThemePrimaryShade true FS_GENERAL_USE_THEME_PRIMARY_SHADE true / false β€” Use theme’s primary shade.
general.zoomFactor 100 FS_GENERAL_ZOOM_FACTOR UI zoom percentage (number).

Playback

Setting path Default Env variable Available values / Description
playback.mediaSession false FS_PLAYBACK_MEDIA_SESSION true / false β€” Media Session API (e.g. browser/media keys).
playback.webAudio true FS_PLAYBACK_WEB_AUDIO true / false β€” Use Web Audio for playback.
playback.audioFadeOnStatusChange true FS_PLAYBACK_AUDIO_FADE_ON_STATUS_CHANGE true / false β€” Fade on play/pause.
playback.preservePitch true FS_PLAYBACK_PRESERVE_PITCH true / false β€” Preserve pitch when changing speed.
playback.scrobble.enabled true FS_PLAYBACK_SCROBBLE_ENABLED true / false β€” Enable scrobbling.
playback.scrobble.notify false FS_PLAYBACK_SCROBBLE_NOTIFY true / false β€” Scrobble notifications.
playback.scrobble.scrobbleAtDuration 240 FS_PLAYBACK_SCROBBLE_AT_DURATION Seconds of playback before scrobble.
playback.scrobble.scrobbleAtPercentage 75 FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE Percentage of track before scrobble.
playback.transcode.enabled false FS_PLAYBACK_TRANSCODE_ENABLED true / false β€” Enable transcoding.
playback.transcode.format (unset) FS_PLAYBACK_TRANSCODE_FORMAT Transcode format string (codec/container), e.g. server-specific value. Empty = use default.
playback.transcode.bitrate (unset) FS_PLAYBACK_TRANSCODE_BITRATE Transcode bitrate (number, kbps or as defined by server).
playback.filters [] FS_PLAYBACK_FILTERS JSON array of player filters: each object needs id, field, operator, value; optional isEnabled. Invalid JSON or shape is ignored.

Discord

Setting path Default Env variable Available values / Description
discord.enabled false FS_DISCORD_ENABLED true / false β€” Discord rich presence.
discord.clientId (built-in) FS_DISCORD_CLIENT_ID Custom Discord application ID.
discord.displayType feishin FS_DISCORD_DISPLAY_TYPE artist / feishin / song.
discord.linkType none FS_DISCORD_LINK_TYPE last_fm / musicbrainz / musicbrainz_last_fm / none.
discord.showAsListening false FS_DISCORD_SHOW_AS_LISTENING true / false.
discord.showPaused true FS_DISCORD_SHOW_PAUSED true / false β€” Show paused state.
discord.showServerImage false FS_DISCORD_SHOW_SERVER_IMAGE true / false.
discord.showStateIcon true FS_DISCORD_SHOW_STATE_ICON true / false.

Lyrics

Setting path Default Env variable Available values / Description
lyrics.fetch true FS_LYRICS_FETCH true / false β€” Fetch lyrics.
lyrics.follow true FS_LYRICS_FOLLOW true / false β€” Follow current line.
lyrics.delayMs 0 FS_LYRICS_DELAY_MS Sync delay in milliseconds.
lyrics.lineLeadTimeMs 800 FS_LYRICS_LINE_LEAD_TIME_MS Milliseconds before a line becomes active while following.
lyrics.followScrollAlignment 0 FS_LYRICS_FOLLOW_SCROLL_ALIGNMENT Vertical follow position. 0 centers the active line; negative moves it higher, positive moves it lower (-50 to 50).
lyrics.preferLocalLyrics true FS_LYRICS_PREFER_LOCAL true / false β€” Prefer local lyric files.
lyrics.showMatch true FS_LYRICS_SHOW_MATCH true / false.
lyrics.showProvider true FS_LYRICS_SHOW_PROVIDER true / false.
lyrics.enableAutoTranslation false FS_LYRICS_ENABLE_AUTO_TRANSLATION true / false.
lyrics.translationApiKey (empty) FS_LYRICS_TRANSLATION_API_KEY API key for lyric translation.
lyrics.translationTargetLanguage en FS_LYRICS_TRANSLATION_TARGET_LANGUAGE Target language code.
lyrics.alignment center FS_LYRICS_ALIGNMENT center / left / right.

Lyrics display

Applies to the default lyrics display profile (lyricsDisplay.default).

Setting path Default Env variable Available values / Description
lyricsDisplay.default.paddingLeft 0 FS_LYRICS_PADDING_LEFT Left content padding as a percentage (0–20).
lyricsDisplay.default.paddingRight 0 FS_LYRICS_PADDING_RIGHT Right content padding as a percentage (0–20).

Auto DJ

Setting path Default Env variable Available values / Description
autoDJ.albumStrategy similar FS_AUTO_DJ_ALBUM_STRATEGY similar / library_random.
autoDJ.allowDuplicates false FS_AUTO_DJ_ALLOW_DUPLICATES true / false β€” Allow songs or albums already in the queue to be added again.
autoDJ.enabled false FS_AUTO_DJ_ENABLED true / false.
autoDJ.itemCount 5 FS_AUTO_DJ_ITEM_COUNT Number of items to add.
autoDJ.mode songs FS_AUTO_DJ_MODE songs / albums.
autoDJ.onlySimilar false FS_AUTO_DJ_ONLY_SIMILAR true / false β€” Treat item count as a maximum; use the first non-empty source (similar, then genre, artist, or random) and do not fill from later sources.
autoDJ.songStrategy similar FS_AUTO_DJ_SONG_STRATEGY similar / library_random.
autoDJ.timing 1 FS_AUTO_DJ_TIMING Timing value (number).

CSS

Setting path Default Env variable Available values / Description
css.content (empty) FS_CSS_CONTENT Custom CSS string (sanitized like in-app custom CSS). Set FS_CSS_ENABLED=true to apply.
css.enabled false FS_CSS_ENABLED true / false β€” Enable custom CSS.

Font

Setting path Default Env variable Available values / Description
font.type builtIn FS_FONT_TYPE builtIn / system / custom.
font.builtIn Inter FS_FONT_BUILT_IN Built-in font name.
font.system (empty) FS_FONT_SYSTEM System font name (when type is system).

Agents Api (`docs/agents/api.md`)

API

Server library data layer (Jellyfin / Navidrome / Subsonic). Read when adding or changing fetches, mutations, normalize, or query keys.

Layers

Layer Path Job
Feature queries / mutations src/renderer/features/<feature>/api/, .../mutations/ queryOptions / useMutation; call api.controller
Facade src/renderer/api β†’ controller.ts Pick backend from current server (ServerType)
Backend client + controller src/renderer/api/{jellyfin,navidrome,subsonic}/ HTTP + map to controller endpoints
Types + normalize src/shared/api/{jellyfin,navidrome,subsonic}/ Zod/types + *Normalize β†’ domain
Domain contracts src/shared/types/domain-types.ts ControllerEndpoint shapes features rely on
Query keys src/renderer/api/query-keys.ts [serverId, resource, …]; use splitPaginatedQuery for list/count

React Query client: src/renderer/lib/react-query.ts.

Add a query

  1. Prefer extending the matching src/renderer/features/<feature>/api/*-api.ts (or add one beside siblings).
  2. Call api.controller.<method>(…) β€” do not invent one-off axios/fetch in UI.
  3. Keys from queryKeys β€” keep serverId first; invalidate the same keys from mutations.
  4. UI uses useQuery(<feature>Queries.…) like nearby features.
ts
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';

Normalize / new endpoint

  • Wire through the per-backend controller, not the feature component.
  • Parse/validate with shared types; map with jfNormalize / ndNormalize / ssNormalize in src/shared/api/....
  • Keep domain types stable for features; backend quirks stay in normalize/controller.

Upstream OpenAPI (last resort)

Do not fetch these for ordinary API work. Prefer in-repo controllers, src/shared/api/*-types.ts, normalize, and existing call sites.

Fetch only when an endpoint, field, or auth detail is missing or contradictory in-repo and you cannot finish without the vendor contract:

Backend Schema
OpenSubsonic (Subsonic / Navidrome-compatible) https://opensubsonic.netlify.app/docs/openapi/openapi.json
Navidrome native API https://github.com/navidrome/navidrome/tree/master/server/nativeapi
Jellyfin https://api.jellyfin.org/openapi/jellyfin-openapi-stable.json

When you must open one: pull only the path/operation/schema (or Go handler file) you need, not the whole spec or folder into context. Then encode the result in our types/normalize/controller β€” do not leave features depending on the remote source.

Don’t

  • Bypass api.controller from feature UI.
  • Put Jellyfin/Navidrome/Subsonic response shapes in React components β€” normalize first.
  • Hand-roll query key arrays when query-keys.ts already has a factory.
  • Open upstream OpenAPI β€œjust in case” or to explore β€” in-repo code first.

Agents Architecture (`docs/agents/architecture.md`)

Architecture

Process and build boundaries. Read when work crosses main / preload / renderer / remote / web, or touches window.api / IPC.

Layout

Tree Role
src/main Electron main: window lifecycle, OS features, core IPC (player, remote server, themes, lyrics, …)
src/preload contextBridge β†’ window.api (src/preload/index.ts)
src/renderer Full app UI (Electron renderer + web build root)
src/remote Remote-control SPA, served by the Electron remote feature
src/shared Cross-process UI, types, normalize, themes, utils β€” no process-specific imports
src/i18n Locales + i18n setup

Builds

Target Command / config Notes
Electron pnpm dev / build:electron (electron.vite.config.ts) main + preload + renderer
Web pnpm build:web (web.vite.config.ts) out/web; Docker uses this
Remote pnpm build:remote (remote.vite.config.ts) out/remote
Default package pnpm build electron + remote

Desktop-only surfaces (custom themes, mpv, MPRIS, many window.api.* modules) must gate on isElectron() and null-check window.api. See docs/CUSTOM_THEMES.md for themes.

Aliases

Alias β†’
/@/main src/main (Electron main; not in web/remote vite)
/@/preload src/preload
/@/renderer src/renderer
/@/shared src/shared
/@/i18n src/i18n
/@/remote src/remote

Import boundaries (culture β€” not ESLint)

  • main β†’ /@/main, /@/shared only.
  • preload β†’ preload + shared (plus the existing relative main env exception).
  • shared β†’ no /@/renderer, /@/main, /@/remote, /@/preload.
  • renderer β†’ /@/renderer, /@/shared, /@/i18n β€” not /@/main.
  • remote β†’ /@/remote, /@/shared; may reuse selected /@/renderer utilities (theme, logger) β€” do not grow that into a full renderer dependency.

Electron capabilities from the UI: window.api.* (typed in src/preload/index.d.ts), never direct main imports.

  • UI / CSS / i18n patterns: docs/agents/frontend.md
  • Server library API: docs/agents/api.md
  • Logging: docs/agents/logging.md

Agents Commits (`docs/agents/commits.md`)

Commits

Conventional Commits for history that can feed release notes and scoped changelogs. Enforced by commitlint (commitlint.config.mjs) and the Test workflow.

Automatic Weblate translation commits (subject Translated using Weblate) are skipped via the config's ignores list, since they do not follow the conventional format.

Format

text
type(scope): subject

[optional body]

[optional footer]

Examples:

  • feat(player): add MPV reload control
  • fix(window): restore minimized window
  • chore(deps): bump electron
  • feat!: drop legacy auth path or footer BREAKING CHANGE: …

Types

Use the conventional set (from @commitlint/config-conventional):

feat Β· fix Β· perf Β· refactor Β· docs Β· style Β· test Β· build Β· ci Β· chore Β· revert

Prefer feat / fix / perf for user-facing notes; keep chore/build/ci out of highlight sections when generating notes.

Scopes

Scope = product area a release-note reader cares about β€” not Electron process (main / preload) and not β€œsettings” (scope the feature the setting belongs to).

Scope is optional. When present, it must be one of (see commitlint.config.mjs β€” source of truth):

Product

Scope Use for
player Queue, engines, mpv/web, playerbar, now-playing playback (any process)
radio Radio stations / radio playback
lyrics Lyrics fetch, display, lyrics-specific options
library Albums, artists, songs, folders, genres, favorites, search, playlists
servers Server list, login, connection
remote Remote-control SPA and remote server
theme Themes, CSS variables, custom themes
ui Pages, styling, and shared UI with no single feature owner
visualizer Butterchurn / audio-motion visualizers
window Titlebar, minimize/restore, fullscreen chrome
sharing Shares
tag-editor Tag editing

Meta

Scope Use for
api Multi-backend controller / normalize / query keys with no single feature owner
i18n Locales / Weblate
ci GitHub Actions / workflows
deps Dependency bumps
release Versioning, changelogs, publish / packaging config

Add a new scope in commitlint.config.mjs first, then document it here.

Subject

  • Imperative, present tense (β€œadd”, not β€œadded”)
  • No trailing period
  • Do not put agent attribution in the message (see AGENTS.md)

Local check

bash
pnpm exec commitlint --last
# or
echo "feat(player): your subject" | pnpm exec commitlint

CI runs commitlint over the PR or push range (see .github/workflows/test.yml).


Agents Domain (`docs/agents/domain.md`)

Domain Docs

How the engineering skills should consume this repo's domain documentation when exploring the codebase.

Before exploring, read these

  • CONTEXT.md at the repo root, or
  • CONTEXT-MAP.md at the repo root if it exists β€” it points at one CONTEXT.md per context. Read each one relevant to the topic.
  • docs/adr/ β€” read ADRs that touch the area you're about to work in. In multi-context repos, also check src/<context>/docs/adr/ for context-scoped decisions.

If any of these files don't exist, proceed silently. Don't flag their absence; don't suggest creating them upfront. The /domain-modeling skill (reached via /grill-with-docs and /improve-codebase-architecture) creates them lazily when terms or decisions actually get resolved.

File structure

Single-context repo (most repos):

text
/
β”œβ”€β”€ CONTEXT.md
β”œβ”€β”€ docs/adr/
β”‚   β”œβ”€β”€ 0001-event-sourced-orders.md
β”‚   └── 0002-postgres-for-write-model.md
└── src/

Multi-context repo (presence of CONTEXT-MAP.md at the root):

text
/
β”œβ”€β”€ CONTEXT-MAP.md
β”œβ”€β”€ docs/adr/                          ← system-wide decisions
└── src/
    β”œβ”€β”€ ordering/
    β”‚   β”œβ”€β”€ CONTEXT.md
    β”‚   └── docs/adr/                  ← context-specific decisions
    └── billing/
        β”œβ”€β”€ CONTEXT.md
        └── docs/adr/

Use the glossary's vocabulary

When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in CONTEXT.md. Don't drift to synonyms the glossary explicitly avoids.

If the concept you need isn't in the glossary yet, that's a signal β€” either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for /domain-modeling).

Flag ADR conflicts

If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:

Contradicts ADR-0007 (event-sourced orders) β€” but worth reopening because…


Agents Frontend (`docs/agents/frontend.md`)

Frontend

Conventions for UI in the renderer: imports, composition, CSS Modules, theme tokens, i18n, icons, toasts, modals, Zustand state.

Imports

Use Path
Shared UI / hooks / theme /@/shared/... β†’ src/shared/...
Features / layouts / stores /@/renderer/... β†’ src/renderer/...
i18n /@/i18n/... or react-i18next / i18next as nearby files do

Deep imports only β€” no component barrels. Example: /@/shared/components/button/button.

Mantine: feature UI imports wrappers under /@/shared/components and /@/shared/hooks, not @mantine/core. Open docs/agents/mantine.md only when debugging those wrappers or adding a new one.

Exception: @mantine/modals (openModal, openContextModal, closeModal, …) is the usual imperative API in features β€” match nearby callers.

Composition ladder

  1. Shared wrapper β€” /@/shared/components/... (design-system primitive).
  2. Feature-shared β€” src/renderer/features/shared/components/ (domain chrome reused across features).
  3. Feature-local β€” src/renderer/features/<feature>/components/ (one feature only).

Layout primitives: Box, Flex, Stack, Group, Grid from /@/shared/components/.... Prefer them over one-off layout divs when composing screens.

Put a new primitive in shared only when it is generic design-system; otherwise keep it feature-local or feature-shared.

CSS Modules

  • Co-locate component-name.module.css next to component-name.tsx (kebab folder + file names).
  • Class names in CSS are kebab-case; Vite maps them to camelCase on styles (.player-button β†’ styles.playerButton).
  • Compose classes with clsx. On Mantine wrappers, pass module classes through the Styles API (classNames={{ root: styles.root, ... }}).
  • Colors, spacing, chrome: prefer var(--theme-…) / var(--theme-colors-…) over raw hex or --mantine-* so custom themes keep working.
  • PostCSS: @mixin light-root / dark-root where light/dark splits belong; lighten / darken / alpha are allowed by stylelint.

Global CSS is rare (src/shared/styles/global.css and third-party/theme overrides) β€” don’t add feature styles there.

Theme tokens

Layer Source
Types / shape src/shared/themes/app-theme-types.ts
Built-in themes src/shared/themes/*, registry app-theme.ts
Runtime CSS variables injected as --theme-* / --theme-colors-* (see use-app-theme)
Mantine theme object src/renderer/themes/mantine-theme.tsx
Bridge to Mantine vars src/shared/styles/global.css

User-loadable desktop theme JSON (colors / app / mantineOverride / stylesheets): docs/CUSTOM_THEMES.md β€” only when changing that surface.

i18n, icons, toasts, modals

  • Copy: useTranslation / t; keys in src/i18n/locales/en.json (common.*, form.*, error.*, page.*, entity.*, …). Add keys next to existing namespaces.
  • Icons: `` from /@/shared/components/icon/icon. New glyphs go on the AppIcon map there β€” don’t import react-icons / Lucide directly in features.
  • Toasts: toast.success|error|info|warn({ message, title? }) from /@/shared/components/toast/toast.
  • Modals: open/close via @mantine/modals; render with shared Modal / ModalButton / provider patterns already in tree. Controlled visibility often uses /@/shared/hooks/use-disclosure.

State (Zustand)

Scope Where
App-wide src/renderer/store/ β€” auth, settings, player, app chrome, etc. Prefer the barrel src/renderer/store when the store is re-exported; otherwise deep-import the *.store.ts file (e.g. custom-themes.store.ts).
Feature src/renderer/features/<feature>/store/ (or a hook-colocated store) when state is feature-owned
Remote SPA src/remote/store β€” remote session / WebSocket only; not the library api.controller

Patterns to match nearby stores:

  • Prefer createWithEqualityFn from zustand/traditional.
  • Persisted app stores often use persist + immer (+ devtools / subscribeWithSelector when siblings do).
  • Multi-field reads: useShallow or the store’s exported selector hooks; see src/renderer/lib/zustand.ts (createSelectors) where used.
  • Don’t duplicate player/auth/settings into a new global store β€” extend the existing one or keep a feature-local store.

Agents Issue Tracker (`docs/agents/issue-tracker.md`)

Issue tracker: Local Markdown

Issues and specs for this repo live as markdown files in .scratch/.

Conventions

  • One feature per directory: .scratch/<feature-slug>/
  • The spec is .scratch/<feature-slug>/spec.md
  • Implementation issues are one file per ticket at .scratch/<feature-slug>/issues/<NN>-<slug>.md, numbered from 01 β€” never a single combined tickets file
  • Triage state is recorded as a Status: line near the top of each issue file (see triage-labels.md for the role strings)
  • Comments and conversation history append to the bottom of the file under a ## Comments heading

When a skill says "publish to the issue tracker"

Create a new file under .scratch/<feature-slug>/ (creating the directory if needed).

When a skill says "fetch the relevant ticket"

Read the file at the referenced path. The user will normally pass the path or the issue number directly.

Wayfinding operations

Used by /wayfinder. The map is a file with one child file per ticket.

  • Map: .scratch/<effort>/map.md β€” the Notes / Decisions-so-far / Fog body.
  • Child ticket: .scratch/<effort>/issues/NN-<slug>.md, numbered from 01, with the question in the body. A Type: line records the ticket type (research/prototype/grilling/task); a Status: line records claimed/resolved.
  • Blocking: a Blocked by: NN, NN line near the top. A ticket is unblocked when every file it lists is resolved.
  • Frontier: scan .scratch/<effort>/issues/ for files that are open, unblocked, and unclaimed; first by number wins.
  • Claim: set Status: claimed and save before any work.
  • Resolve: append the answer under an ## Answer heading, set Status: resolved, then append a context pointer (gist + link) to the map's Decisions-so-far in map.md.

Agents Logging (`docs/agents/logging.md`)

Logging

Use the process logger for diagnosable failures and meaningful state transitions. Skip noise (render churn, routine success paths, prop dumps).

Which logger

Process Import API
Renderer / remote import { logger } from '/@/renderer/utils/logger' logger.debug|info|warn|error(message, meta?)
Main import log from '/@/main/logger' log.debug|info|warn|error(...)

Do not use bare console.* for new logging in app code β€” the loggers format, debounce (renderer), forward to electron-log / files, and respect the configured level.

When to log

Add a log when the code path is hard to reconstruct from the UI alone:

  • Failures and catch blocks that would otherwise vanish (error / warn)
  • Engine / connection / auth / playback transitions that change behavior (info or debug)
  • Fallback paths (e.g. playback engine fell back to web) (warn / info)

Skip: hot loops, every React render, expected empty states, data the user already sees as a toast unless the log adds process-side detail.

Levels

Configured threshold is debug | info (LogLevel in /@/shared/logger/types). Message severity is debug | info | warn | error.

Severity Use for
error Failed operation; include the error in meta or args
warn Degraded / unexpected but continuing
info Notable lifecycle (engine chosen, reconnect, update check)
debug Detail useful when digging (payload shapes, step traces) β€” gated off unless level is debug

Message shape

  • First arg: short human message string.
  • Optional second arg (renderer): structured meta object β€” prefer { error, … } over string-concatenating errors.
  • No secrets: passwords, tokens, full auth headers, or anything sanitize-for-diagnostics would strip from exports. Main diagnostic packaging uses /@/shared/utils/sanitize-for-diagnostics β€” keep ad-hoc logs equally clean.

Agents Mantine (`docs/agents/mantine.md`)

Mantine

Feishin wraps the Mantine components and hooks it uses under src/shared/components/ and src/shared/hooks/. Feature code imports those wrappers (/@/shared/...), not @mantine/*.

Default (most UI work)

  1. Import from /@/shared/components/... and /@/shared/hooks/....
  2. Match callers and props already in use β€” the wrapper is the API.
  3. Do not fetch Mantine LLM docs, MCP, or upstream skills.

@mantine/* belongs only inside the wrapper modules (and a few shared theme/type utilities).

When to open upstream docs

Reach for live Mantine docs only if you are:

  • Debugging a wrapper β€” behavior/bug lives in src/shared/components/* or src/shared/hooks/* (or adjacent shared Mantine adapters), or
  • Adding a new wrapped component or hook β€” introducing a new /@/shared/... re-export that did not exist.

Then:

  1. Confirm Mantine v9 in package.json.
  2. Prefer Mantine MCP if configured (search_docs, get_item_doc, get_item_props); else https://mantine.dev/llms.txt β†’ the linked per-page .md for that item only.
  3. Do not vendor llms-full.txt.

Skill branches (new wrappers only)

Install from mantinedev/skills only when scaffolding a matching new wrapper:

New wrapper kind Skill
@mantine/form / validation / form context mantine-form
Custom select via Combobox mantine-combobox
Factory + Styles API component mantine-custom-components

Repo theme notes

User-loadable desktop theme JSON and mantineOverride: docs/CUSTOM_THEMES.md β€” only when changing that behavior or theme-object shape.


Agents Triage Labels (`docs/agents/triage-labels.md`)

Triage Labels

The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.

Label in mattpocock/skills Label in our tracker Meaning
needs-triage needs-triage Maintainer needs to evaluate this issue
needs-info needs-info Waiting on reporter for more information
ready-for-agent ready-for-agent Fully specified, ready for an AFK agent
ready-for-human ready-for-human Requires human implementation
wontfix wontfix Will not be actioned

When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.

Edit the right-hand column to match whatever vocabulary you actually use.


--- METRICS ---