{"owner":"nukeop","repo":"nuclear","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"skills":{"AGENTS.md":"# AGENTS.md\n\nGuidelines for AI coding agents working on Nuclear.\n\n## Project Overview\n\nNuclear is a free, open-source music player without ads or tracking. Search for any song or artist, build playlists, and start listening. It's a desktop app built with Tauri (Rust + React), organized as a pnpm monorepo managed with Turborepo.\n\n### Packages\n\n- `@nuclearplayer/player` - Main Tauri app (React + Rust)\n- `@nuclearplayer/ui` - Shared UI components\n- `@nuclearplayer/plugin-sdk` - Plugin system (published to npm)\n- `@nuclearplayer/model` - Data model\n- `@nuclearplayer/themes` - Theming system\n- `@nuclearplayer/hifi` - Advanced HTML5 audio component\n- `@nuclearplayer/tailwind-config` - Shared Tailwind config\n- `@nuclearplayer/eslint-config` - Shared linting rules\n- `@nuclearplayer/i18n` - Internationalization\n- `@nuclearplayer/storybook` - Component demos\n- `@nuclearplayer/tools` - Build and maintenance utilities\n- `@nuclearplayer/docs` - Documentation\n- `@nuclearplayer/website` - Project website (Astro)\n\n## Commands\n\n```bash\n# Development\npnpm dev                    # Run player in dev mode\npnpm dev:remote             # Same, but binds Vite to 0.0.0.0 so the remote control UI is reachable from other devices\npnpm storybook              # Run Storybook\n\n# Build\npnpm build                  # Build all packages\npnpm tauri build            # Build Tauri app\n\n# Quality\npnpm lint                   # Lint all packages\npnpm lint:fix               # Lint and auto-fix\npnpm type-check             # TypeScript checks\npnpm test                   # Run all tests\npnpm test:coverage          # Run tests with coverage\npnpm clean                  # Clean build artifacts\n\n# Package-specific testing\npnpm --filter @nuclearplayer/ui test -- src/components/Badge/Badge.test.tsx\npnpm --filter @nuclearplayer/ui test -- --testNamePattern=\"renders\"\n\n# Update snapshots (run at root for all, or filter to a specific package)\n\n# At root\npnpm test -- -u\n\n# Filtering for a specific package\npnpm --filter @nuclearplayer/ui test -- -u\n\n# After cd'ing into a package\npnpm test -u\n```\n\n## Code Style\n\n### General Principles\n\n- Prioritize readability over cleverness\n- No comments in code - explain reasoning in chat/commits\n- Avoid premature abstractions - start concrete, extract later\n- Small, focused changes over large dumps\n- Never commit unless explicitly asked\n\n### TypeScript\n\n- Use `type` not `interface` (except when merging is required)\n- No magic numbers - extract into named constants\n- Strict mode with `noUnusedLocals` and `noUnusedParameters`\n- Do not use one-letter variable names.\nAVOID: `(b) => b.buildIndexEntry()`\nPREFER: `(build) => build.buildIndexEntry()`\n\n### React Components\n\n```tsx\nimport { cva, VariantProps } from 'class-variance-authority';\nimport { ComponentProps, FC } from 'react';\n\nimport { cn } from '../../utils';\n\nconst componentVariants = cva('base-classes', {\n  variants: { /* ... */ },\n  defaultVariants: { /* ... */ },\n});\n\ntype ComponentProps = ComponentProps<'div'> &\n  VariantProps<typeof componentVariants>;\n\nexport const Component: FC<ComponentProps> = ({\n  className,\n  variant,\n  ...props\n}) => (\n  <div className={cn(componentVariants({ variant, className }))} {...props} />\n);\n```\n\n- Use `const Component: FC<Props>` not `function Component()`\n- Compound components (`Component.Sub`) for complex widgets\n- Keep business logic out of UI components\n\n### Adding UI Components\n\nWhen adding a new component to `@nuclearplayer/ui`:\n\n1. Create component directory: `packages/ui/src/components/MyComponent/`\n   - `MyComponent.tsx` - implementation\n   - `MyComponent.test.tsx` - tests (aim for 100% coverage)\n   - `index.ts` - re-exports\n2. Export from `packages/ui/src/components/index.ts`\n3. Add Storybook story in `packages/storybook/src/MyComponent.stories.tsx`\n4. Include snapshot test(s) covering all variants\n\n### Styling (Tailwind v4)\n\n- CSS-first config in `packages/tailwind-config/global.css`\n- Theme colors: `bg-background`, `text-foreground`, `bg-primary`\n- Accents: `accent-green`, `accent-yellow`, `accent-purple`, `accent-blue`, `accent-orange`, `accent-cyan`, `accent-red`\n- Use `cn()` for conditional classes, `cva()` for variants\n\n### State Management\n\n- **Zustand** - persistent UI state\n- **React state** - local, temporary state\n- **TanStack Query v5** - HTTP requests/server state\n- **TanStack Router** - client-side routing\n\n### Standardized Libraries\n\n- **Icons**: Lucide React (not heroicons, not font-awesome)\n- **Toasts**: Sonner\n- **Dates**: Luxon\n- **Utilities**: lodash-es (use individual imports: `import isEqual from 'lodash-es/isEqual'`)\n- **HTTP**: Native fetch via ApiClient base class (no axios)\n\n### Adding New Domains\n\nA \"domain\" is a feature area exposed to plugins (e.g., settings, queue, favorites). When adding a new domain:\n\n1. **Types** (`packages/plugin-sdk/src/types/myDomain.ts`)\n   - Define the `MyDomainHost` interface (the contract between player and SDK)\n   - Export any related types plugins will use\n\n2. **API class** (`packages/plugin-sdk/src/api/myDomain.ts`)\n   - Create a class that wraps the host and exposes methods to plugins\n   - Add to `NuclearAPI` constructor in `packages/plugin-sdk/src/api/index.ts`\n\n3. **Store** (`packages/player/src/stores/myDomainStore.ts`)\n   - Zustand store holding the domain state\n   - Persists to disk via `@tauri-apps/plugin-store` if needed\n\n4. **Host** (`packages/player/src/services/myDomainHost.ts`)\n   - Implements the `MyDomainHost` interface\n   - Bridges the SDK API to the Zustand store\n   - Passed to `NuclearAPI` when initializing plugins\n\n### External API Clients\n\nLive in `packages/player/src/apis/`. Use `ApiClient` base class (fetch→json→Zod).\n\n- Validate external data with Zod schemas\n- Export singleton instances\n- One class per external service\n\n### Internationalization\n\nAll user-facing strings go through i18n - no hardcoded UI text.\n\n```tsx\nimport { useTranslation } from '@nuclearplayer/i18n';\n\nconst { t } = useTranslation();\n<span>{t('navigation.settings')}</span>\n```\n\nAdd new strings to `packages/i18n/src/locales/en_US.json` only. Other locales come from Crowdin.\n\n## Testing\n\nTests use Vitest + React Testing Library. Globals enabled (`describe`, `it`, `expect`, `vi`).\n\n- Integration tests over unit tests for user-facing behavior. Render real components and assert on DOM content rather than verifying mock calls.\n- Unit tests for utilities - standalone data structures (RingBuffer, parsers) deserve isolated tests. Use them sparingly.\n- Test user behavior, not implementation details\n- Minimize mocks - only mock external deps (HTTP, FS, Tauri)\n- Snapshot tests: prefix with `(Snapshot)`, basic rendering only\n- Never use `querySelector` in tests. Prefer RTL queries.\n- When semantic queries aren't possible, add `data-testid` attributes. And don't be shy with them\n- Don't use defensive measures like try-catch or conditional checks in tests. The test will fail anyway if our assumptions are wrong.\n\n### Test-first for views\n\nWhen building a new view, write the test wrapper and tests **before** any implementation code. The tests describe what the user sees and does — they define the contract. Then implement to make them pass.\n\nDon't start with unit tests for internal utilities (grouping functions, registries, etc.). Start from the outside: what does the user see on the page? The internal structure is an implementation detail that falls out of making the tests green.\n\n### Test Wrappers for Views\n\nPlayer views and some components use a `*.test-wrapper.tsx` file that creates a domain-specific abstraction layer over the DOM. This lets tests read like user stories, and if the implementation changes, only the wrapper needs updating.\n\n**Wrapper conventions:**\n- Use **getters** for element queries: `get emptyState()`, `get cards()` — not `getEmptyState()`\n- Use **nested objects** for interactive elements: `createButton: { get element(), async click() }`\n- Use **methods** for multi-step user actions: `async openContextMenu(title: string)`\n- Tests should use `Wrapper.emptyState`, `Wrapper.cards`, `Wrapper.createButton.click()` — not bare `screen` queries\n- The wrapper is the only place that knows about test IDs, roles, and DOM structure\n- Don't use queryX methods in the wrapper - always get or find as appropriate.\n- Never use fireEvent. Always use userEvent for interactions.\n\n### Test fixtures\n\nTo populate the app with testing data, use fixtures. See `packages/player/src/test/fixtures` for examples.\n\n### Wrapper fixtures\n\nTest wrappers can expose a `fixtures` object with factory methods that return pre-configured builders for common test scenarios. This keeps test setup readable and co-located with the wrapper, while the raw fixture data itself lives in `packages/player/src/test/fixtures/`.\n\n```tsx\n// Dashboard.test-wrapper.tsx\nimport { TOP_TRACKS_RADIOHEAD } from '../../test/fixtures/dashboard';\n\nexport const DashboardWrapper = {\n  // ... mount, getters, etc.\n\n  fixtures: {\n    topTracksProvider() {\n      return new DashboardProviderBuilder()\n        .withCapabilities('topTracks')\n        .withFetchTopTracks(async () => TOP_TRACKS_RADIOHEAD);\n    },\n  },\n};\n\n// Dashboard.test.tsx\nDashboardWrapper.seedProvider(DashboardWrapper.fixtures.topTracksProvider());\n```\n\n### The builder pattern for tests\n\nWe use builders to create test data and various entities cleanly. You can see them in `packages/player/src/test/builders`.\n\n- A builder is a class that has an instance of the object it's building\n- When the builder is instantiated, it creates a default object with reasonable defaults\n- The builder has methods that mutate the object and return `this` for chaining\n- The `build()` method returns the final object, which can then be used in tests\n\n```tsx\n// Playlists.test-wrapper.tsx\nexport const PlaylistsWrapper = {\n  async mount(): Promise<RenderResult> { /* ... */ },\n\n  get emptyState() {\n    return screen.queryByTestId('empty-state');\n  },\n  get cards() {\n    return screen.queryAllByTestId('card');\n  },\n\n  createButton: {\n    get element() {\n      return screen.getByTestId('create-playlist-button');\n    },\n    async click() {\n      await userEvent.click(this.element);\n    },\n  },\n};\n\n// Playlists.test.tsx — reads like a user story\nit('shows empty state when no playlists', async () => {\n  await PlaylistsWrapper.mount();\n  expect(PlaylistsWrapper.emptyState).toBeInTheDocument();\n});\n```\n\n## File Organization\n\n```\npackages/ui/src/components/Badge/\n  Badge.tsx           # Implementation\n  Badge.test.tsx      # Tests\n  index.ts            # Re-exports\n  __snapshots__/      # Vitest snapshots\n```\n\n## Rust Backend\n\nThe Tauri backend lives in `packages/player/src-tauri/src/`. Modules:\n\n- `bridge/` - bidirectional RPC. Lets Rust servers call into the frontend (`Bridge::call` emits a `bridge:request` event, frontend replies via the `bridge_respond` command).\n- `http_api/` - Axum REST + SSE server for Nuclear Jam remote control\n- `mcp/` - MCP server exposing player functions as tools to LLM clients\n- `mpd/` - MPD-protocol TCP server for clients like ncmpcpp\n- `stream_server.rs` - local audio proxy adding CORS + Range so the browser can play blocked streams\n- `http.rs` - `http_fetch` command, a CORS-bypassing HTTP proxy for the frontend\n- `ytdlp.rs` / `ytdlp_setup.rs` - yt-dlp subprocess wrapper; auto-downloads the binary\n- `discord.rs` - Discord Rich Presence\n- `commands.rs` - filesystem helpers (zip, download, flatpak detection)\n- `net.rs`, `setup.rs`, `logging.rs` - port binding, log plugin config, startup log ring buffer\n- `lib.rs` - declares modules, registers all commands, runs `init_*` in `.setup()`. `main.rs` - binary entry, env fixups.\n\nCommands (run from `packages/player/src-tauri/`):\n\n```bash\ncargo test          # Rust tests (no clippy/rustfmt configured; run manually)\n```\n\n`pnpm dev` runs `tauri dev`; `pnpm build` (in `packages/player`) runs the full `tauri build`.\n\n## Design Philosophy\n\n- Neo-brutalist with premium polish - bold borders, purposeful shadows\n- Premium, designed feel\n- Animations via `motion` and `tw-animate-css`\n- Disable animations during high-friction moments (resize, drag)\n- Avoid generic AI patterns (icon-grid cards, stock heroes, \"Built with love\" badges)\n\n## Tooling Notes\n\n- **pnpm** with workspace protocol for internal deps\n- **Turborepo** for task orchestration\n- **ESLint + Prettier** run together\n- **Husky + lint-staged** for pre-commit hooks\n\nUse centralized configs from eslint-config and tailwind-config packages.\n\nAssume TanStack Router routes regenerate on dev - don't regenerate manually.\n\n## Changelog\n\n`packages/player/changelog.json` is the source of truth for the in-app \"What's New\" tab and auto-generated GitHub release notes.\n\nWhen building a user-facing feature, fix, or improvement, add an entry to the top of the array according to the format you find there.\n\n## Releasing\n\n### Nuclear Player\n\nReleases are triggered by git tags. The workflow builds for macOS (arm64/x64), Linux, and Windows. Release notes are auto-generated from `packages/player/changelog.json`.\n\n```bash\n# 1. Bump versions, update the appstream metainfo, commit, and tag\npnpm release:prepare X.Y.Z\n\n# 2. Push the tag\ngit push origin player@X.Y.Z\n```\n\n`release:prepare` does everything that's needed for a release. Do not bump versions or edit any files by hand when preparing a release.\n\nThe `release-player.yml` workflow creates a GitHub release with platform binaries.\n\n### Plugin SDK\n\nPublished to npm via the `release-plugin-sdk.yml` workflow.\n\n```bash\n# 1. Update version in packages/plugin-sdk/package.json\n# 2. Commit the version bump\ngit add packages/plugin-sdk/package.json && git commit -m \"plugin-sdk@X.Y.Z\"\n\n# 3. Tag and push\ngit tag plugin-sdk@X.Y.Z\ngit push origin plugin-sdk@X.Y.Z\n```\n\nThe workflow builds with `build:npm`, runs tests, and publishes to npm.\n"},"files":{"AGENTS.md":"# AGENTS.md\n\nGuidelines for AI coding agents working on Nuclear.\n\n## Project Overview\n\nNuclear is a free, open-source music player without ads or tracking. Search for any song or artist, build playlists, and start listening. It's a desktop app built with Tauri (Rust + React), organized as a pnpm monorepo managed with Turborepo.\n\n### Packages\n\n- `@nuclearplayer/player` - Main Tauri app (React + Rust)\n- `@nuclearplayer/ui` - Shared UI components\n- `@nuclearplayer/plugin-sdk` - Plugin system (published to npm)\n- `@nuclearplayer/model` - Data model\n- `@nuclearplayer/themes` - Theming system\n- `@nuclearplayer/hifi` - Advanced HTML5 audio component\n- `@nuclearplayer/tailwind-config` - Shared Tailwind config\n- `@nuclearplayer/eslint-config` - Shared linting rules\n- `@nuclearplayer/i18n` - Internationalization\n- `@nuclearplayer/storybook` - Component demos\n- `@nuclearplayer/tools` - Build and maintenance utilities\n- `@nuclearplayer/docs` - Documentation\n- `@nuclearplayer/website` - Project website (Astro)\n\n## Commands\n\n```bash\n# Development\npnpm dev                    # Run player in dev mode\npnpm dev:remote             # Same, but binds Vite to 0.0.0.0 so the remote control UI is reachable from other devices\npnpm storybook              # Run Storybook\n\n# Build\npnpm build                  # Build all packages\npnpm tauri build            # Build Tauri app\n\n# Quality\npnpm lint                   # Lint all packages\npnpm lint:fix               # Lint and auto-fix\npnpm type-check             # TypeScript checks\npnpm test                   # Run all tests\npnpm test:coverage          # Run tests with coverage\npnpm clean                  # Clean build artifacts\n\n# Package-specific testing\npnpm --filter @nuclearplayer/ui test -- src/components/Badge/Badge.test.tsx\npnpm --filter @nuclearplayer/ui test -- --testNamePattern=\"renders\"\n\n# Update snapshots (run at root for all, or filter to a specific package)\n\n# At root\npnpm test -- -u\n\n# Filtering for a specific package\npnpm --filter @nuclearplayer/ui test -- -u\n\n# After cd'ing into a package\npnpm test -u\n```\n\n## Code Style\n\n### General Principles\n\n- Prioritize readability over cleverness\n- No comments in code - explain reasoning in chat/commits\n- Avoid premature abstractions - start concrete, extract later\n- Small, focused changes over large dumps\n- Never commit unless explicitly asked\n\n### TypeScript\n\n- Use `type` not `interface` (except when merging is required)\n- No magic numbers - extract into named constants\n- Strict mode with `noUnusedLocals` and `noUnusedParameters`\n- Do not use one-letter variable names.\nAVOID: `(b) => b.buildIndexEntry()`\nPREFER: `(build) => build.buildIndexEntry()`\n\n### React Components\n\n```tsx\nimport { cva, VariantProps } from 'class-variance-authority';\nimport { ComponentProps, FC } from 'react';\n\nimport { cn } from '../../utils';\n\nconst componentVariants = cva('base-classes', {\n  variants: { /* ... */ },\n  defaultVariants: { /* ... */ },\n});\n\ntype ComponentProps = ComponentProps<'div'> &\n  VariantProps<typeof componentVariants>;\n\nexport const Component: FC<ComponentProps> = ({\n  className,\n  variant,\n  ...props\n}) => (\n  <div className={cn(componentVariants({ variant, className }))} {...props} />\n);\n```\n\n- Use `const Component: FC<Props>` not `function Component()`\n- Compound components (`Component.Sub`) for complex widgets\n- Keep business logic out of UI components\n\n### Adding UI Components\n\nWhen adding a new component to `@nuclearplayer/ui`:\n\n1. Create component directory: `packages/ui/src/components/MyComponent/`\n   - `MyComponent.tsx` - implementation\n   - `MyComponent.test.tsx` - tests (aim for 100% coverage)\n   - `index.ts` - re-exports\n2. Export from `packages/ui/src/components/index.ts`\n3. Add Storybook story in `packages/storybook/src/MyComponent.stories.tsx`\n4. Include snapshot test(s) covering all variants\n\n### Styling (Tailwind v4)\n\n- CSS-first config in `packages/tailwind-config/global.css`\n- Theme colors: `bg-background`, `text-foreground`, `bg-primary`\n- Accents: `accent-green`, `accent-yellow`, `accent-purple`, `accent-blue`, `accent-orange`, `accent-cyan`, `accent-red`\n- Use `cn()` for conditional classes, `cva()` for variants\n\n### State Management\n\n- **Zustand** - persistent UI state\n- **React state** - local, temporary state\n- **TanStack Query v5** - HTTP requests/server state\n- **TanStack Router** - client-side routing\n\n### Standardized Libraries\n\n- **Icons**: Lucide React (not heroicons, not font-awesome)\n- **Toasts**: Sonner\n- **Dates**: Luxon\n- **Utilities**: lodash-es (use individual imports: `import isEqual from 'lodash-es/isEqual'`)\n- **HTTP**: Native fetch via ApiClient base class (no axios)\n\n### Adding New Domains\n\nA \"domain\" is a feature area exposed to plugins (e.g., settings, queue, favorites). When adding a new domain:\n\n1. **Types** (`packages/plugin-sdk/src/types/myDomain.ts`)\n   - Define the `MyDomainHost` interface (the contract between player and SDK)\n   - Export any related types plugins will use\n\n2. **API class** (`packages/plugin-sdk/src/api/myDomain.ts`)\n   - Create a class that wraps the host and exposes methods to plugins\n   - Add to `NuclearAPI` constructor in `packages/plugin-sdk/src/api/index.ts`\n\n3. **Store** (`packages/player/src/stores/myDomainStore.ts`)\n   - Zustand store holding the domain state\n   - Persists to disk via `@tauri-apps/plugin-store` if needed\n\n4. **Host** (`packages/player/src/services/myDomainHost.ts`)\n   - Implements the `MyDomainHost` interface\n   - Bridges the SDK API to the Zustand store\n   - Passed to `NuclearAPI` when initializing plugins\n\n### External API Clients\n\nLive in `packages/player/src/apis/`. Use `ApiClient` base class (fetch→json→Zod).\n\n- Validate external data with Zod schemas\n- Export singleton instances\n- One class per external service\n\n### Internationalization\n\nAll user-facing strings go through i18n - no hardcoded UI text.\n\n```tsx\nimport { useTranslation } from '@nuclearplayer/i18n';\n\nconst { t } = useTranslation();\n<span>{t('navigation.settings')}</span>\n```\n\nAdd new strings to `packages/i18n/src/locales/en_US.json` only. Other locales come from Crowdin.\n\n## Testing\n\nTests use Vitest + React Testing Library. Globals enabled (`describe`, `it`, `expect`, `vi`).\n\n- Integration tests over unit tests for user-facing behavior. Render real components and assert on DOM content rather than verifying mock calls.\n- Unit tests for utilities - standalone data structures (RingBuffer, parsers) deserve isolated tests. Use them sparingly.\n- Test user behavior, not implementation details\n- Minimize mocks - only mock external deps (HTTP, FS, Tauri)\n- Snapshot tests: prefix with `(Snapshot)`, basic rendering only\n- Never use `querySelector` in tests. Prefer RTL queries.\n- When semantic queries aren't possible, add `data-testid` attributes. And don't be shy with them\n- Don't use defensive measures like try-catch or conditional checks in tests. The test will fail anyway if our assumptions are wrong.\n\n### Test-first for views\n\nWhen building a new view, write the test wrapper and tests **before** any implementation code. The tests describe what the user sees and does — they define the contract. Then implement to make them pass.\n\nDon't start with unit tests for internal utilities (grouping functions, registries, etc.). Start from the outside: what does the user see on the page? The internal structure is an implementation detail that falls out of making the tests green.\n\n### Test Wrappers for Views\n\nPlayer views and some components use a `*.test-wrapper.tsx` file that creates a domain-specific abstraction layer over the DOM. This lets tests read like user stories, and if the implementation changes, only the wrapper needs updating.\n\n**Wrapper conventions:**\n- Use **getters** for element queries: `get emptyState()`, `get cards()` — not `getEmptyState()`\n- Use **nested objects** for interactive elements: `createButton: { get element(), async click() }`\n- Use **methods** for multi-step user actions: `async openContextMenu(title: string)`\n- Tests should use `Wrapper.emptyState`, `Wrapper.cards`, `Wrapper.createButton.click()` — not bare `screen` queries\n- The wrapper is the only place that knows about test IDs, roles, and DOM structure\n- Don't use queryX methods in the wrapper - always get or find as appropriate.\n- Never use fireEvent. Always use userEvent for interactions.\n\n### Test fixtures\n\nTo populate the app with testing data, use fixtures. See `packages/player/src/test/fixtures` for examples.\n\n### Wrapper fixtures\n\nTest wrappers can expose a `fixtures` object with factory methods that return pre-configured builders for common test scenarios. This keeps test setup readable and co-located with the wrapper, while the raw fixture data itself lives in `packages/player/src/test/fixtures/`.\n\n```tsx\n// Dashboard.test-wrapper.tsx\nimport { TOP_TRACKS_RADIOHEAD } from '../../test/fixtures/dashboard';\n\nexport const DashboardWrapper = {\n  // ... mount, getters, etc.\n\n  fixtures: {\n    topTracksProvider() {\n      return new DashboardProviderBuilder()\n        .withCapabilities('topTracks')\n        .withFetchTopTracks(async () => TOP_TRACKS_RADIOHEAD);\n    },\n  },\n};\n\n// Dashboard.test.tsx\nDashboardWrapper.seedProvider(DashboardWrapper.fixtures.topTracksProvider());\n```\n\n### The builder pattern for tests\n\nWe use builders to create test data and various entities cleanly. You can see them in `packages/player/src/test/builders`.\n\n- A builder is a class that has an instance of the object it's building\n- When the builder is instantiated, it creates a default object with reasonable defaults\n- The builder has methods that mutate the object and return `this` for chaining\n- The `build()` method returns the final object, which can then be used in tests\n\n```tsx\n// Playlists.test-wrapper.tsx\nexport const PlaylistsWrapper = {\n  async mount(): Promise<RenderResult> { /* ... */ },\n\n  get emptyState() {\n    return screen.queryByTestId('empty-state');\n  },\n  get cards() {\n    return screen.queryAllByTestId('card');\n  },\n\n  createButton: {\n    get element() {\n      return screen.getByTestId('create-playlist-button');\n    },\n    async click() {\n      await userEvent.click(this.element);\n    },\n  },\n};\n\n// Playlists.test.tsx — reads like a user story\nit('shows empty state when no playlists', async () => {\n  await PlaylistsWrapper.mount();\n  expect(PlaylistsWrapper.emptyState).toBeInTheDocument();\n});\n```\n\n## File Organization\n\n```\npackages/ui/src/components/Badge/\n  Badge.tsx           # Implementation\n  Badge.test.tsx      # Tests\n  index.ts            # Re-exports\n  __snapshots__/      # Vitest snapshots\n```\n\n## Rust Backend\n\nThe Tauri backend lives in `packages/player/src-tauri/src/`. Modules:\n\n- `bridge/` - bidirectional RPC. Lets Rust servers call into the frontend (`Bridge::call` emits a `bridge:request` event, frontend replies via the `bridge_respond` command).\n- `http_api/` - Axum REST + SSE server for Nuclear Jam remote control\n- `mcp/` - MCP server exposing player functions as tools to LLM clients\n- `mpd/` - MPD-protocol TCP server for clients like ncmpcpp\n- `stream_server.rs` - local audio proxy adding CORS + Range so the browser can play blocked streams\n- `http.rs` - `http_fetch` command, a CORS-bypassing HTTP proxy for the frontend\n- `ytdlp.rs` / `ytdlp_setup.rs` - yt-dlp subprocess wrapper; auto-downloads the binary\n- `discord.rs` - Discord Rich Presence\n- `commands.rs` - filesystem helpers (zip, download, flatpak detection)\n- `net.rs`, `setup.rs`, `logging.rs` - port binding, log plugin config, startup log ring buffer\n- `lib.rs` - declares modules, registers all commands, runs `init_*` in `.setup()`. `main.rs` - binary entry, env fixups.\n\nCommands (run from `packages/player/src-tauri/`):\n\n```bash\ncargo test          # Rust tests (no clippy/rustfmt configured; run manually)\n```\n\n`pnpm dev` runs `tauri dev`; `pnpm build` (in `packages/player`) runs the full `tauri build`.\n\n## Design Philosophy\n\n- Neo-brutalist with premium polish - bold borders, purposeful shadows\n- Premium, designed feel\n- Animations via `motion` and `tw-animate-css`\n- Disable animations during high-friction moments (resize, drag)\n- Avoid generic AI patterns (icon-grid cards, stock heroes, \"Built with love\" badges)\n\n## Tooling Notes\n\n- **pnpm** with workspace protocol for internal deps\n- **Turborepo** for task orchestration\n- **ESLint + Prettier** run together\n- **Husky + lint-staged** for pre-commit hooks\n\nUse centralized configs from eslint-config and tailwind-config packages.\n\nAssume TanStack Router routes regenerate on dev - don't regenerate manually.\n\n## Changelog\n\n`packages/player/changelog.json` is the source of truth for the in-app \"What's New\" tab and auto-generated GitHub release notes.\n\nWhen building a user-facing feature, fix, or improvement, add an entry to the top of the array according to the format you find there.\n\n## Releasing\n\n### Nuclear Player\n\nReleases are triggered by git tags. The workflow builds for macOS (arm64/x64), Linux, and Windows. Release notes are auto-generated from `packages/player/changelog.json`.\n\n```bash\n# 1. Bump versions, update the appstream metainfo, commit, and tag\npnpm release:prepare X.Y.Z\n\n# 2. Push the tag\ngit push origin player@X.Y.Z\n```\n\n`release:prepare` does everything that's needed for a release. Do not bump versions or edit any files by hand when preparing a release.\n\nThe `release-player.yml` workflow creates a GitHub release with platform binaries.\n\n### Plugin SDK\n\nPublished to npm via the `release-plugin-sdk.yml` workflow.\n\n```bash\n# 1. Update version in packages/plugin-sdk/package.json\n# 2. Commit the version bump\ngit add packages/plugin-sdk/package.json && git commit -m \"plugin-sdk@X.Y.Z\"\n\n# 3. Tag and push\ngit tag plugin-sdk@X.Y.Z\ngit push origin plugin-sdk@X.Y.Z\n```\n\nThe workflow builds with `build:npm`, runs tests, and publishes to npm.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# AGENTS.md\n\nGuidelines for AI coding agents working on Nuclear.\n\n## Project Overview\n\nNuclear is a free, open-source music player without ads or tracking. Search for any song or artist, build playlists, and start listening. It's a desktop app built with Tauri (Rust + React), organized as a pnpm monorepo managed with Turborepo.\n\n### Packages\n\n- `@nuclearplayer/player` - Main Tauri app (React + Rust)\n- `@nuclearplayer/ui` - Shared UI components\n- `@nuclearplayer/plugin-sdk` - Plugin system (published to npm)\n- `@nuclearplayer/model` - Data model\n- `@nuclearplayer/themes` - Theming system\n- `@nuclearplayer/hifi` - Advanced HTML5 audio component\n- `@nuclearplayer/tailwind-config` - Shared Tailwind config\n- `@nuclearplayer/eslint-config` - Shared linting rules\n- `@nuclearplayer/i18n` - Internationalization\n- `@nuclearplayer/storybook` - Component demos\n- `@nuclearplayer/tools` - Build and maintenance utilities\n- `@nuclearplayer/docs` - Documentation\n- `@nuclearplayer/website` - Project website (Astro)\n\n## Commands\n\n```bash\n# Development\npnpm dev                    # Run player in dev mode\npnpm dev:remote             # Same, but binds Vite to 0.0.0.0 so the remote control UI is reachable from other devices\npnpm storybook              # Run Storybook\n\n# Build\npnpm build                  # Build all packages\npnpm tauri build            # Build Tauri app\n\n# Quality\npnpm lint                   # Lint all packages\npnpm lint:fix               # Lint and auto-fix\npnpm type-check             # TypeScript checks\npnpm test                   # Run all tests\npnpm test:coverage          # Run tests with coverage\npnpm clean                  # Clean build artifacts\n\n# Package-specific testing\npnpm --filter @nuclearplayer/ui test -- src/components/Badge/Badge.test.tsx\npnpm --filter @nuclearplayer/ui test -- --testNamePattern=\"renders\"\n\n# Update snapshots (run at root for all, or filter to a specific package)\n\n# At root\npnpm test -- -u\n\n# Filtering for a specific package\npnpm --filter @nuclearplayer/ui test -- -u\n\n# After cd'ing into a package\npnpm test -u\n```\n\n## Code Style\n\n### General Principles\n\n- Prioritize readability over cleverness\n- No comments in code - explain reasoning in chat/commits\n- Avoid premature abstractions - start concrete, extract later\n- Small, focused changes over large dumps\n- Never commit unless explicitly asked\n\n### TypeScript\n\n- Use `type` not `interface` (except when merging is required)\n- No magic numbers - extract into named constants\n- Strict mode with `noUnusedLocals` and `noUnusedParameters`\n- Do not use one-letter variable names.\nAVOID: `(b) => b.buildIndexEntry()`\nPREFER: `(build) => build.buildIndexEntry()`\n\n### React Components\n\n```tsx\nimport { cva, VariantProps } from 'class-variance-authority';\nimport { ComponentProps, FC } from 'react';\n\nimport { cn } from '../../utils';\n\nconst componentVariants = cva('base-classes', {\n  variants: { /* ... */ },\n  defaultVariants: { /* ... */ },\n});\n\ntype ComponentProps = ComponentProps<'div'> &\n  VariantProps<typeof componentVariants>;\n\nexport const Component: FC<ComponentProps> = ({\n  className,\n  variant,\n  ...props\n}) => (\n  <div className={cn(componentVariants({ variant, className }))} {...props} />\n);\n```\n\n- Use `const Component: FC<Props>` not `function Component()`\n- Compound components (`Component.Sub`) for complex widgets\n- Keep business logic out of UI components\n\n### Adding UI Components\n\nWhen adding a new component to `@nuclearplayer/ui`:\n\n1. Create component directory: `packages/ui/src/components/MyComponent/`\n   - `MyComponent.tsx` - implementation\n   - `MyComponent.test.tsx` - tests (aim for 100% coverage)\n   - `index.ts` - re-exports\n2. Export from `packages/ui/src/components/index.ts`\n3. Add Storybook story in `packages/storybook/src/MyComponent.stories.tsx`\n4. Include snapshot test(s) covering all variants\n\n### Styling (Tailwind v4)\n\n- CSS-first config in `packages/tailwind-config/global.css`\n- Theme colors: `bg-background`, `text-foreground`, `bg-primary`\n- Accents: `accent-green`, `accent-yellow`, `accent-purple`, `accent-blue`, `accent-orange`, `accent-cyan`, `accent-red`\n- Use `cn()` for conditional classes, `cva()` for variants\n\n### State Management\n\n- **Zustand** - persistent UI state\n- **React state** - local, temporary state\n- **TanStack Query v5** - HTTP requests/server state\n- **TanStack Router** - client-side routing\n\n### Standardized Libraries\n\n- **Icons**: Lucide React (not heroicons, not font-awesome)\n- **Toasts**: Sonner\n- **Dates**: Luxon\n- **Utilities**: lodash-es (use individual imports: `import isEqual from 'lodash-es/isEqual'`)\n- **HTTP**: Native fetch via ApiClient base class (no axios)\n\n### Adding New Domains\n\nA \"domain\" is a feature area exposed to plugins (e.g., settings, queue, favorites). When adding a new domain:\n\n1. **Types** (`packages/plugin-sdk/src/types/myDomain.ts`)\n   - Define the `MyDomainHost` interface (the contract between player and SDK)\n   - Export any related types plugins will use\n\n2. **API class** (`packages/plugin-sdk/src/api/myDomain.ts`)\n   - Create a class that wraps the host and exposes methods to plugins\n   - Add to `NuclearAPI` constructor in `packages/plugin-sdk/src/api/index.ts`\n\n3. **Store** (`packages/player/src/stores/myDomainStore.ts`)\n   - Zustand store holding the domain state\n   - Persists to disk via `@tauri-apps/plugin-store` if needed\n\n4. **Host** (`packages/player/src/services/myDomainHost.ts`)\n   - Implements the `MyDomainHost` interface\n   - Bridges the SDK API to the Zustand store\n   - Passed to `NuclearAPI` when initializing plugins\n\n### External API Clients\n\nLive in `packages/player/src/apis/`. Use `ApiClient` base class (fetch→json→Zod).\n\n- Validate external data with Zod schemas\n- Export singleton instances\n- One class per external service\n\n### Internationalization\n\nAll user-facing strings go through i18n - no hardcoded UI text.\n\n```tsx\nimport { useTranslation } from '@nuclearplayer/i18n';\n\nconst { t } = useTranslation();\n<span>{t('navigation.settings')}</span>\n```\n\nAdd new strings to `packages/i18n/src/locales/en_US.json` only. Other locales come from Crowdin.\n\n## Testing\n\nTests use Vitest + React Testing Library. Globals enabled (`describe`, `it`, `expect`, `vi`).\n\n- Integration tests over unit tests for user-facing behavior. Render real components and assert on DOM content rather than verifying mock calls.\n- Unit tests for utilities - standalone data structures (RingBuffer, parsers) deserve isolated tests. Use them sparingly.\n- Test user behavior, not implementation details\n- Minimize mocks - only mock external deps (HTTP, FS, Tauri)\n- Snapshot tests: prefix with `(Snapshot)`, basic rendering only\n- Never use `querySelector` in tests. Prefer RTL queries.\n- When semantic queries aren't possible, add `data-testid` attributes. And don't be shy with them\n- Don't use defensive measures like try-catch or conditional checks in tests. The test will fail anyway if our assumptions are wrong.\n\n### Test-first for views\n\nWhen building a new view, write the test wrapper and tests **before** any implementation code. The tests describe what the user sees and does — they define the contract. Then implement to make them pass.\n\nDon't start with unit tests for internal utilities (grouping functions, registries, etc.). Start from the outside: what does the user see on the page? The internal structure is an implementation detail that falls out of making the tests green.\n\n### Test Wrappers for Views\n\nPlayer views and some components use a `*.test-wrapper.tsx` file that creates a domain-specific abstraction layer over the DOM. This lets tests read like user stories, and if the implementation changes, only the wrapper needs updating.\n\n**Wrapper conventions:**\n- Use **getters** for element queries: `get emptyState()`, `get cards()` — not `getEmptyState()`\n- Use **nested objects** for interactive elements: `createButton: { get element(), async click() }`\n- Use **methods** for multi-step user actions: `async openContextMenu(title: string)`\n- Tests should use `Wrapper.emptyState`, `Wrapper.cards`, `Wrapper.createButton.click()` — not bare `screen` queries\n- The wrapper is the only place that knows about test IDs, roles, and DOM structure\n- Don't use queryX methods in the wrapper - always get or find as appropriate.\n- Never use fireEvent. Always use userEvent for interactions.\n\n### Test fixtures\n\nTo populate the app with testing data, use fixtures. See `packages/player/src/test/fixtures` for examples.\n\n### Wrapper fixtures\n\nTest wrappers can expose a `fixtures` object with factory methods that return pre-configured builders for common test scenarios. This keeps test setup readable and co-located with the wrapper, while the raw fixture data itself lives in `packages/player/src/test/fixtures/`.\n\n```tsx\n// Dashboard.test-wrapper.tsx\nimport { TOP_TRACKS_RADIOHEAD } from '../../test/fixtures/dashboard';\n\nexport const DashboardWrapper = {\n  // ... mount, getters, etc.\n\n  fixtures: {\n    topTracksProvider() {\n      return new DashboardProviderBuilder()\n        .withCapabilities('topTracks')\n        .withFetchTopTracks(async () => TOP_TRACKS_RADIOHEAD);\n    },\n  },\n};\n\n// Dashboard.test.tsx\nDashboardWrapper.seedProvider(DashboardWrapper.fixtures.topTracksProvider());\n```\n\n### The builder pattern for tests\n\nWe use builders to create test data and various entities cleanly. You can see them in `packages/player/src/test/builders`.\n\n- A builder is a class that has an instance of the object it's building\n- When the builder is instantiated, it creates a default object with reasonable defaults\n- The builder has methods that mutate the object and return `this` for chaining\n- The `build()` method returns the final object, which can then be used in tests\n\n```tsx\n// Playlists.test-wrapper.tsx\nexport const PlaylistsWrapper = {\n  async mount(): Promise<RenderResult> { /* ... */ },\n\n  get emptyState() {\n    return screen.queryByTestId('empty-state');\n  },\n  get cards() {\n    return screen.queryAllByTestId('card');\n  },\n\n  createButton: {\n    get element() {\n      return screen.getByTestId('create-playlist-button');\n    },\n    async click() {\n      await userEvent.click(this.element);\n    },\n  },\n};\n\n// Playlists.test.tsx — reads like a user story\nit('shows empty state when no playlists', async () => {\n  await PlaylistsWrapper.mount();\n  expect(PlaylistsWrapper.emptyState).toBeInTheDocument();\n});\n```\n\n## File Organization\n\n```\npackages/ui/src/components/Badge/\n  Badge.tsx           # Implementation\n  Badge.test.tsx      # Tests\n  index.ts            # Re-exports\n  __snapshots__/      # Vitest snapshots\n```\n\n## Rust Backend\n\nThe Tauri backend lives in `packages/player/src-tauri/src/`. Modules:\n\n- `bridge/` - bidirectional RPC. Lets Rust servers call into the frontend (`Bridge::call` emits a `bridge:request` event, frontend replies via the `bridge_respond` command).\n- `http_api/` - Axum REST + SSE server for Nuclear Jam remote control\n- `mcp/` - MCP server exposing player functions as tools to LLM clients\n- `mpd/` - MPD-protocol TCP server for clients like ncmpcpp\n- `stream_server.rs` - local audio proxy adding CORS + Range so the browser can play blocked streams\n- `http.rs` - `http_fetch` command, a CORS-bypassing HTTP proxy for the frontend\n- `ytdlp.rs` / `ytdlp_setup.rs` - yt-dlp subprocess wrapper; auto-downloads the binary\n- `discord.rs` - Discord Rich Presence\n- `commands.rs` - filesystem helpers (zip, download, flatpak detection)\n- `net.rs`, `setup.rs`, `logging.rs` - port binding, log plugin config, startup log ring buffer\n- `lib.rs` - declares modules, registers all commands, runs `init_*` in `.setup()`. `main.rs` - binary entry, env fixups.\n\nCommands (run from `packages/player/src-tauri/`):\n\n```bash\ncargo test          # Rust tests (no clippy/rustfmt configured; run manually)\n```\n\n`pnpm dev` runs `tauri dev`; `pnpm build` (in `packages/player`) runs the full `tauri build`.\n\n## Design Philosophy\n\n- Neo-brutalist with premium polish - bold borders, purposeful shadows\n- Premium, designed feel\n- Animations via `motion` and `tw-animate-css`\n- Disable animations during high-friction moments (resize, drag)\n- Avoid generic AI patterns (icon-grid cards, stock heroes, \"Built with love\" badges)\n\n## Tooling Notes\n\n- **pnpm** with workspace protocol for internal deps\n- **Turborepo** for task orchestration\n- **ESLint + Prettier** run together\n- **Husky + lint-staged** for pre-commit hooks\n\nUse centralized configs from eslint-config and tailwind-config packages.\n\nAssume TanStack Router routes regenerate on dev - don't regenerate manually.\n\n## Changelog\n\n`packages/player/changelog.json` is the source of truth for the in-app \"What's New\" tab and auto-generated GitHub release notes.\n\nWhen building a user-facing feature, fix, or improvement, add an entry to the top of the array according to the format you find there.\n\n## Releasing\n\n### Nuclear Player\n\nReleases are triggered by git tags. The workflow builds for macOS (arm64/x64), Linux, and Windows. Release notes are auto-generated from `packages/player/changelog.json`.\n\n```bash\n# 1. Bump versions, update the appstream metainfo, commit, and tag\npnpm release:prepare X.Y.Z\n\n# 2. Push the tag\ngit push origin player@X.Y.Z\n```\n\n`release:prepare` does everything that's needed for a release. Do not bump versions or edit any files by hand when preparing a release.\n\nThe `release-player.yml` workflow creates a GitHub release with platform binaries.\n\n### Plugin SDK\n\nPublished to npm via the `release-plugin-sdk.yml` workflow.\n\n```bash\n# 1. Update version in packages/plugin-sdk/package.json\n# 2. Commit the version bump\ngit add packages/plugin-sdk/package.json && git commit -m \"plugin-sdk@X.Y.Z\"\n\n# 3. Tag and push\ngit tag plugin-sdk@X.Y.Z\ngit push origin plugin-sdk@X.Y.Z\n```\n\nThe workflow builds with `build:npm`, runs tests, and publishes to npm.\n","category":"root","tokens":3457}]}