Adds SourceType.Scoreboard with the same per-scene eye toggle, drag and presence as every other
element — 'prominent on BRB, dialled back on Live' is geometry, not a second mechanism. Default
geometry is the strip: 1920 wide, 56 tall, bottom edge.
The output resolver returns the presenter's cached frame exactly like the chat box and alert box do,
and every session start clears the tallies. Preview publication writes one shared WriteableBitmap on
the UI thread (the pump thread must never touch one), refreshed only when a displayed value moves.
Guard: ScoreboardLayerTests pins the enum member, the geometry constants, the resolver case and the
session hook, so the layer cannot quietly become a special widget.
- Source.IsBrandLogo persisted (additive migration); layer healed on load, removal blocked
- Image stored as an asset; settings keep the asset id and file name
- App Settings: Brand Logo file field with Browse
- Default placement top-left; per-scene eye uses the existing element visibility
Test: BrandLogoTests; layout/background tests pass (34). Not pushed.
- LeftPanel: SCENE LAYERS header with layer count; per-type blue icons; properties header gear; layers list min/max height; properties placeholder; list and properties indent
- SceneThumbnailStrip: names/hotkeys over the tile; PROGRAM badge with blinking red dot on the selected tile; orange border on selected and live tiles; fixed the element border override that hid the style border; reflag tiles on load
- LiveScene is now set on go-live, record start, and live transitions (it was never assigned, so IsLive never fired)
- Startup stages the scene named Starting
- DESIGN.md: color roles (red = YouTube/on-air/record, orange #ff8c3a = attention, amber = game audio/Super Chat)
- Staged live tile uses background art instead of the green LIVE placeholder
Not pushed.
Replace the external StreamElements feed with NATIVE YouTube events per the creator's
2026-09-23 question.
Chat parity half:
- YouTubeChatService decodes ALL SIX liveChat/messages event types into
ChatMessage.Kind (superChat/superSticker/newSponsor/membershipGifting/
giftMembershipReceived/memberMilestoneChat) — the four formerly-empty overlay
rows are real now — and re-arms its one-shot poll on the server's
pollingIntervalMillis (streamList connection semantics; clamp 1000-6000ms,
maxResults=2000). ParsePage internal static seam + ChatPage record for
deterministic tests; optional HttpClient ctor seam kept; InjectSimulatedMessage
(TASK 41) preserved.
Alerts half (creator rulings: one celebration zone, six UNIQUE animations, no
menus/polls, sub mention = chat row only, NO viewer count):
- New SourceType.AlertBox 'Stream Alerts' (one per layout, CanAddAlerts gate
mirroring chat; idle = transparent).
- AlertRenderer: six distinct branded animations (SuperChat slide-up/shine/
count-up, SuperSticker scale-pop, NewMember drop-in/flash, MemberGift slide-left/
chip-fan, GiftReceived confetti, MemberMilestone rise/growth-bar), every card
drawing the 'made with LlamaCasty!' brand line.
- AlertOverlayLayer: true component (Commit-G pattern) — queue (cap 10, drop tail
never stall), 33ms UI ticker, cache-first RenderFrame + UpdatePreview,
Advance(double) as the deterministic test clock; ChatEventKind.None rows never
enqueue.
- Wired: resolver RenderAlertBox, _alertLayer ctor + dispose, LoadLayout previews,
Add menu item (Controls/LeftPanel.xaml), TestSessionViewModel sims tagged
(member->NewMember, superchat->SuperChat).
Good Dog ONE integration test: AlertLayerTests (RealApp STA, real WPF raster) —
ParsePage classifies all six kinds + cadence fields; None rows enqueue nothing;
six events play pairwise-distinct moving frames then drain to null.
Gate: clean build 0 warnings; full suite 316/317, the one failure
(LayerReorderPersistenceTests.RealMouseDrag) repros on the clean tree — the
known environmental class (real-mouse-drag no-ops with a game/fullscreen window
focused).
References (OBS/overlay ecosystem):
- streamList semantics: https://developers.google.com/youtube/v3/live/docs/liveChatMessages/streamList
- OBS alert-box pattern (designated celebration zone, idle transparent): creator-chosen model
Reorder: OnSceneElementsReordered now calls SaveLayoutNow() (was debounced
ScheduleSave) so a drag-drop persists instantly — one write stores the new
layer list (Source.SortOrder) and each layer's preview geometry (X/Y/W/H) in
the same rows. Reference: WPF ListBox drag-reorder requires an explicit persist
at drop; a debounce window lets a crash lose the drop.
Recording save dialog (RenameRecordingDialog.xaml): height 276→304 so the
Cancel/Save button row is no longer obscured; Save button named
SaveRecordingButton so the sizing test can assert it sits inside the client area.
Tests: deterministic seam test now asserts the dragged layer's geometry (preview
layout) is persisted with the new order; sizing test asserts 304 + textbox and
button-row bottoms within client area. 308/308 green.
List_PreviewMouseMove reorders StagedScene.Elements via RemoveAt/Insert,
bypassing SceneGraph's mutation surface, but never scheduled a save — the
new z-order was lost on restart. The drop now sets _dragReordered and
EndListDrag funnels it through MainViewModel.OnSceneElementsReordered()
(InvalidateBake + ScheduleSave, same background-save path as every other
mutation). Integration test (RealApp + temp DB) reproduces the exact
code-behind mutation and asserts the debounced save lands the new Source
SortOrder. Derivation reference: standard WPF ItemsControl drag-reorder
pattern (OBS layering semantics: bottom-most layer = index 0).
Chat's camera no longer greys Web Cam in Live (per-scene max, not app-wide);
TASK 26 superseded by creator directive (app-level resource model). Add Webcam
now places the existing app default without the picker; the picker runs only
for the initial selection. Removing the last placement keeps the identity
(_webcam never nulled) so Add stays offered. Startup pass adopts a solo camera
as the app default, so a clean layout offers the layer in Live/Chat at once.
Dynamic WebcamAddToolTip names the why (incl. the 'graduated to OBS' line).
Good Dog: WebcamMenuGateTests rewritten (real app + temp DB + camera seams) —
identity in Chat does not gray Live; Add in Live places same wc-1 no picker;
scene-with-placement stays gray; identity survives both removals (DB row 1).
WebcamStartupResourceTests single-lock fact asserts CanAddWebcam after adoption.
Docs same commit (ai.md supersession, TASKS.md, HANDOFF.md). 304/304, 0 warnings.
- Add ElementKind (Static/Dynamic) to SceneElement base; Source/WebcamSceneConfig classify
- Services/SceneGraph.cs: owns the Scenes collection (ViewModel's Scenes delegates to it),
the element mutation surface (Add/Insert/Remove/Move, each invalidating the bake), and the
queries that were scattered LINQ (GetBackground/GetWebcam/GetChatBoxes/GetSplitPoint/IsStatic)
- SceneCompositor: split-aware BakeStaticBase + CompositeLayers + Render(.., staticBase, split);
builds/caches the static base below the split point in source-rect space
- FramePump: optional SceneGraph -> optimized bake+composite path; falls back to full render
- MainViewModel: routes element mutations through the graph; invalidates the bake on static
layout/opacity/visibility/useDefaultBackground changes and after background heal/Ensure
- Integration test SceneGraphTests.BakedStaticBase_WithDynamicLayer_CompositesCorrectly
Derivative survey (mandated): tried before writing — OBS does per-source opacity/visibility
caching and static-scene baking; this mirrors OBS's 'cached static source' optimization.
3 documented defensive deviations from the spec (ChatOverlayLayer stays decoupled;
background helpers stay VM-static for direct testability; full facade peel deferred post-1.0)
in TASKS.md + ai.md. Scope check passed; clean build 0 warnings.
Core MainViewModel.cs 758 -> 495 lines. Moved the source add/remove/image
block (AddSource, NextSourceName, AddImage, BuildImageCandidates,
PickImageBytes, AddAsset, AddImageSource, AddReusedImage, RemoveElement) into a
new MainViewModel.Sources.cs partial, and WebView2 source hosting
(RegisterLoadedWebSources, InitWebView2, OnWebView2PreviewBitmapChanged) into a
new MainViewModel.Web.cs partial. Pruned now-dead usings from core.
Zero behavior change; build 0 warnings; 246 pass, only the 2 known failures.
Docs: ViewModels/index.md Phase 3 tracker updated in same commit.