Files
LlamaCasty/ViewModels/index.md
T
gramps 2f5d0c1759 feat(settings): position slide-out notifications
App Settings now chooses one of six ToastArea positions (left/right ×
top/center/bottom), defaulting to the existing bottom-right. The setting is
persisted per profile and the toast area moves immediately; edge gutters keep
bottom notifications above the footer.

ToastPositionIntegrationTests covers live repositioning and restart persistence.
617 tests green, clean build with 0 warnings.

Reference scan: OBS Notifier supports user-configured notification placement:
https://github.com/DmitriySalnikov/OBSNotifier
2026-11-11 10:06:54 -08:00

28 KiB
Raw Blame History

ViewModels — index

MVVM layer. See schema.md for the memory-map conventions.

Each landed MainViewModel partial owns a row here describing its area; the MainViewModel.cs row describes only what still lives in core. Docking most areas' docs in their own rows keeps this index small to read.

File Purpose
MainViewModel.cs Core. public partial class MainViewModel : ViewModelBase. Source commands + element rows: scenes are pure selection rows; sources carry edit/visibility/trash (EditElementCommand sets element.IsEditing, ToggleElementVisibilityCommand flips IsVisible, RemoveSourceCommand; Backdrop trash slot reserved via HiddenBoolToVisibilityConverter — TASK 13); shared NextSourceName (Image, Image2, Image3… no-space increment from actual names — used by AddSource + AddReusedImage), AddImageCommand. Lifecycle/ctor wiring: fields + ctor wiring (all command assignments, mixer/camera/chat/hotkey/subscription hookups), Shutdown/SaveLayoutNow/ScheduleSave/LoadLayout, CreateBrush + shared BarOnBrush/BarOffBrush (referenced by Audio/Trax/Socials partials), window-geometry pass-through, SelectedElement/OnSelectedElementPropertyChanged (the selection change also raises CanToggleClipShape — TASK 57), ShowPreviewPlaceholder, BrandFlash*, WebView2 hosting (InitWebView2/RegisterLoadedWebSources/OnWebView2PreviewBitmapChanged), frame-pump wiring (construction + _rtmpUrlProvider seam + Dispose, plus ResolveOutputFrame → cached TextOverlay frame / RenderChatBox / RenderAlertBox for text, chat, and alert layers) — the pump's stream-state consumers live in the Streaming partial.
MainViewModel.Account.cs Session + connected account: LoadSavedSessionAsync (DPAPI restore at startup), SignInAsync/ChangeAccountAsync/SignOutYouTubeAsync, IsConnected/AccountAvatarUrl/AccountDisplayName/AccountStatusLightToolTip via SyncConnectedAccount (cleared on End).
MainViewModel.License.cs Polar license (freemium): LicenseKey/LicenseMessage/IsLicenseValidating/IsLicenseEntryOpen/IsHubVisible/IsPremium, OfflineGracePeriod, ValidateLicenseAtStartupAsync/ActivateLicenseAsync.
MainViewModel.Scenes.cs Five-scene catalog: Scenes + StagedScene/LiveScene/IsEditMode, AddScene (canonical names only), MissingScenes/ShowAddScene/AddSceneCommand; scene wiring OnScenesChanged/WireScene/UnwireScene; transitions SelectedTransitionType/TransitionDurationMs/TransitionToScene/ApplyTransitionToLive; global hotkeys HandleHotkey/OpenHotkeyConfig/RefreshHotkeyLabels; scene-gating hints (CanChangeBackground).
MainViewModel.Zoom.cs Zoom box (TASK 52, creator-corrected 2026-10-08) ActivateZoom/ToggleZoom/DismissZoom/StartZoomMove/UpdateZoomMove/EndZoomMove + RefreshZoomUi/RebuildDimRectsForZoom. Right-clicking the live preview anchors a FIXED capture window (the box/2 region centred on the click) and parks a movable 2x magnifier. The box shows a live always-on 2x view of the anchored capture; dragging moves only the box — CaptureX/Y are deliberately untouched in UpdateZoomMove (creator ruling). Each shape is centred on the click and clamped independently (box to the master, capture to box/2 windows): Circle = square, VerticalRectangle = 9:16, HorizontalRectangle = 4:3 (w=h·4/3 with h=0.7·1080; NOT 16:9 — first cut read as a square). The transient ZoomSceneElement mirrors box + capture; the output compositor draws it LAST (layer n+1) from the already-composed frame (SceneCompositor.BlitZoom→ZoomMagnifier) so it captures EVERY layer. No per-frame builder/cache — the resolver case that built ZoomFrameBuilder frames was removed. BuildCompositorOptions (internal = test seam) always returns the tier crop — the box is never the crop source.
MainViewModel.Background.cs Screen backdrop (Live-only by policy): ScreenCaptureManager + ScreenCaptureSourceFactory (WinRT GraphicsCapture); background model EnsureBackground/CreateBackground/NormalizeBackgrounds/HealBackgrounds/StampDefaultBackgroundAsset; screen capture ReacquireScreenCaptures/ResolveAutoCaptureKey/RedesignateBackgroundAsync/ClearBackgroundCaptureAsync/SetBackgroundCapture/OnScreenPreviewBitmapChanged/OnFullscreenMonitorChanged/RefreshBackgroundAutoCapture/BackgroundShowDesktop (keys on full-screen-game monitor or primary display via Win32FullScreenDetector); display picker Displays/SetBackgroundDisplayCommand; preview fns ActiveBackgroundImage/BackgroundImage/UpdateActiveBackground/UpdateBackgroundImage/RefreshSnapshotsAsync/LoadBackgroundImage/CreatePlaceholderSnapshot; ChangeCaptureCommand/RefreshCaptureCommand/ToggleShowDesktopCommand/BrowseBackgroundCommand. ShowPreviewPlaceholder stays in core.
MainViewModel.Webcam.cs Webcam identity: _webcam/ReacquireWebcam/SwapWebcamIdentityAsync; per-scene configs AddWebcamToStagedSceneAsync/ShowWebcamInStagedScene/ChangeWebcamAsync (Windows picker always opens); capture OnCameraPreviewBitmapChanged/OnCameraFailed; gating CanAddWebcam/CanChangeWebcam/CanShowWebcamInStagedScene/IsWebcamSelected/WebcamError; size safeguards WebcamMax*/ClampWebcamToBounds/HealLegacySquareRect; AddWebcamCommand/ChangeWebcamCommand/ShowWebcamCommand. TASK 57 (the "vv" feature): the vertical (9:16) tier's camera placement — IsVerticalTier/VerticalBand + ResolveVerticalCameraLayouts/ResolveVerticalCameraLayout (the tier swap: the vertical tier brings each scene's own vertical placement in, the Live scene gets its small tile on first use, 16:9 puts the desktop layout back exactly) + MoveWebcamInVerticalTier/SnapWebcamInVerticalTier (the six-spot snap) + EnsureVerticalCameraLayout; the Chat scene's own rules (Unit 2) — IsChatSceneCamera/CanToggleClipShape (the circle is NOT offered in the vertical Chat layout) and ResizeWebcamInVerticalTier (the Chat camera's two sizes, then the snap; the Live camera resizes freely inside the band)
MainViewModel.Audio.cs Mic bar: read-only realtime meter AudioLevel/MeterFillWidth/MeterBrush (fill = Math.Min(1, AudioLevelMeter.ToDisplay((float)AudioLevel) * MicVolume), green→yellow→red at 60%/80% zones), MicVolume (0.8 default; drives MicMuted ⇔ volume 0; unmute flash BeginVolumeFlash/EndVolumeFlash/CancelVolumeFlash), MicSourceName (left-justified inside the bar, persists via LayoutStore.Settings), ToggleMicMute/PickMicrophone (MicPickerDialog; picked device swaps the live source via _audioMixer.RestartMic(), loopback keeps running), status dot MicStatus/MicStatusBrush/MicStatusToolTip (Models.MicStatus: green Connected / yellow Problem / red no-device), capture starts once at startup (StartMicCaptureAsync) — NOT go-live. Game audio bar: overlaid bottom-center, always visible (show/hide gating removed 2026-08-15); GameAudioLevel (loopback via OnLoopbackLevelChanged), GameAudioVolume/GameMuted/GameMuteText/ToggleGameMute/PushSystemVolume, mirror volume flash SetGameVolumeAdjusting/BeginGameVolumeFlash/EndGameVolumeFlash/CancelGameVolumeFlash. Mixer handlers OnMicLevelChanged/OnLoopbackLevelChanged/OnMicConnected/OnMicFailed.
MainViewModel.Trax.cs Background music (TASK 9, rides the loopback into the stream): _musicPlayer (MusicPlayer), IsMusicPlaying/TraxError/TraxDotBrush/TraxToolTip, interaction TraxLeftClick/TraxRightClick/PickTraxTrack, playback LoadTrax/PlayTrax/PauseTrax/OnTraxPlaybackEnded/RefreshTraxUi, UpdateTraxLocalGain (mirrors the game bar's mute onto MusicPlayer.LocalGain so the track ducks/restores with desktop audio).
MainViewModel.Socials.cs Social bar (global resource, overlaid on every scene): _socials/_socialBarFrame/_socialValidator (ISocialValidator/HttpSocialValidator), HasSocials/SocialBarVisible/SocialBarDotBrush/SocialBarGlowBrush/Socials/SocialBarTop, OpenSocialDialogCommand/OpenSocialDialog (spawns SocialsDialogViewModel + window; saved copy replaces _socials), NotifySocialsChanged/RenderSocialBarFrame (SocialBarRenderer → immutable _socialBarFrame the frame pump reads), fediverse heal HealFediverseSoftwareAsync (static, test-facing) + HealFediverseSoftwareInBackground, bar edge toggle SetSocialBarPosition/ToggleSocialBarPosition. BarOnBrush/BarOffBrush/CreateBrush stay in core (shared tokens).
MainViewModel.Streaming.cs Go-live/record + health + quality tiers + session UI. Stream state: StreamStatus/StreamVisibility setters + _streamStatus field, IsOffline/IsLive/IsLivePrivate. Go-live flow: StartSession (routes by pill state) → BeginRecordOnly (signed-out local recording) or BeginGoLive (GoLive dialog → captures title/description/visibility + BroadcastForm.CaptureGoLive → PrepareAndStartLiveAsync: reusable stream → CreateBroadcast (stores _currentBroadcastId) → empty-metadata warn → chat poll start (liveChatId) → _framePump.StartAsync() + health poll; failures → StreamStatus.Error, never a crash); StartRecordFile (auto-named ty-…-0000.mp4) + FinalizeRecordingAsync (rename modal, UniquePath); StopStream (posts Offline, stops chat/audio pipe/pump, clears _currentBroadcastId; session/token deliberately NOT cleared — sign-out stays explicit). Health: CurrentHealth/ResetHealth/ApplyHealth/OnFramePumpHealthUpdated (marshal FramePump.HealthUpdated off the stderr thread), PollHealthAsync/OnHealthPollTick/ApplyHealthIssue (report-by-exception banner HealthIssueBanner/HealthIssueBackground via StreamHealthReporter). Session timer: _liveTimer (ctor-wired) UpdateSessionTimer/OnLiveTimerTick/LiveElapsedText/_recDotPulse. Start/End UI: mode switch RecordPillOn/OnAirPillOn, top-bar worlds (ShowIdleWorld/ShowRecordAction/ShowStreamActions/ShowSignInButton/ShowAvatarButton, reality line RunningVisible/RunningText/RunningDotBrush, Test gate ShowTestButton/CanStartTest — RaiseTopBarModes()), ShowPrimaryStartButton/CanStartSession/ShowEndStreamButton/EndButtonText, REC sign RecDotBrush/RecTextBrush/RecDotOpacity/OnAirBrush, live window tokens WindowTitle/TopBarBackground/PreviewGlow* via UpdateLiveVisuals; ChooseRecordFolder/ResetRecordFolder/_recordFolder/RecordFolderDisplay/DefaultRecordFolder. Resolution dropdown: QualityOptions/SelectedQuality/ResolutionHelp/ApplyStreamQuality + OutputRect*/DimRects/IsOutputCropped/ResolutionBadgeText over the 1920×1080 master; TASK 57: the tier change also resolves every scene's camera placement (the vertical tier brings the vertical placements in, 16:9 restores the desktop layout — see MainViewModel.Webcam.cs). Pipeline bits: _framePump/_liveTimer/_healthPollTimer fields, BuildEncoderOptions. Commands StartStreamCommand/EndStreamCommand/ChooseRecordFolderCommand/ResetRecordFolderCommand. Broadcast pull-out: CurrentBroadcastId/BroadcastForm. TASK 57 (Unit 2): the tier change also raises CanToggleClipShape (the circle option disappears in the vertical Chat layout).
MainViewModel.Overlays.cs Settings/bug/feature/about panels + creator-hub links: panel state IsSettingsOpen/IsBugOpen/IsFeatureOpen/IsAboutOpen (via SetPanel → IsAnyOverlayOpen), IsLicensesOpen (flips IsHubVisible), OverlayTitle, ShowOverlay (one-panel exclusivity), ShowAbout (returns to the About hub) + ShowLicenses (loads shipped THIRD-PARTY-NOTICES.txt into LicensesText in-app); bug/feature report BugReportText/BugReportEmail/FeatureRequestText/FeatureRequestEmail + SubmitBug/SubmitFeature/ComposeEmail/OpenUrl; About hub consts ChannelUrl/MastodonUrl/CoffeeUrl + PremiumUrl (https://llamacasty.com, opens only when explicitly clicked) + IsPremiumAvailable + SupportEmail + AppVersionLabel; commands OpenSettingsCommand/OpenBugCommand/OpenFeatureCommand/OpenAboutCommand/CloseOverlayCommand/SubmitBugCommand/SubmitFeatureCommand/OpenChannelCommand/OpenMastodonCommand/OpenCoffeeCommand/OpenPremiumCommand/OpenLicensesCommand. A locked quality selection opens About without changing the tier. Ctor command assignments stay in core.
MainViewModel.Settings.cs App Settings drawer state + global slide-out toast position (ToastPosition), persisted in LayoutStore.Settings; ToastAreaMargin keeps the six supported positions aligned to the window edges/safe footer gap.
MainViewModel.Chat.cs Chat overlay + preview pipeline: ChatMessages (500-message cap; clear-on-fade), OnChatMessageReceived (marshal _youtubeChat.MessageReceived onto the dispatcher, switch previews mock→real, then feed both component layers), fade _chatFadeTimer/ResetChatFadeTimer (shortest ChatFadeSeconds across chat layers); live render RenderChatBox (called from ResolveOutputFrame, stays in core) + preview render UpdateChatBoxPreview (bitmap onto chatBox.VideoImageSource); mock preview MockChatMessages/StartMockChatPreview/StopMockChatPreview/RunMockChatPreviewAsync/_mockChatCts; UpdateActiveChatBoxPreviews (wires Chat* property-change subscriptions + first render); CanAddYouTubeChat (one-per-layout gate), ShowChatInactiveMessage (!IsLive). Alerts (TASK 43): same facade for the native alert box — _alertLayer (AlertOverlayLayer), RenderAlertBox/UpdateAlertBoxPreview, UpdateActiveAlertBoxPreviews, CanAddAlerts (one alert zone per layout). Ctor wiring (MessageReceived hookup, new ChatBoxRenderer(), new AlertRenderer(), LoadLayout preview start, _youtubeChat.Dispose + _alertLayer.Dispose) stays in core.
MainViewModel.Logs.cs Non-edit left-panel tab state and bounded current-launch log collections; refreshes the process-local AppLog snapshot while Logs is selected, pins Class 1 before Class 2 until acknowledged, then returns entries to the LIFO timeline; stops its dispatcher timer on shutdown.
GoLiveViewModel.cs Start Stream dialog: account row (saved channel shown with Change Account, or Sign in to YouTube; Start gated on IsSignedIn/IsBusy) + title/description + locked Visibility (always "Private" — ship step 7, no dropdown), start/cancel requests
About hub (TASK 11, in MainViewModel.Overlays.cs) the About overlay is the in-app creator hub: ChannelUrl/MastodonUrl/CoffeeUrl (public consts, live) + PremiumUrl (tabled billing seam — TASK 12 → the greyed "Unlock Premium" button lights up when filled); OpenLicensesCommand → ShowLicenses() loads the shipped THIRD-PARTY-NOTICES.txt into LicensesText and sets IsLicensesOpen (in-app scrolling panel, never the OS viewer); BackToAbout_Click in MainWindow.xaml.cs flips it off.
ReuseImageViewModel.cs Add Image dialog: candidate list (ReuseImageCandidate), reuse/new/cancel
CameraPickerViewModel.cs Add Webcam dialog: async camera list (loading / has / none states), UseRequested(CameraDeviceInfo)/CancelRequested
MicPickerViewModel.cs Select Microphone dialog (voice source): async mic list (loading / has / none states), UseRequested(MicrophoneDeviceInfo)/CancelRequested
SocialsDialogViewModel.cs Social Media Site Promotion dialog (six fixed slots, SocialSlotViewModel): row 0 = YouTube sign-in gate (sign-in provider fills the channel, delete = sign-out); rows 1-5 validated entries / empty / freemium locks 2-5 (isPremium seam); DetectService → ISocialValidator on confirm (input snaps to text + real logo on success, red do-not + stays editable on failure); CanSave blocked while any slot edits/validates/errors; BarEnabled switch; commit via CommittedEntries + BuildSocialsConfig() (YouTube first) + SaveRequested/CancelRequested. WPF-free — the window owns the dialogs (delete confirm, sign-in gate) and calls StartEdit/ConfirmEdit/DeleteSlotAsync.

| MainViewModel.Sources.cs | Source creation/removal; AddSource creates the standalone ticker layer with one-per-scene gating and its 1920×48 default geometry; TextOverlay starts as a visible 900×140 draggable text layer. | | MainViewModel.TextOverlay.cs | Raster-on-change TextOverlay preview hookup and settings/dimension invalidation; exposes font categories and selected-layer font/size options; output resolution reads its immutable cached frame. | | MainViewModel.AlertTicker.cs | AlertBox ticker preview routing: selects the preview scene's AlertBox settings and publishes the positioned box-sized frame to PreviewPane. |

Related: Models/index.md for the data; Services/index.md for what the ViewModels drive; base class in Helpers/ViewModelBase.cs.

MainViewModel partial split (in progress)

MainViewModel.cs is being split into one file per functional area, zero behavior change, one commit per partial (each commit a roll-back point). Commit naming: titles carry the series id (Commit N/12), tags are refactor-commit-N — e.g. git show refactor-commit-1. Every partial lands with a ViewModels/index.md update in the SAME commit. Each partial's docs live in its table row above; this tracker is just the code↔commit map.

Progress (Commit N/12 · tag):

  • Commit 1/12 · refactor-commit-1 — Account → MainViewModel.Account.cs
  • Commit 2/12 · refactor-commit-2 — License → MainViewModel.License.cs
  • Commit 3/12 · refactor-commit-3 — Scenes → MainViewModel.Scenes.cs
  • Commit 4/12 · refactor-commit-4 — Background → MainViewModel.Background.cs
  • Commit 5/12 · refactor-commit-5 — Webcam → MainViewModel.Webcam.cs
  • Commit 6/12 · refactor-commit-6 — Audio → MainViewModel.Audio.cs
  • Commit 7/12 · refactor-commit-7 — Trax → MainViewModel.Trax.cs
  • Commit 8/12 · refactor-commit-8 — Socials → MainViewModel.Socials.cs
  • Commit 9/12 · refactor-commit-9 — Streaming → MainViewModel.Streaming.cs
  • Commit 10/12 · refactor-commit-10 — Chat → MainViewModel.Chat.cs
  • Commit 11/12 · refactor-commit-11 — Overlays → MainViewModel.Overlays.cs
  • Phase 1 complete: core MainViewModel.cs split into 10 functional partials (1,890 → 758 lines).

Phase 2 — MainWindow.xaml split into UserControls (new Controls/ folder, namespace ytLive.Controls):

  • Commit 12 · refactor-commit-12 — BottomBar → Controls/BottomBar.xaml(.cs) (gear menu + stream stats + quality dropdown; GearButton_Click moved to the control's code-behind).
  • Commit 13 · refactor-commit-13 — HealthBanner → Controls/HealthBanner.xaml (Row 1). Also promoted the 7 value↔visibility converters out of MainWindow.Window.Resources into Themes/Controls.xaml so extracted controls can resolve them (app-level, not window-level).
  • Commit 14 · refactor-commit-14 — TopBar → Controls/TopBar.xaml(.cs) (Row 0: logo, REC/ON-AIR pills + signs, avatar, Sign In/Start/End buttons). Avatar fetch moved (async-preserved) into the control; window exposes _topBar.RefreshAvatar() invoked on IsConnected/AccountAvatarUrl changes.
  • Commit 15 · refactor-commit-15 — PreviewPane → Controls/PreviewPane.xaml(.cs) (Row 1 Col 1 center preview pane). Moved the 1920×1080 canvas (OverlayCanvas/SelectionOverlay/BrandFlashLayer/SocialBarElement), text pull-out, and the live-controls row (TRAX/game/mic meters + sliders + speakers + socials button). Code-behind moved: preview drag/select/resize/hit-test (Preview_Mouse*, HitHandle, HitElement), webcam-context-menu handlers (WebcamMenu_), social-bar glow (SocialBar_, SocialsButton_, SocialGlowTick), audio sliders/speakers (VolumeSlider_, GameVolumeSlider_, MicSpeaker_, GameSpeaker_, SetSliderValueFromClick, FindParent), TraxButton_. Window keeps the source-list left panel + thumbnail strip + edit UI, and routes selection-overlay via _previewPane.UpdateSelectionOverlay(bool) plus _previewPane.IsClickInsidePreview/Drawer. BackgroundMenu_* stayed in the window (used by the left-panel source context menu).
  • Commit 16 · refactor-commit-16 — SceneThumbnailStrip → Controls/SceneThumbnailStrip.xaml(.cs) (Row 0 scene-picker strip). Thumbnail_MouseLeftButtonDown moved into the control (transitions via DataContext.TransitionToScene).
  • Commit 17 · refactor-commit-17 — LeftPanel → Controls/LeftPanel.xaml(.cs) (Row 1 Col 0 left panel: layers/properties edit UI + live chat list). Moved ALL property-editor and chat handlers: SourceNameBox_*/CommitSourceEdit, AddSourceButton_Click, OnChatMessagesChanged (now self-subscribed on Loaded), MirrorButton_Click, ShapeButton_Click, SourceMenu_Remove, BackgroundMenu_*, OpacitySlider_ValueChanged, list drag-to-reorder (_dragStartPoint+List_Preview*+EndListDrag+FindItemContainerAt), and the snapshot family (TakeSnapshot, WebcamBorder*, Countdown*, Web*). Window exposes via _leftPanel.IsClickInside(original) (deselect guard) and _leftPanel.OnSelectionChanged(vm) (opacity label + snapshot). Pending: overlay host region.
  • Commit 18 · refactor-commit-18 — OverlayHost → Controls/OverlayHost.xaml(.cs) (settings/bug/feature/about dialogs). BackToAbout_Click moved with it (uses inherited DataContext). Window keeps the thin WebViewHostPanel + ToastArea root stubs so name lookups are untouched (WebView2 ctor init, NotificationService.InArea("ToastArea")) — toast/WebView2 behavior unchanged. Phase 2 COMPLETE — MainWindow.xaml carved down to a 123-line shell over 6 controls (TopBar, HealthBanner, BottomBar, SceneThumbnailStrip, LeftPanel, PreviewPane, OverlayHost). Pending: session wrap-up (HANDOFF.md) + push on approval. (The WebViewHostPanel root stub is since GONE — the 2026-09-14 composition-capture redesign moved InitWebView2() to MainWindow_Loaded and deleted the panel; see Controls/index.md.)

Phase 3 — 500-line compliance (production .cs only). Bring all production source files under 500 lines. One roll-back commit per file; same drill (0-warning build, verify.sh 246+2, explicit-file scope-check, tracker in same commit, tag refactor-commit-N, no per-commit push).

  • Commit A · refactor-commit-A — core → MainViewModel.cs 758 → 495. Moved source add/remove/image block to new MainViewModel.Sources.cs (234 lines: AddSource, NextSourceName, AddImage, BuildImageCandidates, PickImageBytes, AddAsset, AddImageSource, AddReusedImage, RemoveElement) and WebView2 hosting (RegisterLoadedWebSources, InitWebView2, OnWebView2PreviewBitmapChanged) to new MainViewModel.Web.cs. Pruned now-dead usings (Microsoft.Win32, NAudio.CoreAudioApi, System.Windows.Media.Imaging, Collections.ObjectModel/Specialized). Pending: Streaming, Background, SocialsDialogViewModel, LayoutStore.
  • Commit B · refactor-commit-B — Streaming → MainViewModel.Streaming.cs 789 → 330 + new MainViewModel.Streaming.Operations.cs (459 lines). Moved the go-live/record/health operation bodies (StartSession, BeginRecordOnly, BeginGoLive, StartRecordFile, PrepareAndStartLiveAsync, PollHealthAsync/OnHealthPollTick/ApplyHealthIssue, StopStream, FinalizeRecordingAsync, UniquePath, ChooseRecordFolder, ResetRecordFolder, DefaultRecordFolder, OnFramePumpFailed, OnFramePumpHealthUpdated, ApplyHealth, ResetHealth, BuildEncoderOptions) into the Operations partial; Streaming.cs keeps state + quality/resolution tiers + session timer + command declarations. Pruned dead usings both files. Pending: Background, SocialsDialogViewModel, LayoutStore.
  • Commit C · refactor-commit-C — Background → MainViewModel.Background.cs 567 → 418 + new MainViewModel.Background.Model.cs (163 lines). Moved the static scene-model helpers (LoadBackgroundImage, CreatePlaceholderSnapshot, EnsureBackground, CreateBackground, NormalizeBackgrounds, AllBackgrounds, ResolveAutoCaptureKey, MonitorKeyPrefix) into the Model partial; Background.cs keeps capture/live state + operations (UpdateActiveBackground, UpdateBackgroundImage, RefreshSnapshotsAsync, ReacquireScreenCaptures, OnScreenPreviewBitmapChanged, OnFullscreenMonitorChanged, RefreshBackgroundAutoCapture, BrowseBackground, ClearBackgroundCaptureAsync, RedesignateBackgroundAsync, ChangeBackgroundCaptureAsync, SetBackgroundCapture, DefaultBackgroundAsset, HealBackgrounds, StampDefaultBackgroundAsset). Pending: SocialsDialogViewModel, LayoutStore.
  • Commit D · refactor-commit-D — SocialsDialog → SocialsDialogViewModel.cs 515 → 334 + new SocialSlotViewModel.cs (183 lines). Moved the DialogEntry record + the row-level SocialSlotViewModel (six-fixed-slot row VM: service/logo/lock/edit/validate state + computed Show* flags) into its own file; SocialsDialogViewModel.cs keeps the dialog VM (sign-in gate, validation orchestration, commit). DialogEntry stays public in the same namespace so both files see it. Pending: LayoutStore.
  • Commit E · refactor-commit-E — LayoutStore → LayoutStore.cs 1402 split into 6 concern-based partials: LayoutStore.cs (47: shell — Instance, fields, ctor + EnsureSchema, GetUserVersion, Dispose), LayoutStore.Migrations.cs (495: all Migrate* + EnsureSchema), LayoutStore.Load.cs (221: Load), LayoutStore.Save.cs (339: Save), LayoutStore.Settings.cs (292: mic/record-folder/reusable-stream + license/transition/broadcast/hotkey/window GetSetting/UpsertSetting), LayoutStore.Assets.cs (41: GetAssetBytes/UpsertAsset). PHASE 3 COMPLETE — no production .cs over 500.
  • Commit F · refactor-commit-F — Recording (context/concern) → new MainViewModel.Recording.cs (121 lines). The encoder (Services/Encoder/FfmpegEncoder.cs) and frame pump (Services/Encoder/FramePump.cs) were ALREADY extracted services — the planned "extract FFmpegEncoder/StreamHealthMonitor/FramePump" was a no-op premise. The honest functional seam was the local recording-output concern: StartRecordFile, FinalizeRecordingAsync, UniquePath, ChooseRecordFolder, ResetRecordFolder, DefaultRecordFolder + fields _recordFolder/RecordFolderDisplay/_activeRecordPath/_recordStartTime/_recordLength moved out of the live-stream orchestration. MainViewModel.Streaming.Operations.cs 475 → 377; MainViewModel.Streaming.cs 328 → 323. Same partial class, MVVM glue untouched.
  • Commit G · refactor-commit-G — ChatOverlayLayer (true component) → new Services/ChatOverlayLayer.cs (199 lines). The first REAL decomposition of the 4,105-line god-object: MainViewModel.Chat.cs 194 → 44 (thin delegation). The layer owns all chat behavior — Messages buffer, ChatBoxRenderer, fade timer, mock-preview timer, per-source preview renders, live-output RenderFrame. The VM keeps only the binding surface: ChatMessages (delegates to _chatLayer.Messages, so XAML bind + LeftPanel CollectionChanged hold), CanAddYouTubeChat/ShowChatInactiveMessage (computed + OnPropertyChanged from VM setters), and thin forwards. Scenes are handed in as args (no Func seam) so the layer stays scene-graph-free. Zero behavior change; 246 pass / 2 known. The partial .cs files land next to MainViewModel.cs in this folder as the split proceeds; each partial carries its own usings and re-declares nothing from core.