# Stream GUI RS architecture ```text React (browsing + Watching + settings, public DTOs only) ↓ typed Tauri IPC Rust backend ├── Twitch device authorization / validation / refresh │ └── CredentialVault → platform CredentialStore ├── Browsing queries / safe application DTOs │ └── Helix endpoint methods / typed models │ ├── bounded cache / pagination / ID batching │ └── shared Twitch HTTP pool / rate coordinator ├── followed-stream monitor → native notifications / tray / window lifecycle ├── versioned global/channel/profile settings + trusted browser/Chatterino chat └── Streamlink discovery / probe / session supervision ↓ native process + argument array, never a shell Streamlink ↓ default / mpv / VLC / custom player external player ``` The application identity is `io.github.stream-gui-rs`; Tauri derives application directories from it. Secure credentials use the separate service `io.github.stream-gui-rs.oauth`, keyed by the public Twitch client ID. Changing either identity would start a new settings or credential namespace and requires explicit migration planning. Twitch/Helix modules and IPC contracts retain their existing names and ownership. ## Ownership `domain` holds errors, request DTOs and the service coordinator. `streamlink`, `twitch`, `twitch_http`, `helix`, `config`, `credentials`, `diagnostics`, `monitor`, `updates`, `chatterino`, and `platform` have no Tauri dependency. `desktop` composes services, resolves the platform application directory, opens the window, and handles shutdown. `commands` contains only adapters. The optional `desktop` Cargo feature allows backend tests without WebView libraries. The frontend shell and authentication hook live in `app`; `browse` owns view navigation, query snapshots, shared stream/image presentation, search and details; `lib` owns generated DTOs and typed IPC; `styles` owns theme tokens. `playback` holds the snapshot/request hook and quality presentation; `features` holds the shared Watching and playback settings components, plus authentication diagnostics. React state contains only UI inputs and backend snapshots. There is no frontend OAuth client or token persistence. The monitor, auth and process tasks remain with their Rust services independently of webview lifetime. Native notification/tray adapters live under `desktop`. ## IPC Rust DTOs derive `TS` and generate `src/lib/generated.ts`. A checked-in equality test enforces the contract. `src/lib/ipc.ts` is the only frontend `invoke` entry point and accepts a closed set of named operations and typed request objects. Rejections carry `{code, message}`. External provider bodies never become errors or logs. Commands cover backend diagnostics, probe/save, global settings get/update, channel override get/update, restricted browser chat, player discovery, launch, stop, generation-checked restart, session snapshots, auth status/login/cancel/logout/validate/refresh/account, opening the current Twitch verification link, the browsing operations listed below, and `desktop_status`, `pause_monitor`, `resume_monitor`, `request_notification_permission`, `acknowledge_desktop_action` and `quit_application`. Additional commands include `save_discovery_language`, `lookup_channel`, `support_report`, `app_info`, `show_about`, and the parameterless `open_repository`. No frontend-selected shell, arbitrary argv, generic filesystem API or arbitrary browser URL is exposed. Selecting a custom Streamlink executable is an intentional local trust decision: version probing is validation, not executable sandboxing. The main window has an explicit command allowlist generated by Tauri's application manifest. Remote origins get no capability. Production CSP blocks third-party scripts, direct web API connections, frames, and form submission. Only Rust contacts Twitch OAuth and Helix. Safe account and browsing DTOs permit HTTPS images from `static-cdn.jtvnw.net`; the CSP allows only that image origin in addition to local assets. The authentication browser-opening command accepts no URL argument and checks the provider's HTTPS Twitch activation URL; chat and repository intents likewise construct or select their restricted destinations in Rust. ## Streamlink sessions `Supervisor` is the sole process registry. Typed settings/command construction, identity resolution and restart generations do not introduce a second process owner. ```text Watch(broadcaster ID, public auth-session ID, optional quality policy) → bounded service-owned playback operation → uncached Rust Helix live-identity lookup for that exact auth session → Rust resolves global preferences < selected profile < channel overrides < request quality → native executable discovery/version probe → LaunchSpec (resolved paths, trusted identity, immutable effective preferences) → pure CommandSpec (executable + argv vector, no process creation) → existing Supervisor → Streamlink → external player ``` The login alone constructs `https://www.twitch.tv/` after ASCII channel-name validation. Display names, titles and categories are session metadata and never become command arguments. The service rechecks authentication after probing. Twitch stream ID, broadcaster ID, login and display name remain distinct. Existing session restart retains that identity without a new Helix lookup; it can therefore report offline or renamed-channel failure through Streamlink diagnostics. It never resolves a new account on behalf of an old request. Discovery tries absolute PATH entries and common platform locations: Linux `/usr/local/bin`, `/usr/bin`, `/bin`, `~/.local/bin`; macOS additionally `/opt/homebrew/bin` plus mpv/VLC executables in system/user Applications bundles; Windows native `.exe` names under PATH, Program Files and Local App Data/Programs. A custom absolute path is the escape hatch. No interpreter/download framework is involved. Explicit paths preserve the user's spelling/symlink in settings and resolve their current target for each use. Directories, non-executable Unix files and Windows script wrappers are rejected. Player discovery is independently testable without Tauri. The version probe concurrently drains both pipes with bounded capture and a five-second deadline, killing/reaping on timeout. Production playback requires Streamlink 8.0+. The base argv is `--no-config --no-plugin-sideloading --loglevel info --player-verbose`, followed by optional player/quality flags and `--`, canonical channel URL, quality selection. `Command::new` receives the executable path separately; no shell command is composed. The prototype URL/quality launch adapter is retained for native contract tests and is not reachable through IPC. OAuth tokens are not passed in argv, environment additions or configuration files. | Policy | Positional selection | `--stream-sorting-excludes` | | --- | --- | --- | | Source | `best` | omitted | | High | `high,best,best-unfiltered` | `>720p30` | | Medium | `medium,best,best-unfiltered` | `>540p30` | | Low | `low,best,best-unfiltered` | `>360p30` | | Audio | `audio,audio_only` | omitted | These follow the original app's policy model using current Streamlink syntax. The final unfiltered fallback deliberately permits a rendition above the nominal cap when needed. There is no rendition-discovery request/UI or automatic observer-driven relaunch. Player modes are default (delegate discovery to Streamlink), mpv, VLC and custom. mpv adds `--keep-open=no --quiet`; VLC adds `--play-and-exit`, with `--no-one-instance` only on Linux/Windows because native macOS VLC does not support that option. Additional arguments are a bounded vector of literal strings, not shell text. Each entry has formatting braces doubled, is POSIX-single-quoted with embedded quotes escaped, and is joined for Streamlink's second parsing boundary (Formatter followed by `shlex.split`, including on Windows). An owned `{playerinput}` is always appended so a literal user string containing that text cannot suppress stdin. Executable paths are separate unquoted argv values. The supported transport is Streamlink's normal player stdin only. See [testing policy](testing.md) for current playback acceptance. Each session has a stable UUID, generation, trusted metadata, selected policy, immutable effective-settings snapshot, optional chat error, process phase, timestamps, optional typed failure and bounded logs. A successfully spawned process is `running`; restart preparation is `starting`, and cleanup is `stopping`. Natural success or requested stop ends in `exited`; unexpected process failure ends in `failed`. Nonzero exit before three seconds is classified `startup_failed`, later exit `streamlink_exited`. This timing is diagnostic only: neither it nor `Starting player:` proves video rendering. One task owns each child and two concurrent readers drain stdout/stderr. Stop signals under the registry lock, releases it before waiting, and is idempotent for retained IDs. Restart accepts the observed generation, reserves the same session, reaps the old child/readers, resolves current settings and then spawns the replacement. Stale generations and overlapping restart attempts are rejected. An external Stop increments a cancellation revision, preventing a pending restart from spawning; shutdown closes the registry. A dropped IPC caller cannot abandon production launch/restart work because the service task owns it. Dropping a lower-level restart future also releases its reservation. Replacement retains metadata/ID and bounded diagnostics with a restart marker; it never overlaps the prior generation. Eight active/reserved sessions, eight outstanding playback operations and sixteen retained snapshots bound capacity. A terminal history entry cannot restart past active capacity. Only unreserved terminal snapshots may be evicted; unknown/evicted IDs return `not_found`. Spawn failures before a first child create no session; restart failures retain the session and a safe diagnostic. Starting channel A does not stop B. Requests always target explicit session IDs. Per session, 200 log entries are retained. Individual lines are bounded to 2,048 input bytes; oversize lines are omitted while pipes continue draining. Invalid UTF-8 is replaced, terminal control characters are stripped, and common credential-related lines are suppressed. Logs are React text, available only within that session, and never persisted. An untrusted custom executable can still output arbitrary sensitive content; the filter is not a universal secret detector. Unix children use a separate process group. Windows children start suspended and enter a kill-on-close Job Object before their primary thread is resumed, preserving the reviewed ToolHelp adapter and hidden-console behavior. Stop and normal parent exit clean up owned descendants and reap the direct child; readers are cancelled if inherited pipes remain open. Intentionally detached/custom external players are outside that ownership boundary. Navigation, webview reload and Twitch logout do not stop already-owned sessions. Restart and Stop remain available after logout; new browsing launches require an authenticated session. Main-window Close defaults to the existing shutdown path. With opt-in background close, it hides the window when a usable tray exists, otherwise minimizes it. Normal minimize is unchanged. Explicit Quit always enters the same shutdown coordinator: cancel/join the monitor, close service gates, signal sessions, cancel pending restart, await sessions/probes/authentication/chat cleanup, drain native notification ownership, then exit. Tray and main-interface Quit labels state the active playback count that will stop. `kill_on_drop` and group/job ownership are backups. An uncatchable Unix app kill cannot guarantee cleanup; no process-recovery daemon is implemented. ## Kick playback boundary `PlaybackStream` is a closed, service-tagged union: `Twitch(TwitchPlaybackStream)` preserves Helix stream/broadcaster IDs and metadata; `Kick { slug: KickSlug }` and `Youtube { locator: YoutubeLocator }` contain only a validated locator. `StreamingService` has exactly Twitch, Kick and YouTube. No generic provider interface, second supervisor or Kick/YouTube cache exists. `KickSlug` has a private string, validates construction and deserialization, accepts 1–100 ASCII letters/digits/underscores/hyphens, and normalizes lowercase. This application input bound does not assert Kick's registration policy. `play_kick_channel` accepts only `{slug, quality}`. A bounded service-owned operation validates the slug, resolves existing global/profile settings, discovers/probes executables, and uses the same `LaunchSpec → CommandSpec → Supervisor` path. It never obtains an auth lease or calls Helix/Kick HTTP. Rust alone constructs `https://kick.com/`; the webview supplies no URL. Twitch launch retains its unchanged auth/Helix checks before and after probing. Public deep-link grammar remains Twitch-specific and cannot launch Kick. `SettingsStore::effective_anonymous` (shared by Kick and YouTube) uses the existing resolution function with empty overrides, without looking up a sentinel or numeric Twitch ID. It disables automatic chat and clears Chatterino routing in the immutable snapshot. No settings schema, persistent slug record, bookmark or hidden entry is added. Restart retains the validated service identity and resolves current preferences; it never acquires Twitch authentication for a Kick run. Command construction selects `--kick-low-latency` or `--twitch-low-latency` from the closed service union. Player modes, literal arguments, version guards and quality selections are shared. Audio has no video fallback and no separate rendition query. Nonzero process exit remains a typed process failure, never authoritative offline/quality/browser status; child text is not parsed for lifecycle decisions. UI failure guidance mentions browser/challenge and quality availability without claiming a detected cause. Streamlink owns Kick media access and its browser challenge. The app does not contact Kick APIs, inspect challenge profiles, read cookies or add browser configuration. Streamlink/browser descendants inherit existing process-group/job ownership; deliberately detached children remain outside it. For Kick, both pipes are bounded and drained, but all child text is discarded before snapshot retention; only fixed supervisor diagnostics remain. Anonymous support summaries add the closed service enum, never identity, URL, child logs or browser data. Native challenge cleanup and actual player output must be accepted on each target OS. `open_kick_browser_chat` accepts only a slug, validates it and constructs `https://kick.com/popout//chat`. It uses the existing bounded browser opener independently of playback and authentication. Kick chat targets have no Twitch login and cannot dispatch to Chatterino even if accidentally passed the saved Twitch provider. No embedded chat or automatic Kick chat is provided. Open Channel reuses the exact Twitch lookup component, adds a native service selector and a Kick play/chat form, and is reachable while Twitch is signed out. Watching renders service, locator and process/preferences only for Kick. ## YouTube playback boundary YouTube follows the Kick boundary with three differences. `YoutubeLocator` has a private string and accepts exactly one of three non-overlapping shapes: an 11-character video ID (`[A-Za-z0-9_-]`, case preserved), an `@handle` of 3–30 ASCII letters/digits/periods/underscores/hyphens (normalized lowercase), or a 24-character `UC…` channel ID (case preserved). It is a locator, not a verified identity, and never a URL. `play_youtube_stream` accepts only `{locator, quality}` and shares the Kick launch path (`Services::play_anonymous`) without auth leases or Helix/YouTube HTTP. Rust alone constructs `https://www.youtube.com/watch?v=`, `https://www.youtube.com/@/live` or `https://www.youtube.com/channel//live`. Streamlink has no YouTube low-latency option, so command construction emits no low-latency flag for YouTube even when the effective preference is on; it is not replaced with generic HLS tuning, and Watching reports low latency as unavailable. Quality selections are shared: YouTube offers no `high`/`medium`/`low` names, so High/Medium/Low fall through to `best` with the existing sorting exclusion. YouTube has no browser challenge, so child output keeps the normal bounded, redacted Twitch retention for diagnosing consent or bot-check failures; it is still never parsed for lifecycle, offline or quality decisions. No cookies, browser profiles or Google credentials are read or passed. `open_youtube_browser_chat` accepts only a locator and opens the fixed `https://www.youtube.com/live_chat?v=` popout through the existing browser opener. Only a video ID identifies a chat without an API lookup, so handles and channel IDs return `invalid_youtube_locator`; the UI disables or hides the chat action for them. Support summaries add `service=youtube` only. ## Authentication and settings One Rust service serializes credential restoration, device polling, validation, refresh, and logout. An auth-only async mutex protects operations spanning HTTP and secure-store writes; it is independent of the playback locks. A separate watch snapshot makes `auth_status` immediately readable during network or keychain waits. Explicit cancellation signals the pending device request before waiting for the credential owner; late completion cannot reauthenticate after cancellation or logout. Auth operations belong to service tasks, not to the lifetime of an IPC caller. Dropping a caller cannot abandon a token rotation or let a queued keychain write land after logout. Concurrent explicit validation/refresh calls share completed operation revisions; concurrent Helix 401s verify the original session before comparing credential generations and sharing one refresh. An internal access lease carries a secret token plus account/session/generation identity and cancellation; it is never serializable. Logout/revocation/shutdown cancel leases. A later login has a new session identity, so it cannot reuse an old account's cache entries or supply a replacement lease to an older logical Helix operation. The Rust startup configuration resolves an explicit `TWITCH_CLIENT_ID` developer override before the public ID embedded from `TWITCH_CLIENT_ID_BUILD`. Missing or invalid startup configuration is an error before secure storage is opened. `build.rs` requires an embedded ID for non-debug Cargo profiles and Tauri `custom-protocol` builds, including packaged debug builds; a runtime override cannot satisfy that requirement. The public ID may be present in the binary, while OAuth token persistence remains exclusively in the platform credential store. See [configuration precedence and build commands](authentication.md). Startup and hourly validation run in Rust independently of the webview. Identity, client ID, scopes, and expiry must validate before the account is exposed. Refresh preserves the validated account ID. Transient validation errors hide identity and retry after 60 seconds. Refresh occurs at known expiration, recoverable access-token validation rejection, or a Helix 401, and never blindly retries a one-use refresh token. Newly issued tokens are validated without recursive refresh. Secure deletion must be confirmed by an absent-entry read before rotation is dispatched. Normal shutdown cancels device polling, rejects queued/new auth work, and waits for an active rotation and secure-store write; process shutdown proceeds concurrently. `CredentialStore` remains the Rust-only storage interface. `CredentialVault` serializes its blocking OS calls on blocking workers. Desktop uses Keychain, Credential Manager, or Secret Service; memory storage is for deterministic tests. A process-held filesystem lock prevents two instances using this application's configuration directory from rotating the same entry. Credentials are a single versioned secure-store record, separate from settings. There is no insecure fallback. Token types have no Debug/IPC serialization and owned token buffers zeroize on drop; networking/OS libraries may have their own temporary copies. See [authentication](authentication.md) for rotation and storage tradeoffs. `TwitchHttp` owns one reqwest connection pool shared by OAuth and Helix. The Helix client owns its shared rate coordinator and bounded cache, with typed endpoint methods and no arbitrary endpoint/URL IPC. The cache has distinct live/metadata/category lifetimes; stale results and incomplete batches are explicit. Foreground callers control each pagination step; the monitor deliberately assembles one bounded followed-live scan. A small internal clock helper accumulates elapsed time from monotonic and wall clocks for authentication, cache age and rate resets. System sleep counts even when monotonic time pauses; backward wall adjustments cannot rewind freshness or extend an observed deadline. Short waits remain Tokio timers. See [Helix contracts](helix.md). React holds UI inputs and safe backend snapshots. It reconstructs auth from status/account commands after reload and discards late account responses after logout. The shell polls auth at most once per second after completion, reads account details once per public auth-session generation, and does not reload account state on navigation. The application playback hook independently polls Rust session snapshots at most once per second after completion, including when Watching is closed. A request revision discards polls predating a completed action. Reload reconstructs sessions from Rust; React only owns pending-request guards, panel visibility and a draft restart quality. Failed account reads retry after 60 seconds. Pending auth, cancellation, probe/launch, and each session's Stop have separate guards; the backend remains authoritative. There is no frontend Twitch API client or authoritative query cache. `SettingsStore` owns strict version 9 JSON: a `version`, typed global `settings`, and a sparse `channelOverrides` map. Globals retain all released fields, including `chatProvider`, `chatterinoPath`, `profiles`, `selectedProfileId`, `discovery`, `shortcuts`, and `uiLanguage`, and add `themePalette`. Versions 1–8 migrate in memory without rewriting the old file; only a successful save writes v9. Version 8 gains `themePalette: "default"` and retains its System/Light/Dark `theme` value. Version 7 gains `uiLanguage: "system"`, while version 6 gains empty discovery lists and the native platform shortcut defaults. The v5 migration preserves every released field and channel override, defaults to Browser with no path override, and starts with an empty profile list/no selection. Old schemas cannot smuggle in new fields. No legacy GUI or prior identity namespace is imported. Update checks are manual, so no update preference is needed. React uses the bundled English, German, Spanish and French catalogs in `src/i18n/` for interface text. The selected `uiLanguage` lives in Rust settings; System resolves through a narrow, read-only native locale command so the webview, tray, notifications and macOS About agree. Browser-only preview uses `navigator.languages`. Unsupported languages use English. A separate narrow `save_ui_language` command saves a choice without replacing unrelated settings or drafts. Translation strings render as text; remote data and arbitrary backend error messages never become translation templates. See [translations](translations.md) for contributor rules. The directory comes from Tauri's `app_config_dir()` (XDG configuration on Linux). Reads/writes are bounded to 256 KiB and 1,000 channel records. A document mutex serializes snapshots and updates; global saves preserve channel records, and path probes update only the path. Discovery has a narrow atomic mutation; whole playback-settings drafts preserve its current stored value. The window-level settings coordinator retains mutation guards across Settings, filter, channel and Developer tools remounts, with at most eight outstanding intents in total. Global/path/language mutations share one serialized queue; channel writes use stable broadcaster IDs and may complete independently of global writes. Overlapping full drafts for the same channel are rejected. Only successful responses advance the accepted-snapshot revision; late initial global reads cannot replace a newer accepted global snapshot, and failed attempts do not invalidate initialization or prior successes. Channel reads wait for relevant pending mutations and retry reads overtaken by mutations, obtaining effective values from Rust. A global snapshot refresh never resets the channel save guard or suppresses its successful result. Forms overlay explicitly edited fields on accepted snapshots, including individual player/background/channel fields. An unedited reopened global form adopts a pending save when it completes; a reopened channel waits for its pending save before reading. Full shortcut drafts have a coordinator-owned pending guard: a reopened shortcut editor waits for the previous save before enabling edits. Deliberate edits survive unrelated reconciliation. Channel components remain keyed by broadcaster and authentication session, so old UI callbacks cannot populate a new identity. Both normal and Developer tools path probes use the same global guard and adopt a Rust settings read after successful probing. No channel snapshot cache or frontend inheritance resolver is introduced. Blocking writes use a temporary file in the same directory, sync it, then atomically replace the destination. Failed persistence leaves memory and the original file unchanged. Malformed JSON, invalid enums/paths/IDs and unknown schema fields/versions fail safely; desktop setup reports the categorized error and exits without panicking or replacing the file. Explicit Streamlink paths are probed and require version 8+ before save, and player paths are resolved/validated before save. Channel records use stable positive decimal broadcaster IDs (at most 32 ASCII digits), never login/display names. Each independent `quality`/`automaticChat`/`notifications`/`lowLatency` field is nullable: null inherits, while false explicitly disables the respective boolean preference. Rust resolves notification overrides against the global notification default separately from playback configuration. An entirely inherited record is removed. These are local device preferences shared across signed-in accounts, not account-specific Twitch data. Channel settings cannot grant playback or chat authority; those operations separately validate the current authentication session and trusted identity. Rust alone resolves global preferences, then the selected profile, then channel overrides, then optional request quality into `EffectivePlaybackSettings`. Channel previews return this same resolution. `LaunchSpec`, command construction and the retained session snapshot carry a copy; changing preferences cannot modify a running process. Restart retains the session identity, reaps the old generation and resolves current preferences; the UI sends null by default or an explicitly selected quality. Frontend drafts never independently resolve inheritance. The narrow `channel_settings` and `save_channel_settings` commands return the selected channel's overrides, global defaults and effective preview. There is no arbitrary settings-key API, filesystem destination or token field. Global, profile and channel updates are separate typed operations. Profile CRUD/selection uses the same bounded global frontend mutation queue and the backend Streamlink/settings operation lock; it has no independent revision counter. The profile write owns its operation guard through the blocking atomic write even if its caller disappears. Global updates preserve profile contents/selection under the document mutex, so a stale global draft cannot resurrect a deleted profile or overwrite selection. Successful custom-path probes save the validated absolute path selected by the user, preserving stable symlinks across package upgrades. Discovery resolves its current target for each probe and launch; the resolved path is diagnostic output, not the persisted preference. ## Shell, settings and navigation The compact sidebar exposes Following, Live, Categories, Search, Open channel and Bookmarks; account controls remain in the top bar. A small in-memory route union adds category, channel, login and team detail routes. No browser URL router or new component framework is needed for this desktop application. A bounded twelve-entry history across Back and Forward records route, search query/type, Following mode, language, scroll position and focused item; native buttons provide keyboard activation and visible focus. The minimum native window is 620 × 600; grids adapt and navigation becomes horizontal at smaller preview widths. System/Light/Dark remains a Rust-persisted color mode, separate from a Rust-persisted closed theme palette selection. Default leaves the existing CSS tokens intact. Eight bundled palettes adapt Sidra's dark/light color ladders to this app's semantic tokens; bundled text, accent, live, and focus colors are adjusted only when necessary for contrast on app surfaces. Dracula always resolves Dark without changing the stored color mode. The appearance hook listens to `prefers-color-scheme`, updates root mode/palette/resolved state and native control color-scheme, and removes its listener on unmount. System follows the preference exposed by the native webview; no desktop-specific theme integration is assumed. A bounded, validated `custom-theme.json` beside `settings.json` supplies only complete hex color palettes through a narrow Rust IPC command. React polls that command only while Settings is open or Custom is selected. Missing or invalid files hide the Custom option and resolve the stored Custom selection to Default without erasing it; a valid file restores it. No account, query, cursor, credential or preference authority lives in localStorage. `DeveloperTools` retains the old screen and process actions, unmounting the browsing shell while open. Stream cards and live channel rows/details now provide a separate keyboard-accessible Watch action. Watching and playback settings are compact, scrollable panels; opening either leaves the browsing workspace mounted. Normal UI renders fixed typed-error messages rather than arbitrary backend error text. Settings has Playback, Streamlink, Player, Appearance, Background, Updates, Shortcuts and Hidden items sections with one visible form section. A draft survives section navigation, Save accepts the validated backend snapshot, Cancel restores it, and closing the panel discards unsaved changes. A successful Streamlink test separately saves only its path. Channel details provide independent inheritance selectors and manual chat. Loading/error/pending states remain explicit; late channel responses are discarded across navigation and account changes; global changes reconcile the accepted preview without discarding pending channel saves or deliberate edits. Focused keyboard handling is a DOM listener in the application shell, not an OS hotkey. Control+K/1/2/3/4/5/6/comma/R selects Search/Following/Live/Categories/Watching/Open channel/Bookmarks/Settings/Refresh on Linux and Windows; macOS uses Command. Back/Forward default to Alt+Left/Right on Linux/Windows and Command+[/] on macOS. Home defaults to Alt+Home. Navigation reuses the existing bounded route history and focus restoration; Search focuses its input. Open channel uses the exact lookup view and does not start playback. Refresh delegates to the current browsing control. Inputs, textareas, selects, editable content, composition, repeated keys and modal dialogs suppress shortcuts. Settings provides a keyboard-accessible capture editor with platform labels, visible conflicts, unassignment and reset; developer diagnostics does not install a second shortcut map. ## Restricted external browser chat `open_channel_chat` accepts only a broadcaster ID and public auth-session marker. Helix resolves a fresh user login bound to that exact session, including offline users; a mismatched/invalid response fails. The service rechecks the lease before dispatch. `ChatTarget` validates the login and constructs only `https://www.twitch.tv/popout//chat`. React/provider metadata cannot supply a URL, executable, browser arguments or token. Automatic chat runs in Rust once after a successful launch or explicit restart, using that run's immutable effective preference and retained trusted login. Existing session restart remains possible after logout and does not re-resolve an old stream under another account. A failed chat dispatch records a safe generation-checked `chatError` without failing or hiding playback. Navigation and snapshot polling never open chat. `BrowserChat` has four bounded request slots and serializes native dispatch. A service-owned task retains the blocking call if an IPC caller disappears; queued requests recheck cancellation/shutdown before dispatch, and shutdown waits for the in-progress native opener. Linux uses GIO's default URI handler with a private worker context, awaiting asynchronous dispatch and allowing GIO to reap short-lived browser launchers. This also fixes the pre-existing Linux authentication browser opener's unreaped launcher. macOS/Windows retain the existing `webbrowser` adapter. GIO was already in Tauri's GTK dependency graph; it is now an explicit optional Linux desktop dependency, with no newly resolved package. See [GIO dispatch semantics](https://docs.gtk.org/gio/type_func.AppInfo.launch_default_for_uri_async.html). The authentication command still accepts no arguments and opens only its validated activation URL. Both commands use the same internal native adapter; no generic URL IPC is introduced. Browser pages use the browser's own Twitch session, and browser lifetime is independent of owned playback. Neither normal tests nor the isolated Linux browser regression open a real browser. ## Browsing IPC and DTO boundary | Command | Request | Response | | --- | --- | --- | | `list_followed_streams` | `BrowseRequest` | `PagedResult` | | `list_followed_channels` | `BrowseRequest` | `PagedResult` | | `list_streams` | `StreamBrowseRequest` | `PagedResult` | | `list_categories` | `BrowseRequest` | `PagedResult` | | `list_category_streams` | `CategoryStreamsRequest` | `CategoryDetails` | | `search_channels` | `SearchRequest` | `PagedResult` | | `search_categories` | `SearchRequest` | `PagedResult` | | `get_channel` | `EntityRequest` | `ChannelDetails` | | `lookup_channel` | `LookupChannelRequest` | `ChannelIdentity` | `BrowseRequest` carries a public `sessionId`, an optional cursor, and an explicit refresh flag. Entity/search requests add an ID/query, never an arbitrary endpoint or URL. `AuthStatus.sessionId` is a public generation marker, not a credential or authority grant. Rust obtains an authenticated lease for that exact session before querying and pins every enrichment/retry to it. Logout or a later login cancels the original lease. React keys its whole browsing workspace by this marker, clearing all navigation snapshots even when the same user logs out and back in. Thin command adapters acquire one of eight browse permits and invoke `helix::browse`. This bounds outstanding searches and rate-limit waiters without queuing unlimited IPC work. Authentication and process management have independent capacity. Query DTOs distinguish stream ID, broadcaster ID, login and display name. Optional category/description/language/image fields are explicit; DTOs exclude raw provider bodies, tokens, email and private account metadata. Errors are typed codes rendered through fixed user-facing messages. Following channels enriches only the requested page with one user metadata batch and at most one live-status batch. Fresh positive live matches from the first followed-stream page reuse the existing backend cache; unseen IDs are still queried, since omissions in a paginated response never prove offline status. Explicit refresh bypasses live reuse. A successful complete stream response can establish offline state; network failures or incomplete responses produce unknown status and warnings. Partial metadata failure preserves follow identity and follow date. Authentication failures remain terminal. Channel details use the same distinction. ## Freshness, pagination and rendering bounds The existing Rust cache remains authoritative: 15-second live data, 300-second user metadata, 3,600-second category metadata, at most 128 entries/8 MiB. Cross-view reuse uses the same session-scoped query keys and TTL checks, with no second cache. Refresh bypasses live/query cache entries; long-lived profile/category enrichment may still use its own fresh metadata entry. Result DTOs report network/cached/stale state, maximum source age and partial-detail warnings. Each list command requests 30 items and returns an explicit cursor. React requests another page only on Load more, deduplicates by the correct entity ID, rejects repeated cursors and retains at most 10 pages/300 items per view. Refresh starts that view over. Its `ViewMemory` retains at most twelve view snapshots for navigation; it has no TTL, background refresh or independent freshness rules. Restored and accumulated snapshots are labeled Previous results; failed refresh keeps existing data with an update-failed message. Only Rust can return a fresh/cached result. The interface does not automatically fall back to expired backend entries. Search debounces for 350 ms, trims input, ignores empty queries and separates channel/category cursors. Editing the query immediately unmounts older results, so a late response cannot replace the new query even during debounce. Superseded IPC calls may finish under the existing HTTP/rate bounds and eight-query capacity; account cancellation terminates them at the backend. Request generations and unmount guards also protect refresh, pagination and account reads. Shared `StreamList`/`StreamPreview`, category artwork and channel rows keep presentation consistent. Rust permits only Twitch CDN HTTPS image URLs and substitutes bounded dimensions (480 × 270 preview, 144 × 192 artwork, compact profiles). React loads images lazily, keeps aspect ratios reserved, uses empty alt text alongside labeled controls, and displays placeholders on failure. No image proxy, original-resolution fetcher or virtualization layer was introduced. Loading, empty, missing permission, unavailable, offline and network-error states remain distinct. Use [testing policy](testing.md) for current automated and native acceptance. [Release notes](releasing.md) and the [roadmap](road-to-1.0.md) summarize released milestones without treating old test counts as current evidence. ## Background monitor and native lifecycle `Services` owns one `Monitor` task alongside its existing `AuthService`, shared `HelixClient`, settings and sole playback Supervisor. `desktop` starts it once; React routing, visibility and reload cannot start or stop it. Auth snapshots expose only the internal session marker and cancellation token to the monitor. Every scan pins that session through pagination, retries and lease reacquisition. Temporary auth unavailability pauses observation and requires a quiet recovery baseline, but retains deduplication while the original session token remains uncancelled. Logout/session cancellation or replacement drops that session's transition history. New login starts quietly, including re-login to the same user, and old notification targets remain cancelled. Persistent settings are `monitoringEnabled`, `notificationsEnabled`, `closeToBackground` (all false by default) and `intervalSeconds` (60, 120 or 300; default 60). Pause is ephemeral: it cancels polling and outstanding notification actions without logging out or stopping playback, lasts until Resume or restart, and resumes with a quiet baseline. Changes to background preferences also cancel/rebaseline. Other global settings saves do not reset monitoring. The monitor runs one complete bounded scan per interval, with 0–10 seconds of per-process jitter and a 45-second deadline. Ordinary scans share fresh Following pages. Every quiet baseline instead uses `CachePolicy::Refresh` for all pages, fetching after the new observation boundary without invalidating unrelated cache entries. All scans yield scarce request capacity/budget to interactive work; [Helix contracts](helix.md#background-followed-live-scans) specify the limits. Incomplete scans are errors, never a truncated baseline. Recoverable errors retain a marked previous count and back off exponentially from the chosen interval, capped at 900 seconds plus jitter. A successful recovery is a quiet baseline. There is no unbounded polling log: the snapshot carries the current phase, error category, retry delay, previous/live count and notification-delivery failure flag. Transitions use `(broadcaster ID, Twitch stream ID)`, independent of names or titles. Initial streams enter history silently. Two complete missing scans confirm absence for the live count; failures/incomplete scans never count as absence. Reappearance of the same stream ID remains suppressed even after confirmed absence. A new stream ID is a new event. History is bounded to 16,000 entries; reaching capacity discards history and establishes another quiet baseline. Deduplication survives frontend reload and retries, but intentionally does not persist after app exit. At most ten eligible notifications are dispatched per scan; other observed events are marked seen and suppressed, not replayed later. The existing suspend-aware `Clock` drives scheduling. A gap over ten seconds between one-second heartbeats means suspend or a long scheduler stall: refresh the quiet baseline. During an in-flight scan, the heartbeat can cancel the scan; result acceptance also checks the last heartbeat before overwriting the elapsed boundary or accepting transitions. A response that completes before the first post-resume heartbeat is therefore discarded too. Recovery fetches a quiet baseline without clearing shared live cache pages. Ordinary slow scans may continue up to the scan deadline while heartbeats run. Auth validation and rate/cache expiry keep their existing suspend-aware behavior; the monitor never bypasses an auth lease. Native notifications use a bounded 32-command queue and one worker. OS notification records are capped at 32 and retired on cancellation, expiry (15 minutes), applicable close/click events, or shutdown. Every dispatch and activation rechecks auth/monitor cancellation. Text is control-stripped and bounded; markup/XML is escaped where needed. No remote image retrieval is required. Failure never stops Twitch monitoring. Permission is exposed separately and is requested only by the explicit UI action. The opt-in `notification-acceptance` Cargo feature adds a fixed `dev_notification_test` send/clear command and a separate local-main-window capability. `build.rs` rejects the feature outside debug/test profiles. Developer tools use the production notification queue, adapters, opaque activation IDs, restore/focus path and action acknowledgement. The sole synthetic target (`notification-acceptance`, broadcaster `0`) opens a labeled local view without Helix or playback; only feature builds exempt this exact pair from the desktop action's auth-session check. A separate cancellation token lets Clear retire only test notifications through normal adapter cleanup. It never changes settings, auth, monitor baseline or deduplication. Delivery failures can still set the existing native-delivery error flag. See [manual acceptance](notification-acceptance.md). - Linux uses GIO and `org.freedesktop.Notifications`, detects advertised action support, and reports OS-managed delivery. `desktop-entry` matches the packaged `Stream GUI RS.desktop` basename. Notification-server owner changes clear old action IDs. A missing server reports unavailable and is checked again later. - Windows uses WinRT toast APIs with `io.github.stream-gui-rs` and a balanced worker apartment. Banner timeout retains the notification and its activation target for Notification Center. A running COM activator is registered under the same AUMID's per-user `CustomActivator`; both banner and COM callbacks resolve a random opaque ID in one bounded registry and consume it once. Native callbacks cannot supply channel IDs, URLs or commands. At most 32 targets/handler sets live for 15 minutes, including time asleep. User dismissal, activation, cancellation, expiry or Quit removes handlers and the exact tag/group history entry. A session-local identity guard prevents a second instance from replacing the route or clearing the first instance's notifications. Quit revokes the COM factory and activation route; no executable/cold-launch registration is added. Startup removes orphaned history after acquiring that identity. OS display identity and notification preferences remain. Installed application testing is required; delivery/activation from a bare portable executable is not promised. Failed native delivery is safely categorized. - macOS uses UserNotifications with the matching app bundle ID, explicit authorization, bounded retained requests and a native delegate. A bare executable is unavailable. Permission checks remain asynchronous and bounded while the user considers a prompt; the worker has short-lived autorelease pools. Tauri's tray API provides Show, Hide, Pause/Resume, Watching count, About and Quit. A one-second native presentation loop reads Rust snapshots; it owns no monitoring or playback state. Linux checks the indicator library before calling the dynamically loaded API, reads the StatusNotifier watcher’s registered-host property (watcher presence alone is insufficient), disables Hide without a host and restores a hidden window if the host disappears. Tray failure leaves a reachable minimized window for background close. Menu actions are the portable interaction contract; pointer-event parity is not assumed. The Linux window title bar is separate from the tray. During Tauri setup, `desktop/titlebar.rs` adapts Tao's existing Wayland `EventBox`/`HeaderBar`: child controls receive input first, the unused subtitle row is disabled, and scoped GTK CSS compacts the header and its minimize/maximize/close buttons. The realized title bar and its native handlers remain in place. X11 window-manager decorations are unchanged. GTK is an explicit Linux desktop dependency already present through Tauri; backend-only builds do not enable it. Notification activation restores/focuses the window and retains one safe `DesktopAction` in Rust until acknowledged. The latest action replaces an earlier unhandled action. Channel targets contain trusted broadcaster and auth-session IDs, never arbitrary URLs. `desktop_status` removes obsolete-account targets. React polls this small native snapshot every two seconds, guards late polls, reconstructs after reload and navigates once to the matching workspace; clicking does not launch playback. Accepted channel/Watching intents dismiss About/support-report modals and focus the destination after modal cleanup, including navigation to an already-selected target. Rejected session targets neither dismiss overlays nor receive a successful acknowledgement. Watching tray actions use the same snapshot path and work after logout. Quit always uses the existing shutdown coordinator. ## Discovery and playback contracts `StreamBrowseRequest` wraps the existing page request and a closed optional `StreamLanguage`; category streams add the category ID. The curated ISO 639-1 codes plus `other` become ordinary Helix Get Streams query parameters. Any omits the parameter. Following/search DTOs do not accept this filter. Existing query-keyed Rust cache entries therefore separate languages and categories under the original session. Discovery cursors wrap the opaque provider cursor with session/language/category scope (16 KiB envelope, existing 2 KiB provider limit); Rust rejects mismatched scopes. This is an input consistency envelope, not an authentication capability or another cache. Navigation records a visit's language. Changing the filter clears the destination view's retained pages and remounts its query generation; Back restores the earlier visit without overwriting the saved default. The workspace still retains twelve snapshots and ten pages/300 items per view. Late persistence may update the saved default without changing a newer visit or stealing focus. Exact lookup normalizes one 1–25 byte ASCII alphanumeric/underscore login to lowercase, without accepting URLs, whitespace, controls or Unicode. It calls Get Users with `login` through the existing bounded Helix client and original session. It returns only broadcaster ID/display name after an exact match and session recheck. The existing channel-detail route then handles live/offline/unknown states; lookup is never a launch or automatic-chat intent. `lowLatency` resolves global → nullable channel override into `EffectivePlaybackSettings` and immutable `LaunchSpec`/session snapshots. A true value emits exactly one `--twitch-low-latency` before the `--` boundary. Quality/player presets, argument encoding, capacity and the sole Supervisor are unchanged. Explicit Restart re-resolves current settings for the retained trusted session; another session remains untouched. No numeric latency target is claimed. ## Accessibility, support report, and About `TextScale` is a closed enum serialized as `"100"`, `"125"`, or `"150"`. Rust persists it; React applies only accepted settings as root percentages, with rem text sizes and wrapping controls/cards. Single-line inputs, dropdowns and action buttons share a 2.125rem control height (34px at a 16px system base); buttons can grow for wrapped labels, while navigation, inline links and content cards retain their own layouts. Native dropdowns use normal line height and no vertical padding so the webview can center text without clipping descenders. Theme tokens and focus contrast remain shared. Panel focus restoration remembers the opener, uses a stable fallback if it disappears, and checks for a deliberate later focus move. Card descriptions expose live/offline/unknown and category/title alongside the action name; decorative images remain empty-alt. A bounded action status region announces returned process phases/errors, never log streams or inferred video rendering. `diagnostics::support` builds `SupportReport { text }` from an explicit allowlist, independently of `BackendDiagnostics`. It includes compile-time name/version/commit, OS/architecture constants, player mode, a known numeric Streamlink version or “not checked”, and the first sixteen anonymous retained process summaries (phase, typed failure, optional numeric exit). Output is capped at 64 KiB. It never serializes settings/sessions wholesale or includes IDs, usernames, channel/account metadata, paths, player arguments, timestamps, PIDs, raw output, HTTP, credentials or arbitrary error strings. A private path-associated version cache is populated by existing validated probes; preparing the report only reads snapshots and never spawns, reads credentials/environment, or performs HTTP. The modal is a preview with a read-only selectable textarea; copy/sharing is manual. `build_info::snapshot()` supplies one compile-time metadata source to About and the report; diagnostics uses the same constants. Desktop owns About dispatch. The tray entry is enabled independently of monitoring; the main footer offers the same intent when a tray is unavailable. macOS menu/tray/UI intents all call the existing native AppKit panel, retaining native icon and repository credits. Linux/Windows place a unique-ID `DesktopAction::About` in the existing action slot and restore the main window. React consumes/acknowledges it even while signed out, reusing one modal on repeated activation. There is no extra Tauri window, app shell, process owner or lifetime. The custom dialog contains the app icon, name, `Version ()`, fixed repository link, and Close. `open_repository` accepts no URL and dispatches only the compile-time repository through the existing native browser adapter on a blocking worker. It exposes only a categorized browser error. Main-window close/background behavior and explicit Quit remain authoritative for the dialog. Native HTML dialog handles focus/Escape/inertness; a small feature-detected fallback provides keyboard containment, background exclusion and restoration for older system WebKit builds without `showModal`. No dependency was added. ## Profiles and independent chat `config/profiles.rs` holds the bounded model: up to 16 UUID-identified profiles, case-insensitively unique trimmed names of 1–64 Unicode characters / at most 128 UTF-8 bytes, existing `PlayerSettings`, and nullable quality/low-latency preferences. IDs, duplicate names, arguments and dangling selections are validated before persistence. Create assigns the UUID in Rust; update preserves it. Delete clears an active selection atomically. There are no channel profile references or one-off launch profile selectors. Default player settings remain separate; no automatic profile is created during migration. A selected profile replaces the global player configuration; its optional quality/low latency overlay global values. Sparse channel overrides then win, and an explicit request quality wins last. The same Rust resolver builds channel previews and immutable effective snapshots, including the profile ID and chat configuration. Restart re-resolves from current settings. Saving/editing profiles validates executable paths; launch/restart resolves them again. Missing paths do not invalidate stored settings on startup. The safe support report uses the effective selected player-mode category and still omits profile IDs/names, paths and arguments. The existing restricted chat service serializes/bounds dispatch, keeps fresh session-bound identity for manual opens and records automatic-chat errors against the playback generation. `open_channel_chat` uses the accepted provider; `open_browser_chat` explicitly requests the browser with the same identity checks. Neither accepts a URL, executable or arguments. Provider choice/path are global; channel automatic-chat overrides remain sparse and independent. Missing Chatterino is an error, not a silent fallback. Browser chat remains a separate explicit UI action. `chatterino` reuses bounded native discovery and executable validation. PATH search is capped at 128 absolute entries; platform fallbacks cover Linux package paths, macOS Homebrew/system/user Applications bundles and Windows Program Files/Local App Data (including Programs). Explicit overrides are validated at use, preserving configured symlinks. No recursive directory scan, version probe, download or generic application framework is involved. On Linux only, native discovery can fall back to the host `flatpak` executable and the fixed `app/com.chatterino.chatterino` application reference. It probes the user installation first, then the default system installation, and retains that scope for launch. Named system installations are not searched. A single probe process at a time, discarded output and a shared two-second deadline bound discovery; timed-out info probes are killed and reaped. Missing Flatpak/app, failed probes and timeouts produce normal discovery absence. The selected source is an internal enum; the unchanged discovery DTO shows native paths or a fixed “Installed” label. It never writes a Flatpak command into the native override field. Chatterino receives exactly `--channels`, `t:`. The Flatpak path prepends only `run`, the detected `--user`/`--system` scope and the fixed application reference. The `app/` prefix prevents interpreting a runtime as a shell target; no user-supplied app ID, Flatpak option, shell or wrapper is accepted. These commands follow the [Flatpak command reference](https://docs.flatpak.org/en/latest/flatpak-command-reference.html). No display metadata, arbitrary arguments, app credentials or token substitution are accepted. The child environment is cleared and rebuilt from a fixed allowlist of OS/desktop/session essentials; arbitrary token, proxy and loader variables are not forwarded. Stdin/stdout/stderr are null. On Unix a new session isolates the chat from playback groups; direct `execve` in a minimal async-signal-safe pre-exec callback prevents libc's ENOEXEC shell fallback. Windows launches a native `.exe` with `CREATE_NO_WINDOW`, without suspension or assignment to a playback Job Object. Up to 16 direct Chatterino children have independent waiter threads. Cancellation is rechecked after discovery, before dispatch. A thread is created before spawning; successful spawn acknowledgement returns promptly, while that thread owns `wait` and releases capacity on exit. Shutdown drains dispatch, not the lifetime of the independent application; Stop/Quit never kill chat. Surviving Unix children are adopted by the OS when the parent exits, and Windows children survive closure of non-owning process handles. Upstream channel-only mode disables settings saving and may start a new window/process; cross-platform reuse is not promised. Record native observations and packaging gaps under the current [testing policy](testing.md). ## Manual release awareness `updates::Updates` starts with an empty Rust cache and performs no startup HTTP/client initialization. Parameterless `update_status` only reads it. `check_updates` uses a 24-hour cache including failures; explicit `refresh_updates` bypasses it subject to a 60-second minimum interval. A try-lock permits one service-owned check; concurrent/dropped IPC callers cannot duplicate or abandon its request/cache update. There are no timers, retry loops, startup checks or notifications. The existing suspend-aware clock expires the cache; shutdown cancels in-flight HTTP. A separate unauthenticated reqwest client contacts only `https://api.github.com/repos/ChrisLauinger77/stream-gui-rs/releases/latest`, with redirects/retries disabled, a ten-second connect/request deadline and a 256-KiB declared/streamed body bound. It never uses Twitch HTTP, auth headers, cookies or an API token. A small typed JSON projection reads only tag/URL/draft/prerelease; remote title/body/HTML never reach the UI. `semver` parses and compares version precedence (ignoring build metadata), using `build_info::VERSION`. Drafts and prereleases cannot produce an update offer. Stable remote tags must be canonical `vMAJOR.MINOR.PATCH` without build metadata, and their URL must exactly match this repository's release URL reconstructed from that tag. The status DTO contains only a closed phase and accepted version. Parameterless `open_update_release` reconstructs the destination again from the cached version and dispatches through the existing native browser adapter on a blocking worker. No URL arrives from React/settings and no installer/updater APIs exist. About still renders compile-time metadata independently, including its native macOS panel. Update status lives in Settings, including while signed out. ## Discovery preferences and navigation intents `config::discovery` owns two independent lists of at most 200 `SavedItem` records (channel/category, canonical positive decimal ID, presentation label). Labels are limited to 128 characters/256 UTF-8 bytes and reject controls/bidi overrides. Duplicate kind/ID pairs are invalid in persisted documents. `modify_discovery` is an idempotent add/remove intent; a repeated add updates only its label. The store checks the complete 256-KiB document bound before atomic replacement. These local preferences survive logout/account replacement; no Twitch follow mutations or secondary database exist. `config::shortcuts` defines twelve actions and one optional binding per action. Null intentionally unassigns. Bindings contain a closed validated key and boolean Primary/Control/Alt/Shift/Meta modifiers. Every action must be present, with no unknown actions. A complete older ten-action settings map gains the new defaults in memory on load; when an existing custom binding occupies either combination, the corresponding new action starts unassigned. Duplicate effective combinations are rejected for both macOS and non-macOS interpretations. Modifier-only, unmodified and reserved keys fail. Rust owns accepted settings; React owns capture/draft/error presentation. The global settings coordinator serializes these mutations with profiles and global saves, and the Rust owned operation guard survives a dropped caller through the blocking write. Global `SettingsStore::update` preserves discovery/shortcuts/profiles/selection inside the document lock, so a stale global draft cannot undo a completed mutation. Channel records retain their existing independent atomic store operation. Hide filtering projects the accepted Rust preference snapshot onto displayed results, without changing query snapshots, provider cursors or freshness. Live excludes hidden channels and categories; category streams exclude hidden channels; category discovery excludes hidden categories. Following, search, team members and direct channel/category navigation do not suppress their destinations. Bookmarks remain reachable even when hidden. Empty filtered pages retain Load more if the provider returned a cursor. A removed focus target falls back to the view heading or management region. `helix::browse::teams` projects the existing Helix Team wire model into `TeamDetails`. Exact team names use 1–100 ASCII letters/digits/underscore/hyphen. The bound `RequestSession`, shared foreground rate coordination, two-MiB HTTP body limit, five-minute metadata TTL and existing 128-entry/eight-MiB cache all apply. An unexpected team identity, malformed member or oversized description is rejected. At most 10,000 wire members and 16 KiB description are accepted; members are sorted by login/ID/name, deduplicated by stable broadcaster ID, and the first 300 are exposed with total/limit metadata. Helix supplies no Teams pagination. Members have unknown live state until existing channel details resolve it; playback still performs its independent fresh Rust validation. Team descriptions are rendered as literal text, never interpreted HTML/Markdown, and images use the existing Twitch CDN allowlist. `navigation` owns a strict raw-string parser and a one-entry `NavigationInbox`. The grammar is documented in README. Every accepted intent gets a UUID; replacing the entry drops the previous one and acknowledgement removes only a matching UUID. The inbox is installed before native plugin initialization, including cold-start input, so concurrent activation cannot be overwritten by startup. `desktop_status` returns the safe typed pending intent and `acknowledge_navigation_intent` accepts only its ID. There is no frontend URL-parser or execution command. No links, identities or shortcut contents were added to the support report. On Linux/Windows, the official Tauri single-instance plugin is registered first, before services or credential restoration. macOS uses native LaunchServices bundle activation and Tauri Opened/Reopen events; it does not compile the plugin’s additional Unix socket receiver. Its callback validates only the executable plus one link argument and restores the main window on the native thread. macOS `RunEvent::Opened` processes at most sixteen OS-normalized URLs and retains only the latest valid intent. Tauri CLI's `plugins.deep-link.desktop` configuration supplies installer/bundle protocol metadata; the deep-link runtime/JavaScript plugin is unnecessary. Linux DEB/RPM packages use `packaging/linux.desktop` with an explicit `%u` argument; AppImage generation reuses the Debian template. CI extracts the built DEB and exercises its launcher through isolated GIO associations with the native fixture, proving URI delivery without launching the app. No runtime association writes or global hotkeys are added. Invalid input is rejected with fixed text and never echoed. React handles native intents after settings/auth restoration; browsing intents wait for sign-in. A channel-login destination owns its asynchronous resolution inside the keyed visit, so a new route or logout unmounts it and late results cannot navigate elsewhere. Native acknowledgements retry without replaying an already handled destination. Frontend reload may redeliver only a still-unacknowledged intent. Existing notification actions preserve their separate session-bound checks. Refresh-on-focus is deferred because navigation snapshots do not decide cache age. A separate CLI is deferred because the Linux/Windows forwarding plugin does not expose an application-level result acknowledgement/exit-status contract. The executable's single URI argument is only the OS protocol entry point. No settings mutation, playback arguments, generic URL opening, remote server or later-phase feature is introduced.