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
Desktop (recommended)
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:
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:
dir=/your/application/directory
curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir" wayland-nativeIt also provides a simple uninstall routine, removing the downloaded files:
dir=/your/application/directory
curl 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/install-feishin-appimage' | sh -s -- "$dir" removeThe 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:
# 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 feishinDocker Compose
To install via Docker Compose, use the following snippet. This also works on Portainer.
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:8190Configuration
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.
After restarting the app, you will be prompted to select a server. Click the
Open menubutton and selectManage servers. Click theAdd serverbutton 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.comorhttp://192.168.0.1:4533).
- Navidrome - For the best experience, select "Save password" when creating the server and configure the
SessionTimeoutsetting in your Navidrome config to a larger value (e.g. 72h).- Linux users - The default password store uses
libsecret.kwallet4/5/6are also supported, but must be explicitly set in Settings > Window > Passwords/secret store.
- Linux users - The default password store uses
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 inPUBLIC_PATH=/feishin.Optional - To hard code the server url, pass the following environment variables:
SERVER_NAME,SERVER_TYPE(one ofjellyfinornavidromeorsubsonic),SERVER_URL. To prevent users from changing these settings, passSERVER_LOCK=true. This can only be set if all three of the previous values are set. WhenSERVER_LOCK=true, you can also setLEGACY_AUTHENTICATION=trueorLEGACY_AUTHENTICATION=falseto configure the legacy authentication flag for the server (only applicable for Subsonic/OpenSubsonic servers).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, setREMOTE_URLto said public-facing URL.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.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.
OpenSubsonic compatible servers, such as...
-
- Feishin fork by lux032 - Plex is not natively supported. Use the fork by lux032 to use Plex with Feishin.
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.
chmod 4755 chrome-sandbox
sudo chown root:root chrome-sandboxUbuntu 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 serverpnpm run dev:watch- Start the development server in watch mode (for main / preload HMR)pnpm run start- Starts the app in production preview modepnpm run build- Builds the app for desktoppnpm 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 projectpnpm run package:dev- Package the project for development locallypnpm run package:linux- Package the project for Linux locallypnpm run package:mac- Package the project for Mac locallypnpm run package:win- Package the project for Windows locallypnpm run publish:linux- Publish the project for Linuxpnpm run publish:linux:beta- Publish the project for Linux (beta channel)pnpm run publish:linux-arm64- Publish the project for Linux ARM64pnpm run publish:linux-arm64:beta- Publish the project for Linux ARM64 (beta channel)pnpm run publish:mac- Publish the project for Macpnpm run publish:mac:beta- Publish the project for Mac (beta channel)pnpm run publish:win- Publish the project for Windowspnpm run publish:win:beta- Publish the project for Windows (beta channel)pnpm run typecheck- Type check the projectpnpm run typecheck:node- Type check the project with tsconfig.node.jsonpnpm run typecheck:web- Type check the project with tsconfig.web.jsonpnpm run lint- Lint the projectpnpm run lint:fix- Lint the project and fix linting errorspnpm 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
/caveman-commitAlso triggers on phrases like "write a commit", "commit message", "generate commit".
Example output
Diff: new endpoint for user profile.
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 #128Diff: breaking API rename.
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
SKILL.mdβ full LLM-facing instructions- Caveman README β repo overview
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
/caveman-reviewAlso triggers on "review this PR", "code review", "review the diff".
Example output
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
SKILL.mdβ full LLM-facing instructions- Caveman README β repo overview
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
/caveman-commitAlso triggers on phrases like "write a commit", "commit message", "generate commit".
Example output
Diff: new endpoint for user profile.
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 #128Diff: breaking API rename.
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
SKILL.mdβ full LLM-facing instructions- Caveman README β repo overview
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
/caveman-reviewAlso triggers on "review this PR", "code review", "review the diff".
Example output
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
SKILL.mdβ full LLM-facing instructions- Caveman README β repo overview
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
- Open Settings β General β Theme.
- Under Custom Themes, click Open Folder to reveal the Themes directory.
- Add a
.jsonfile (for examplemy-theme.json). - 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.
{
"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.
{
"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
extendsis a built-in id, merging with that built-inβs defaults happens in the renderer. - If
extendsis 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:
{
"mode": "dark",
"extends": "defaultDark",
"stylesheets": ["overrides/my-theme.css"]
}Themes/
my-theme.json
overrides/
my-theme.cssNotes:
- Feishin watches JSON theme files for automatic reload. After editing only a CSS file, click Reload in Settings (or touch/save the
.jsonfile) 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:
{
"mode": "dark",
"extends": "defaultDark",
"colors": {
"primary": "rgb(80, 160, 255)"
}
}Full light palette
paper-light.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:
{
"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
- Prefer extending the matching
src/renderer/features/<feature>/api/*-api.ts(or add one beside siblings). - Call
api.controller.<method>(β¦)β do not invent one-off axios/fetch in UI. - Keys from
queryKeysβ keepserverIdfirst; invalidate the same keys from mutations. - UI uses
useQuery(<feature>Queries.β¦)like nearby features.
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/ssNormalizeinsrc/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.controllerfrom feature UI. - Put Jellyfin/Navidrome/Subsonic response shapes in React components β normalize first.
- Hand-roll query key arrays when
query-keys.tsalready 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,/@/sharedonly. - 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/@/rendererutilities (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.
Related
- 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
type(scope): subject
[optional body]
[optional footer]Examples:
feat(player): add MPV reload controlfix(window): restore minimized windowchore(deps): bump electronfeat!: drop legacy auth pathor footerBREAKING 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
pnpm exec commitlint --last
# or
echo "feat(player): your subject" | pnpm exec commitlintCI 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.mdat the repo root, orCONTEXT-MAP.mdat the repo root if it exists β it points at oneCONTEXT.mdper 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 checksrc/<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):
/
βββ 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):
/
βββ 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
- Shared wrapper β
/@/shared/components/...(design-system primitive). - Feature-shared β
src/renderer/features/shared/components/(domain chrome reused across features). - 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.cssnext tocomponent-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-rootwhere light/dark splits belong;lighten/darken/alphaare 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 insrc/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 theAppIconmap there β donβt importreact-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 sharedModal/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
createWithEqualityFnfromzustand/traditional. - Persisted app stores often use
persist+immer(+devtools/subscribeWithSelectorwhen siblings do). - Multi-field reads:
useShallowor the storeβs exported selector hooks; seesrc/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 from01β never a single combined tickets file - Triage state is recorded as a
Status:line near the top of each issue file (seetriage-labels.mdfor the role strings) - Comments and conversation history append to the bottom of the file under a
## Commentsheading
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 from01, with the question in the body. AType:line records the ticket type (research/prototype/grilling/task); aStatus:line recordsclaimed/resolved. - Blocking: a
Blocked by: NN, NNline near the top. A ticket is unblocked when every file it lists isresolved. - Frontier: scan
.scratch/<effort>/issues/for files that are open, unblocked, and unclaimed; first by number wins. - Claim: set
Status: claimedand save before any work. - Resolve: append the answer under an
## Answerheading, setStatus: resolved, then append a context pointer (gist + link) to the map's Decisions-so-far inmap.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 (
infoordebug) - 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
metaobject β prefer{ error, β¦ }over string-concatenating errors. - No secrets: passwords, tokens, full auth headers, or anything
sanitize-for-diagnosticswould 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)
- Import from
/@/shared/components/...and/@/shared/hooks/.... - Match callers and props already in use β the wrapper is the API.
- 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/*orsrc/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:
- Confirm Mantine v9 in
package.json. - Prefer Mantine MCP if configured (
search_docs,get_item_doc,get_item_props); else https://mantine.dev/llms.txt β the linked per-page.mdfor that item only. - 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 ---
- Files Extracted: 16
- Estimated Token Budget: ~15346 tokens
- Canonical Reference: https://codewiki.google/github.com/jeffvli/feishin