Files
LlamaCasty/ViewModels/index.md
T
gramps 3a77662a95 vv webcam Unit 2: the Chat scene in the vertical tier - rect only, tile or one whole 607x540 area
The creator's locked table (2026-10-17) gives the Chat scene its own rules in
the 9:16 tier: RECT ONLY - the circle option is not offered - and a size that
is either the normal small tile or ENLARGED to fill one whole 607x540 area
(top or bottom). Unit 1 shipped the per-scene placement; this unit is the Chat
scene's own variant of it.

The rule lives on the MODEL, not on the button:
- Models/SceneElement.cs: ClipShape and ToggleClipShape() are now virtual.
- Models/WebcamSceneConfig.cs: VerticalShapeLocked (set per scene by the VM)
  OVERRIDES the ClipShape property, so no writer can slip a circle in - the
  toggle, the Shape button and the placement swap all go through it. The lock
  is cleared BEFORE the 16:9 layout comes back, so the desktop shape returns.
- ViewModels/MainViewModel.Webcam.cs: CanToggleClipShape (the circle is NOT
  offered in the vertical Chat layout - the Shape button disappears, it is not
  merely greyed out), IsChatSceneCamera, ResizeWebcamInVerticalTier.
- Models/VerticalCameraLayout.cs: FillsArea (the "enlarged" state, derived -
  no new column) and SizeFor - the Chat camera's TWO sizes, the tile or the
  whole area, whichever is nearer the requested width, so the resize handle
  has two detents instead of any size in between.
- An enlarged camera FILLS the area its centre is in (top or bottom), so
  dragging it up/down swaps the areas.
- The Chat camera is auto-tiled like the Live one when the vertical tier is
  picked - otherwise the vertical chat stream has no camera at all.
- Controls/PreviewPane.xaml.cs: the resize routes through the VM; the drag
  assigns, lets the model clamp into the band, then snaps (so the enlarged
  camera can be dragged from the top area to the bottom one).

Test: ytLive.Tests/VerticalChatCameraLayoutTests.cs - the ONE Good Dog test for
the unit: the vertical tier is picked -> the Chat camera comes into the band as
a small RECT tile -> the circle option is not offered and cannot be set even on
the model -> dragged up and enlarged it fills the TOP area exactly -> back to
16:9 the desktop layout is restored exactly and the circle option is offered
again. 430/430 tests pass, 0 warnings.

Research (the creator's table is the established practice, not our invention):
- Streamlabs' vertical guide: "splitting the screen in half horizontally,
  placing the camera feed in the top half and the main content on the bottom
  (or vice versa) may be the bet", and a just-chatting vertical wants the
  camera "to take up most of the screen"
  (streamlabs.com/content-hub/post/overlays-vertical-and-horizontal-streaming)
- Bloopbot's vertical overlay docs describe the same per-layer model we use at
  scene granularity: "move or resize a layer on Vertical and it stops being
  Auto-placed. It keeps the place you gave it, and the horizontal layout is not
  touched" (bloopbot.com/docs/vertical-overlays-twitch-dual-format) - our
  swap-at-tier-change, stored per scene.
- Landmine recorded: "about the bottom 560 px of the vertical video can be
  covered by Twitch's chat and player controls on phones" (Bloopbot) - the Chat
  camera's enlarged BOTTOM area may be half-hidden by the platform chrome;
  TASK 53's safe-zone work decides that.

Memory in the same commit: ai.md (the Chat rules, the shape lock, the tests),
TASKS.md + TASKS/task-57-vertical-camera-layout.md (TASK 57 complete), HANDOFF.md,
MyMistakes.md (3 recipes: lock the model property not the UI button; two detents
for a discrete size set; the edit tool cannot match an oldString with an em
dash), Models/ViewModels/Controls index rows.
2026-10-21 19:23:29 -07:00

26 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 → RenderChatBox/RenderAlertBox for chat + 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) + 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, 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.
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.