Files
LlamaCasty/ViewModels/index.md
T
gramps c01206fb8a feat: web sources frame-captured via composition (CoreWebView2CompositionController → Windows.Graphics.Capture); PNG poll + CaptureScheduler deleted
The ~30Hz CapturePreviewAsync PNG poll capped real cadence at ~20Hz
(35–165ms full-HD encode+decode), so a 60fps widget still juddered at ~1/6
speed. Replaced polling with frame-driven capture of the composition
controller's root visual — the mechanism WebView2CompositionControl and
Flutter's webview_windows use (graphics_context.cc captures the root
surface_ visual via CreateGraphicsCaptureItemFromVisual; reference:
github.com/microsoft/Windows.UI.Composition.WinUI / flutter-internal
webview_windows). Frames now arrive at the renderer's own pace; capture
memory is epoch'd ring reuse + one crop-sized shared WriteableBitmap.

New Services/WebCaptureFrameSource.cs owns GraphicsCaptureItem + free-
threaded Direct3D11CaptureFramePool + session (Straight alpha readback,
per-frame FindContentBounds → CropBounds). WebView2Manager reworked around
per-session composition controllers + one UI-thread Compositor created via
the CoreMessaging CreateDispatcherQueueController P/Invoke (the 19041
projection lacks CreateOnCurrentThread); internal seam ctor
(Dispatcher, Func<string,IScreenCaptureSource>?) for hermetic tests.
CaptureScheduler.cs deleted; the three SetCaptureInterval cadence hooks
removed; InitWebView2() moved from MainWindow ctor to Loaded (a parent
HWND must exist for the composition controller); the hidden WebViewHostPanel
overlay deleted. TransparentBackgroundScript unchanged.

Tests: WebView2ManagerTests reworked — 4 control-size + scheduler tests
dropped, FindContentBounds tests moved to WebCaptureFrameSource, ONE
integration test (Frames_PublishCroppedPreview_And_CoalesceToLatest_CarryingCropBounds)
drives the seam with a FakeWebSource + background-STA DispatcherPump.
Suite 293/293, 0 warnings.

NOTE: composition path NOT yet verified on a device — the take is the
next step. Web work committed locally only (no push per standing rule).

Docs same-commit: ai.md Slice 14 + supersede marker on Slice 11, HANDOFF,
MyMistakes (CoreMessaging DQ + namespace-landmine recipe), TASK 17,
Controls/ViewModels/Services indexes.
2026-09-14 16:14:00 -07:00

24 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, ShowPreviewPlaceholder, BrandFlash*, WebView2 hosting (InitWebView2/RegisterLoadedWebSources/OnWebView2PreviewBitmapChanged), frame-pump wiring (construction + _rtmpUrlProvider seam + Dispose, plus ResolveOutputFrame → RenderChatBox for chat 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.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.
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: REC/ON-AIR pills (RecordPillOn/OnAirPillOn/CanToggleOnAir), ShowPrimaryStartButton/CanStartSession/ShowEndStreamButton/EndButtonText/ShowLogInButton, 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. Pipeline bits: _framePump/_liveTimer/_healthPollTimer fields, BuildEncoderOptions. Commands StartStreamCommand/EndStreamCommand/ChooseRecordFolderCommand/ResetRecordFolderCommand. Broadcast pull-out: CurrentBroadcastId/BroadcastForm.
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) + ShowLicenses (loads shipped THIRD-PARTY-NOTICES.txt into LicensesText on first open, never the OS viewer); bug/feature report BugReportText/BugReportEmail/FeatureRequestText/FeatureRequestEmail + SubmitBug/SubmitFeature/ComposeEmail/OpenUrl; About hub consts ChannelUrl/MastodonUrl/CoffeeUrl (public, integration-test live) + PremiumUrl (tabled billing seam — TASK 12 → greyed "Unlock Premium" lights up when filled) + IsPremiumAvailable + SupportEmail + AppVersionLabel; commands OpenSettingsCommand/OpenBugCommand/OpenFeatureCommand/OpenAboutCommand/CloseOverlayCommand/SubmitBugCommand/SubmitFeatureCommand/OpenChannelCommand/OpenMastodonCommand/OpenCoffeeCommand/OpenPremiumCommand/OpenLicensesCommand. Ctor command assignments stay in core.
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), 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). _chatBoxRenderer field. Ctor wiring (MessageReceived hookup, new ChatBoxRenderer(), LoadLayout preview start, _youtubeChat.Dispose) stays in core.
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.

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.