Files
LlamaCasty/HANDOFF.md
T
gramps f91bf8715d fix(reorder): save layer list AND preview layout immediately at drop; recording save dialog +10% (276→304)
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.
2026-09-18 10:20:25 -07:00

15 KiB
Raw Blame History

HANDOFF — 2026-09-18 (drag-reorder persistence proven via real mouse input + recording-dialog height, both local — no push)

Branch / Commit State

main HEAD = Rename Recording dialog 20% taller + teardown unhooks the full-screen hook (local). Below it: the layer drag-reorder persistence fix (LayerReorderPersistenceTests), then webcam work. Real-input drag verification test added on top (unpushed, see below).

No push yet. Working tree clean.

⚠️ Branding (2026-09-14, creator-corrected): product = llamacasty, internals = ytLive

Product is llamacasty; repo path, csproj AssemblyName/RootNamespace, DB/log paths (%APPDATA%\ytLlive\...), most code names are legacy ytLive/ytLlive. User-facing text: "llamacasty" / UI labels use the catalog names ("Web Cam", "YouTube Chat", "Countdown Timer"…).

✅ Committed — webcam app-default gate + attainability (9d00955, 75723ac)

Background: the DB truth (%APPDATA%\ytLlive\ytLlive.db; NOT layout.db, a 0-byte legacy file) put the single WebcamSceneConfig in Chat, so the old TASK 26 app-wide rule grayed Live's "+" → Web Cam even though the camera was available and in use. The creator ruled (rigorous resource model): the webcam is an app-level resource — ONE app-wide default selection, usable in every scene that can host it; it is NOT blocked by other scenes' placements. Scope of this slice: the availability layer. TASK 26's app-wide gate is superseded by creator directive.

  • Per-scene gate: CanAddWebcam = StagedScene is { WebcamConfig: null } && IsWebcamAttainable. A scene already hosting the webcam stays gray (one webcam per stream); another scene holding it does NOT gray the row elsewhere.
  • Attainable = a live lock, not a saved identity (75723ac refinement): IsWebcamAttainable = _webcam != null && CameraManager.IsRunning(_webcam.DeviceId). An identity whose camera is unplugged / can't start leaves the row greyed (tooltip "No webcam is currently available…"), and it un-greys the moment a session is running (startup lock, first frame, picker + acquire).
  • Add places the default directly: when attainable, "Add Webcam" creates the placement with NO picker; the Windows picker runs only for the initial selection (_webcam == null).
  • Identity survives last-placement removal: RemoveElement no longer nulls _webcam (that was the TASK 26 rule). LayoutStore writes the singleton whenever it's non-null, so it persists.
  • Startup lays an app-wide BASE lock: the single-camera branch ALWAYS acquires (the old IsRunning skip is gone) — on an already-running session it just bumps the refcount, and that ref is the app's own hold. Result: removing every scene's placement leaves RefCount = 1, the session stays alive, and the row stays offered (the app default outlives the scenes). It still adopts a solo camera as the app default when no identity exists, so a clean layout offers Web Cam at once.
  • Dynamic why-gray tooltip: WebcamAddToolTip (raised wherever the gate can flip: staging, removal, startup lock success, first frame, camera failure, identity swap) — "Already in this scene — one webcam per stream. A second camera means you've graduated to OBS." / "No webcam is currently available — plug one in, or allow camera access in Windows." / "Adds the app default webcam to this scene."
  • Good Dog integration tests WebcamMenuGateTests (real app + temp DB + camera seams): (1) Chat's identity does NOT gray Live; Add in Live places the same wc-1 without a picker; scene-with- placement stays gray; identity survives both removals (DB row count keeps 1, session kept by the base lock). (2) Negation: identity loaded but its session can't start → row NOT offered. WebcamStartupResourceTests test 1 strengthened: after startup the single camera is adopted → CanAddWebcam true. 305 tests (304 pass + the known audio flake, see Landmines); build 0 warnings.

Task docs: TASKS.md Open items + ai.md Webcam section updated (supersession recorded).

✅ Committed — layer-list drag-to-reorder persists (creator-reported, 2026-09-18)

StagedScene.Elements order (RemoveAt/Insert), bypassing SceneGraph's mutation surface, so the drop must be trapped and persisted. Fix (c117885): the drag sets _dragReordered and, on mouse release, EndListDrag funnels the drop through MainViewModel.OnSceneElementsReordered() (Sources.cs) = SceneGraph.InvalidateBake(staged) + SaveLayoutNow() — an IMMEDIATE save at drop (no debounce window, added 2026-09-18 after the creator re-reported it "doesn't save"; the single write stores BOTH the layer list = Source SortOrder AND the preview layout = each layer's X/Y/Width/Height in the same Source rows; LayoutStore.Save deletes+reinserts Source rows in scene.Elements order, LayoutStore.Load reads them back ORDER BY SortOrder). Good Dog tests LayerReorderPersistenceTests (real App + temp DB): (1) reproduces the exact code-behind mutation calls the trap, pumps the dispatcher until the save lands, asserts the DB Source SortOrder matches the in-memory Elements order. Creator later insisted the reorder "doesn't save" even after the build — settled 2026-09-18 with a REAL-gesture test. My first test short-circuited EndListDrag — it never exercised the mouse handlers (impossible with synthetic events: e.GetPosition reads the physical cursor). Added RealMouseDrag_OnTheLayerList_PersistsTheReorder: real SetCursorPos + mouse_event/SendInput against the shown MainWindow physically drag row 3 (ImgC) onto row 1 (ImgA), pumping between steps, then asserts the DB lands at [Background, ImgC, ImgA, ImgB] — AND that ImgC's geometry (X/Y/W/H, the preview layout) is intact in the same rows. Both pass. The mouse-down/move/up → _dragReordered → EndListDrag → OnSceneElementsReordered → DB seam is now proven with real input; a real user drag working differently would mean a stale binary (the Debug exe from 08:42 postdates fix commit 08:18) or a different deployment.

✅ Committed — Rename Recording dialog taller + full-screen hook unhooked on shutdown (2026-09-18)

  • WHERE/HOW: the end-of-recording save dialog is RenameRecordingDialog.xaml (repo root — the dialog that pops up with a default file name when a recording is ended so it can be saved/renamed; title "Rename Recording"). Its fixed Height attribute controls everything: 230 → 276 (+20%, textbox clipped at 230) → 304 (+10% more, creator: the +20% revealed the Cancel/Save button row had ALSO been obscured). The Save button is named SaveRecordingButton. Good Dog test RenameRecordingDialogSizingTests: shows the real dialog in the RealApp host and asserts BOTH the file-name textbox bottom AND the Save button bottom sit inside the client area (client area ≈ Height − ~37px of chrome; measure via TransformToAncestor against the dialog's content root). The dialog's Icon was changed from root-relative /Assets/... to the explicit /ytLive;component/... (the form MainWindow.xaml already uses) — root-relative only resolves in production via Application.ResourceAssembly, so dialogs couldn't be constructed by tests otherwise. The other dialogs still use the fragile form; fix as they get tested.
  • Teardown leak fix (dependency, discovered by the above): MainViewModel.Shutdown() now calls _fullScreenDetector.StopWatching(). The global EVENT_SYSTEM_FOREGROUND hook was never unhooked, so after the VM was collected the next foreground event invoked a garbage-collected WinEventProc delegate → process crash ("Test Run Aborted" in the suite). Harmless in production (exits the process) but fatal to the multi-window test host when a test shows/foregrounds a window.

✅ Earlier committed — webcam resource lifecycle (startup slice, 1e4017d)

Startup poll + tri-state (ValidateWebcamResourceStartupAsync after LoadLayout): 0 → run on, no alarm; 1 → AcquireAsync as app-wide lock (now always runs — see base-lock note above); ≥2 → no auto-lock (App Settings selector next). Lock failure → persistent red alert (WebcamLockAlert in the Layers panel + Retry), re-polls every 5 s (_webcamLockPollTimer), clears on lock success or any first real frame. CameraManager.IsRunning(deviceId) = session exists (started OR starting). Camera test seams CameraEnumeratorOverride/CameraFrameSourceFactoryOverride (mirror LayoutPathOverride). Attribution correction committed: "NVIDIA Broadcast opens the webcam exclusively" is a suspect-list claim — CameraConflictProbe reads process names only, no device handles; do not restate as fact.

⚠️ Open items

  • RenameRecordingDialog too short — DONE (see below).
  • Webcam take fix 2 of 2 — the 09:16 crash is UNFIXED (hard, untested): hypothesis — native WMF death when MediaCapture.Failed fires mid-stream and SafeStopAsync (fire-and-forget) disposes reader+capture while the frame thread sits in TryAcquireLatestFrame/Marshal.Copy (outside the frame's try/catch). No repro, no integration test, native race not catchable — deferred per spin-guard. Record a second occurrence's pattern.
  • Multi-camera selector slice (user-queued): when ≥2 webcams exist, add web-cam selection text + control to the App Settings dialog (gear icon, OverlayHost — left-click opens settings, right-click the context menu). Selecting + successfully allocating/locking activates the WebCam layer. Currently ≥2 cameras → nothing shows in settings. Also: the "Change Webcam" picker should carry the OBS one-liner (one webcam per stream — a second means OBS).
  • Static (+) catalog slice (user-queued): all catalog rows always listed (Background, "Web Cam", "Countdown Timer", "Web Resource", "YouTube Chat", "YouTube Event"…), gray only for (1) system-level unavailability (persistent alert), (2) max already allocated, (3) not available for the current scene; tooltips must name the reason. Static labels only, no new code-behind.
  • Device re-verify (one take session): 1. slice-18 cache verdicts (NR/WH hits, stalls, band ~60); 2. webcam: fresh launch with the browser holding the camera must show the webcam or a NAMED chip (now also the persistent red alert, and the alert must clear after the browser closes + retry); 3. verify Live now OFFERS the Web Cam layer even while Chat holds the camera (the exact creator goal: "webcam offered in the layer stack when I switch over to the Live view").
  • No push yet — after the take verdict, re-measure the clap offset (/tmp/opencode/avsync.py), then decide push with the user.

Open threads (carried)

  • Audio-silence verification — fixed (724af14); creator heard real audio.
  • Webcam MJPG missing / ~10–14Hz, layer SortOrder, truncation-with-dynamic-scenes — queued.
  • Sync control user-doc tutorial — REQUIRED before 1.0 (creator directive; TASK 22).
  • Signed A/V sync: verify the negative (advance) direction on device.
  • Focus-loss capture lag — closed as NOT the cause (1824: delivery healthy ~60-100/s).

Landmines

  • testhost shares startup.log — filter by time; the app also writes fake-device failures ('test-camera', 'dev-physical-9') from CameraManager unit tests.
  • cmd.exe /c "taskkill /F /IM ytLive.exe" (WSL double-slashes mangle) before rebuilds — a live app process locks ytLive.exe (MSB3021).
  • Build/tests: Windows dotnet host (/mnt/c/Program Files/dotnet/dotnet.exe). 0 warnings — only ./scripts/verify.sh "<files>"'s clean build counts. Running ytLive.csproj alone does NOT rebuild ytLive.Tests.dll — run the Tests csproj before vstest. Known audio flake: Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback — a stale loopback sample still in the pipe when the mute flips (phase 2's Assert.Equal(0f, f, 6) sees 0.4). Confirmed pre-existing and currently deterministic in this environment: it fails on clean 9d00955 (stash test) and on the working tree, alone and in-suite. Unrelated to camera work; do not "fix" it inside a webcam slice.
  • Assert footguns in this repo's xUnit: Assert.Null(x, "msg")/Assert.Single(coll, "msg") do NOT take a message — the 2-arg overloads mean something else (predicate/item). Empty message = 1 arg.
  • The webcam's real state lives in %APPDATA%\ytLlive\ytLlive.db (layout.db there is a 0-byte legacy file). sqlite3 at /home/gramps/android-sdk/platform-tools/sqlite3.
  • Contention failure modes (startup.log): "no frames within 4s" = init OK but starved; "OutputFormatNotSupported" = reader subtype below the MJPG-active camera; "file being used by another process / CaptureMode is SharedReadOnly" = re-negotiation refused while others hold the device. CameraConflictProbe = suspect list, not handle evidence.
  • FramePump tests asserting per-frame CONTENT need a STABLE scene; cache-sensitive assertions can't use a call-count resolver flip (tick resolves twice) — key alternation on OutputIndex.
  • ffmpeg/ffprobe: /mnt/c/Program Files/Krita (x64)/bin/ with Windows paths.
  • MyMistakes.md: WINRT resource-allocation RECIPE, freeze-audit RECIPE, A/V sync recipe, deadline pacing, CoreMessaging DQ, WGC-CLIP + slice blocks. Grep before re-deriving.
  • Test-host crash (fixed 2026-09-18): "callback was made on a garbage collected delegate … Win32FullScreenDetector+WinEventProc" — Shutdown() now unhooks the foreground-event hook; never re-introduce StartWatching without a matching teardown, and dialogs whose Icon is the root-relative /Assets/… form can't be instantiated by tests (use /ytLive;component/…).
  • Scene.Elements layer tests: any seeded scene with HasBackground=1 loads a healed Background element pinned at Elements[0] (created by EnsureBackground on StagedScene set) — compute expected layer orders from the live collection, never hardcode indices.
  • Real-input injection test (RealMouseDrag_OnTheLayerList_PersistsTheReorder) needs an interactive desktop session: the window is shown Topmost and clicks are injected at real screen coords — a covered/locked session makes the pointer no-op and the drag never lands. Fine locally.
  • C:\tmpout is for ffmpeg evidence artifacts; keep them out of the repo.

Next step

The multi-camera App Settings selector slice (+ static catalog rows after), then the creator's take on this build. The webcam gate slice answers the creator's "go" ask — switching to the Live view must now offer Web Cam in the layer stack (Chat holding the camera no longer blocks it). The red Layers alert remains the new expected behavior to eyeball on the take: browser holds the camera → fresh launch → alert (unless Chat's config grabbed it) that clears after a Retry once the browser closes.