The widget is designed around four hard constraints:
- official playback integrations only: YTMDesktop Companion, Windows Media Session, or Cider's local API
- polished always-on-top desktop widget UX
- low idle overhead
- easy future extension for more window modes, locales, and platforms
src/app/AppRoot.tsxsrc/app/AppProvider.tsxsrc/app/WidgetWindow.tsxsrc/app/SettingsWindow.tsx
Responsibilities:
- bootstrapping settings and runtime mode
- choosing the user-facing production source independently from the development simulator override
- wiring theme, i18n, and window-specific UI
- exposing a simple app model to components
src/domain/playback/types.tssrc/domain/playback/connectionMachine.tssrc/domain/playback/controller.tssrc/domain/playback/mapping.tssrc/domain/playback/progress.ts
Responsibilities:
- explicit connection state machine
- source snapshot to UI-state mapping with explicit capabilities
- reconnect scheduling with backoff
- progress smoothing between realtime updates
- stable command surface for UI components
- Real gateway:
src/integration/companion/realGateway.ts - Windows Media gateway:
src/integration/windowsMedia/windowsMediaGateway.ts - Cider gateway:
src/integration/cider/ciderGateway.ts - Tauri invoke/event bridge:
src/integration/companion/tauriBridge.ts - Simulator gateway:
src/integration/simulator/simulatorGateway.ts
Responsibilities:
- keeping frontend code unaware of transport details
- separating runtime-only bridge concerns from UI/domain code
- providing a realistic simulator without replacing the real architecture
src-tauri/src/companion.rssrc-tauri/src/cider.rssrc-tauri/src/settings.rssrc-tauri/src/startup.rssrc-tauri/src/windows_media.rssrc-tauri/src/lib.rs
Responsibilities:
- Companion API HTTP + realtime socket integration
- Cider loopback REST + shared application-wide Socket.IO integration
- current-session WinRT metadata, artwork, timeline, capability, and transport integration
- token storage via keyring
- settings persistence on disk
- tray integration and hide-to-tray behavior
- launch-on-startup on Windows
- window creation and position persistence
src/components/**src/styles/global.csssrc/locales/en.jsonsrc/locales/ru.json
Responsibilities:
- reusable glass panels, artwork layers, controls, and settings sections
- visual consistency across widget and settings windows
- externalized user-facing strings with matching English/Russian locale keys
flowchart LR
UI["React widget UI"] --> Domain["PlaybackController + state machine"]
Domain --> Gateway{"Development source mode"}
Gateway -->|real/auto in Tauri| Product{"Playback source"}
Gateway -->|simulator| Sim["Local simulator"]
Product -->|Companion| Bridge["Tauri invoke/event bridge"]
Product -->|Windows Media Session| Bridge
Product -->|Cider local API| Bridge
Bridge --> Rust["Rust backend"]
Rust --> Companion["YTMDesktop Companion Server API"]
Rust --> Cider["Cider 127.0.0.1:10767"]
Rust --> WMSWorker["Lazy dedicated MTA worker"]
WMSWorker --> WMS["Windows Global System Media Transport Controls"]
Rust --> Storage["Keyring + settings JSON + Windows integration"]
The connection state machine exposes these explicit states:
disconnecteddiscoveringauth_requiredauthenticatingconnectedreconnectingerror
Why this matters:
- UI states stay intentional instead of stringly-typed
- reconnect logic is isolated from rendering
- simulator and real gateway can drive the same domain layer
- future telemetry and richer diagnostics can attach to the same transitions
- Frontend starts
PlaybackController. - Controller asks the gateway whether stored auth exists.
- Rust backend probes public
GET /metadata. - If auth is missing, the UI moves to
auth_required. - Auth uses
POST /api/v1/auth/requestcodefollowed byPOST /api/v1/auth/request. - If auth exists, Rust fetches
GET /api/v1/statewith the raw token in theAuthorizationheader and opens the realtime socket. - Realtime connects to
/api/v1/realtimeover websocket with the token inauth.token. - Rust emits Companion events back to the frontend.
- The controller maps raw Companion payloads into UI-ready playback snapshots.
- Commands from the UI flow back through
POST /api/v1/commandwith{ command, data }payloads.
The simulator exists for UI development, unit tests, and Playwright coverage.
Design rules:
- it implements the same
CompanionGatewayinterface as the real client - it emits realistic track changes and time progression
- it does not bypass the domain controller
- it is opt-in and clearly separated from production integration
Delivery status: tasks 0050 and 0051 corrected the worker/runtime and isolated E_ACCESSDENIED to the restricted Codex sandbox token. Task 0052 then removed a separate transient attach failure: connect no longer waits for a complete current-session snapshot, live poll diagnostics survive the native/frontend boundary, and a failed poll reacquires the manager. The unpackaged path succeeds in the normal interactive Windows user session with Apple Music; package identity is not required, so task 0049 remains only an optional future installer/signing decision.
- The persisted
playbackSourceselectswindowsMediaSession; Companion remains the migration/default value. - The Rust adapter lazily starts one long-lived
std::thread, initializes it withRoInitialize(RO_INIT_MULTITHREADED), and sends typed requests through an actor queue. - Discovery and connection require manager access only. Session enumeration/current-session detail is best-effort, and connect commits before metadata, timeline, controls, or artwork are read.
- Manager acquisition, session discovery, blocking WinRT futures, polling, metadata, timeline, artwork, and commands all execute sequentially on that worker rather than on the Tokio runtime.
- A failed poll retains the previous snapshot, drops the stale manager, publishes a structured diagnostic, and retries manager/current-session acquisition on the next 750 ms worker cycle. A later successful poll restores the connected status.
- The shared mapping layer produces the same
PlaybackSnapshotcontract used by Companion. - UI controls use capability flags rather than source-name checks.
- WMS Like/Dislike/Mute are disabled and remain successful no-ops in both frontend and Rust as defense in depth.
- Timeline values are normalized relative to
StartTime, seek targets are clamped toMinSeekTime/MaxSeekTime, and polling waits 750 ms between unchanged snapshots rather than replaying missed async ticks in a burst. - Media text and raster artwork are bounded. Artwork is resolved once per track and omitted from subsequent state events so base64 data is not repeatedly copied through IPC.
- Requests have a 15-second caller bound; cancelled connects are not committed, and disconnect/source switching clears the worker's manager, consumer handle, snapshot, and polling state.
- The first poll publishes an explicit empty state, and field-level metadata/timeline/playback/control failures retain safe previous/default values without invalidating manager access.
- Public errors stay generic while an optional diagnostic object preserves only stage, HRESULT, and category. Access denied produces localized guidance to launch the portable EXE directly in the normal interactive user session; the app never attempts to escape a restricted launcher.
- Native WMS failures append only timestamp, operation, stage, category, and optional HRESULT to a 256 KiB rotating JSONL file under the app log directory. Logging is best-effort and never controls playback success.
- No WMS media data is persisted in version 3.1.0.
playbackSource: ciderselects a dedicated gateway; it is not inferred through WMS.- Native discovery and REST calls are fixed to
http://127.0.0.1:10767; remote/LAN endpoints are intentionally rejected by design. - Settings accepts a Cider external-application token, validates it against
playback/now-playing, and stores it in Windows Credential Manager under a Cider-specific account. Native requests send it only in theapptokenheader; it is not placed in URLs, frontend settings, events, logs, or public errors. - The adapter requires the initial
/api/v1/playback/now-playingstate and independently attemptsGET /api/v1/playback/volume. A volume failure leaves now-playing connected withcanMute: false; a later valid REST retry or socket event recovers capability without restarting the app. - Valid finite Cider volume values stay in the native
0..1domain and are normalized to the shared0..100playback contract. Socket.IOAPI:Playbackevents of typeplayerStatus.volumeDidChangeupdate only volume/capability memory and preserve track metadata. - Mute and unmute use
POST /api/v1/playback/volume. Mute posts zero after remembering a reliable non-zero level; unmute restores the current connection's last non-zero value, then manager-level memory retained across reconnect, then a conservative 25% fallback. State changes are published only after successful REST confirmation, while repeated already-satisfied commands are idempotent. - Supported transport, seek, and rating actions continue to use
/api/v1/playback/*. Windows Media Session remains mute-unsupported; Companion keeps its distinct official mute/unmute path. - Main and Settings controllers reuse one live native socket, one authoritative playback/volume cache, and one actor-style command queue. The Tauri manager lock is released before a command waits on HTTP, and the short playback-cache lock is never held across network I/O.
- Intentional disconnect/replacement invalidates the lifecycle before close so it cannot publish a false global transport failure; a genuine close is published once.
Settings are grouped by feature area:
- Playback Source
- API / Connection (Companion only)
- UI / Display
- Widget Layout
- Widget Size
- Transparency / Background
- Window / Behavior
- Developer controls
- About
Persistence model:
- Tauri runtime: JSON settings in the app config directory
- browser preview:
localStorage - auth token: OS keyring through Rust, not in frontend storage
- locale: persisted as part of UI settings; English is the backward-compatible default
- widget size: persisted as a named mode plus one canonical Custom percentage; custom width and height are derived views of that percentage
- widget layout: persisted as a normalized permutation of six typed block IDs plus explicit visibility modes; unknown/duplicate IDs are repaired and missing IDs are appended
- Settings disclosure state: persisted as a deduplicated whitelist of top-level section IDs
- playback source: persisted separately from the development
sourceMode; existing settings migrate tocompanion - WMS diagnostics: bounded rotating JSONL in the native app log directory; no title, artist, artwork, source-app identity, credential, token, or command payload
package.jsonis the only manually edited application-version source.- Tauri resolves
versionthrough../package.json. - React imports the root package version for Settings/About display.
- Rust Companion metadata uses
CARGO_PKG_VERSION. npm run version:syncupdates required Cargo and lockfile copies;npm run version:checkis part ofnpm run verify.
- frameless
- transparent
- canonical 336 px cover-driven layout with Compact, unchanged Default, Large, and linked Custom uniform scaling
- intrinsic content height is measured before the selected scale is applied to both the content layer and native window
- six primary blocks render through a persisted order while fallback/auth state cards remain outside the user-controlled order
- free border resize remains disabled; sizing is controlled through Settings
- always-on-top capable
- draggable on free surface
- hidden to tray on close
- separate window label
- opened on demand
- remembers its own position
- shares the same app model and visual language
Performance-sensitive choices in the current implementation:
- expensive artwork styling only changes when artwork URLs change
- progress is smoothed locally instead of forcing constant transport updates
- simulator and transport logic are kept outside presentation components
- reconnect timing is handled in the controller instead of in React render paths
- the WMS worker starts only when WMS is first used and keeps blocking WinRT calls outside Tokio
- UI animation is mostly CSS-driven and short in duration
The current structure is intentionally ready for:
- future alternate responsive/reflowing window layouts beyond the current proportional size modes
- optional free border resize if a later task defines safe persistence and aspect-ratio behavior
- additional locale JSON bundles beyond the current English/Russian pair
- Linux platform services and an official MPRIS/D-Bus adapter before macOS work
- GitHub CI/release automation for each supported native platform
- a scalable language picker and additional complete locale bundles
- macOS window, tray, startup, keychain, build, and signing behavior after the Linux/CI foundations
- richer diagnostics and logging around Companion reconnects
- enabling or disabling seek behavior with minimal UI churn
Current coverage focuses on the highest-value layers first:
- unit tests for connection-state transitions
- unit tests for Companion raw-state mapping
- unit tests for native Companion v2 request payload construction
- simulator behavior coverage
- widget rendering coverage for key states
- Playwright smoke flow for widget and settings views in simulator mode
- live Tauri MCP validation against a running debug app
The project is also wired for the Tauri MCP server named tauri, using the MCP bridge plugin in debug builds. Reference repo: https://github.com/hypothesi/mcp-server-tauri