# HANDOFF — current state **Branch:** `main` (pre-1.0, no feature branches). **Last pushed:** `7f14ffb`. **This session's work is COMMITTED LOCALLY, NOT PUSHED** — **four** commits ahead of `origin/main` (`fae8ec5`, `5aedca7`, `f5a9d46`, `de81fa3`) plus this unit. Push only when the creator says so. ## The 2026-09-26 proof-of-concept round (creator reporting) Context: **no YouTube private test recordings are being saved**, so the creator is judging compositing from **local recordings**. They assumed the live path needed a separate "redirect" — **it does not, and that is verified, not assumed:** one `_framePump` is constructed in the `MainViewModel` ctor with a single `brandFlash:` callback, and both `Streaming.Operations.cs:68` (go-live) and `:199` (record) call that same `StartAsync`. Record+simulcast is one ffmpeg with two outputs. Five items, tracked in `TASKS.md` → "Creator-reported batch". ### Done this unit: recording save dialog — Cancel now discards `RenameRecordingDialog` was always correct (Enter → `DialogResult=true`, Cancel/X/Escape → `false`). **The caller was not**: `FinalizeRecordingAsync` only overwrote `stem` when the dialog returned true and then ran `File.Move` **unconditionally**, so a cancelled dialog silently saved the recording under the default name. Extracted the decision into internal `MainViewModel.CompleteRecordingSave(startPath, dir, autoStem, chosenStem, creatorSaved)`: - `creatorSaved == false` (Cancel/Escape/X) → **delete the temp file**; the videos folder is left empty. A failed delete returns `DiscardFailed` and the toast names the file + folder. - `creatorSaved == true` (Save, or Enter on the pre-filled default) → `File.Move` to the final name. Blank box still means "keep the auto name" — but only on an explicit Save. Test: `ytLive.Tests/RecordingSaveDialogTests.cs` (5 facts, real temp files, no WPF needed) — the headline asserts Cancel leaves the directory **empty**, not merely "the stem is unchanged". Lesson recorded in `MyMistakes.md` (falsy `ShowDialog()` falling through into a side effect). ## Two earlier work units (committed) ### 1. Branding flash is now composited into the OUTPUT (TASK 36 shipped) It used to be a WPF `BrandFlashLayer` TextBlock in the preview at 25% opacity on a 300s timer — a credit the creator could see and **no viewer ever could**. Now one rendered `VideoFrame` goes to **both** the frame pump and the preview, so they cannot drift: - `Services/Compositor/BrandFlashPresenter.cs` (new) — neon raster (white core + feathered red/blue halo, channels split opposite), 2s envelope, random on-canvas placement per presentation, licence gate re-checked on every read. - `ViewModels/MainViewModel.BrandFlash.cs` (new) — `BrandFlashFrame()` for the pump + `PublishBrandFlashPreview()` for the pane. - `FramePump` gained `brandFlash:` → the existing per-frame `flashFrame` slot, and mixes it into the C4 cache signature. `SceneCompositor.Overlay` is now `internal` for the shortcut path. - `PreviewPane.xaml`: `BrandFlashLayer` → `BrandFlashElement` (bound to the published frame). - `BrandFlashEnabled` is derived `!IsPremium` and no longer assignable. Spec met: 500ms in / 1000ms hold / 500ms out, first credit ~5s after go-live then every `rand(30s)+30s`, single unwrapped line, random spot every time, fully on canvas. ### 2. Dev-only: two instances side by side `Helpers/InstanceProfile.cs` (new). Set `YTLIVE_INSTANCE=` and that process gets a private `%APPDATA%\ytLlive\instances//` root for the **layout DB, auth file, startup.log and the WebView2 user data folder**. Chromium locks that folder exclusively — without this the second instance does not start at all. Recording folder and the ffmpeg `tools` cache stay shared on purpose; global hotkeys stay un-namespaced. **Usage:** `$env:YTLIVE_INSTANCE=2; dotnet run` The entire implementation is inside `#if DEBUG`. Release compiles to `DataRoot => DefaultRoot` + `WebViewDataFolder => null`; the call sites are unconditional so Release cannot drift. ## Bugs found and fixed along the way - **Pre-existing, not mine:** `FramePump`'s **fully-static shortcut** stretched the cached bake and returned it, silently discarding **every per-frame overlay** — the social bar and the alert ticker were already lost there, not just the flash. Now composites overlays onto a copy before stretching. - **Cadence bug caught by my own test:** the interval was an absolute deadline, not a period, so gaps collapsed (6.7s / 3.5s / 2.8s instead of 30–60s). Fixed. - `BrandFlashPresenter` recycles one 8MB master buffer and **must** stamp `VideoFrame.Epoch` per read, or the paste cache freezes the credit. It does. ## Test state — VERIFIED - **357 total, 356 pass.** 18 new facts (10 brand flash, 8 instance isolation). - The one failure is `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`, and it is **flaky by environment, not a regression** — 2 fail / 1 pass in isolation. Its own doc comment: "Requires an interactive desktop session: if the window is covered or the session is locked, the pointer no-ops and the drag never lands." The symptom matches exactly (order unchanged). **Run the suite with ytLive CLOSED** (see `MyMistakes.md` 2026-08 era note). - `RealAppHost.RunAsync` was added for this: a frame-pump test MUST `await` inside it. A blocking wait in `RealAppHost.Run` occupies the one shared STA thread and hangs the whole suite with no output. **Always background a `vstest` run and poll the log** — foreground piped `vstest` returns nothing in this shell even on success. ## Landmines - A stale `ytLive.exe` (PID 2544) locked `bin/.../ytLive.exe` and broke `dotnet run` with MSB3027. Killed. **If the build fails to copy `ytLive.exe`, check for a running instance first.** - `GlobalHotkeys`: two instances registering the SAME global hotkey — Windows refuses the second. Left alone deliberately. - `OverlayHost.xaml` says Polar unlocks alerts too; alerts are not gated. Unresolved, queued. ## Next 1. **Ungate the brand flash from `IsLive`** (TASK batch #2) — it must run in every scene and appear in recordings; today it only starts in `UpdateLiveVisuals()`'s live branch, so a recording made without ever going live carries no credit. 2. **Confine the ticker to the alert box** (#3) — it is still a global 1920×48 top overlay (`BlitOverlay(…, 0, 0)`); needs a rect + scale/clip decision in both preview and output. 3. **Multi-instance** (#4) — route `FfmpegLocator._toolsDir` through `InstanceProfile`, and confirm the launch method: `dotnet run` while the first app holds `bin/…/ytLive.exe` fails with MSB3027 *before any instance starts* (a build-output lock, not a log conflict). `$env:YTLIVE_INSTANCE=2` + `dotnet run --no-build` in a second shell is the known-good path. 4. **Post-session efficacy report** (#5) — roll up `CurrentHealth` (dropped frames, duration, health message) when a stream or recording ends. 5. Push the commits when the creator asks. 6. Test-console chat UX (queued): don't clear the chat window, 20px right padding on the pull-out input, Enter inserts a newline. 7. Decide the alert-gating copy question above. 8. Ship-checklist items live in `TASKS.md` → "1.0 gates".