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.
24 KiB
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.cssplit 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_Clickmoved 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 ofMainWindow.Window.ResourcesintoThemes/Controls.xamlso 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 onIsConnected/AccountAvatarUrlchanges. - 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_MouseLeftButtonDownmoved into the control (transitions viaDataContext.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_Clickmoved with it (uses inheritedDataContext). Window keeps the thinWebViewHostPanel+ToastArearoot 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. (TheWebViewHostPanelroot stub is since GONE — the 2026-09-14 composition-capture redesign movedInitWebView2()toMainWindow_Loadedand deleted the panel; seeControls/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.cs758 → 495. Moved source add/remove/image block to newMainViewModel.Sources.cs(234 lines:AddSource,NextSourceName,AddImage,BuildImageCandidates,PickImageBytes,AddAsset,AddImageSource,AddReusedImage,RemoveElement) and WebView2 hosting (RegisterLoadedWebSources,InitWebView2,OnWebView2PreviewBitmapChanged) to newMainViewModel.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.cs789 → 330 + newMainViewModel.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.cs567 → 418 + newMainViewModel.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.cs515 → 334 + newSocialSlotViewModel.cs(183 lines). Moved theDialogEntryrecord + the row-levelSocialSlotViewModel(six-fixed-slot row VM: service/logo/lock/edit/validate state + computedShow*flags) into its own file;SocialsDialogViewModel.cskeeps the dialog VM (sign-in gate, validation orchestration, commit).DialogEntrystayspublicin the same namespace so both files see it. Pending: LayoutStore. - Commit E ·
refactor-commit-E— LayoutStore →LayoutStore.cs1402 split into 6 concern-based partials:LayoutStore.cs(47: shell —Instance, fields, ctor +EnsureSchema,GetUserVersion,Dispose),LayoutStore.Migrations.cs(495: allMigrate*+EnsureSchema),LayoutStore.Load.cs(221:Load),LayoutStore.Save.cs(339:Save),LayoutStore.Settings.cs(292: mic/record-folder/reusable-stream + license/transition/broadcast/hotkey/windowGetSetting/UpsertSetting),LayoutStore.Assets.cs(41:GetAssetBytes/UpsertAsset). PHASE 3 COMPLETE — no production.csover 500. - Commit F ·
refactor-commit-F— Recording (context/concern) → newMainViewModel.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/_recordLengthmoved out of the live-stream orchestration.MainViewModel.Streaming.Operations.cs475 → 377;MainViewModel.Streaming.cs328 → 323. Same partial class, MVVM glue untouched. - Commit G ·
refactor-commit-G— ChatOverlayLayer (true component) → newServices/ChatOverlayLayer.cs(199 lines). The first REAL decomposition of the 4,105-line god-object:MainViewModel.Chat.cs194 → 44 (thin delegation). The layer owns all chat behavior —Messagesbuffer,ChatBoxRenderer, fade timer, mock-preview timer, per-source preview renders, live-outputRenderFrame. The VM keeps only the binding surface:ChatMessages(delegates to_chatLayer.Messages, so XAML bind + LeftPanelCollectionChangedhold),CanAddYouTubeChat/ShowChatInactiveMessage(computed +OnPropertyChangedfrom VM setters), and thin forwards. Scenes are handed in as args (noFuncseam) so the layer stays scene-graph-free. Zero behavior change; 246 pass / 2 known. The partial.csfiles land next toMainViewModel.csin this folder as the split proceeds; each partial carries its ownusings and re-declares nothing from core.