feat(ui): TASK 42 top bar redesign — one Record-or-Stream surface (2026-09-22)

The bar now renders ONE surface from the FIRST decision the creator makes —
Record or Stream — with each world being the whole bar. Creator directive:
"I wrote the fucking thing and I still can't figure-out how to do stuff",
"forget this one dog plan bullshit". OBS/streaming-tool reference for the
one-surface paradigm: https://obsproject.com (single mode toggle + context
actions) — cited per spin-guard habit; the go-live/test pattern follows
CEV (https://cev-desktop.aSean.xyz) precedent.

- Mode: single segmented REC|ON-AIR switch (SegmentToggle/SegmentLabel in
  Themes/Controls.xaml); radio-exclusive world selection, one tap to flip.
- Record world: switch + Start Recording, zero YouTube identity.
- Stream world: switch + Go Live + Test + account zone right (Sign In until
  connected, then avatar with Change Account/Logout context menu).
- Test is a child of Stream: procs only stream-armed AND signed-in.
- Running: one reality line "dot word elapsed" (REC/LIVE/TEST; green/red/
  gold) + End; worlds + switch retire.
- Gear moved up from bottom bar, ~3 wordmark letters past the brand, single
  click opens Settings/Bug/Feature/About menu (TASK 40 Unit B shipped early).
- Fix folded in: BeginTestStream now arms OnAirPillOn explicitly (unarmed
  pump booted with zero encoder outputs -> "At least one output is required"
  forced-stop cascade from TASK 41's pipeline).
- VM: world/reality props + RaiseTopBarModes() wired into StreamStatus,
  pill, IsRecording, IsTestStream, IsConnected setters.

Doc: ai.md top-bar model, ViewModels/index.md, Controls/index.md, TASKS.md,
HANDOFF.md, new TASKS/task-42-top-bar-redesign.md; task-40 note.

Tests: TopBarModeTests.cs (new Good Dog), PillRadioTests + TestStreamTests
gates updated. Build 0 warnings; full suite 314/315 — single abort is the
pre-existing env-dependent RealMouseDrag test (Path of Exile 2 running)
This commit is contained in:
2026-09-22 17:57:06 -07:00
parent bb5dcb4ba2
commit e69db4d26c
18 changed files with 538 additions and 236 deletions
+22 -18
View File
@@ -624,25 +624,29 @@ driven by the `FramePump` below.
### Local recording (TASK 18 — shipped 2026-08-29, VM + top-bar UX; plan in TASKS.md)
Recording is **independent from streaming** and needs no YouTube sign-in. Two pill toggles in the top bar
declare intent: **REC pill** (local file, works signed-out) and **ON-AIR pill** (streaming; **armable
signed-out** — intent may light while offline, 2026-09-20: a greyed pill was the dead end the guard
shipped to forbid; the ON-AIR setter's signed-out light IS what the pill is for). The pill is intent;
`IsRecording`/`IsLive` are reality — the REC status dot only turns
green when a session is actually recording, the ON-AIR dot when actually live.
Recording is **independent from streaming** and needs no YouTube sign-in. A single segmented
**REC|ON-AIR switch** in the top bar picks the mode (redesigned 2026-09-22 from the old two pills):
**REC** (local file, works signed-out) / **ON-AIR** (streaming; **armable signed-out** — intent may
light while offline, 2026-09-20: a greyed pill was the dead end the guard shipped to forbid). The switch
selection is intent; `IsRecording`/`IsLive` are reality — the reality line (dot + word + elapsed) only
lights when a session is actually running.
- **State model (`MainViewModel`):** pills `RecordPillOn`/`OnAirPillOn` (radio-exclusive — arming one
clears the other, record-OR-live 2026-09-01; the ON-AIR setter stays armable signed-out (2026-09-20: intent must be able to light while offline — a pill that could not light signed-out was the dead end the guard shipped to forbid)), `ShowPrimaryStartButton` = **always the idle face** (`IsOffline && !IsRecording`,
2026-09-01 — the old connected/record-only gate blanked the bar after stopping signed-out),
`ShowEndStreamButton`, `CanStartSession`, `AccountStatusLightToolTip`. Sign-in is a **context-menu
item on Start** ("Sign in to YouTube" → `SignInCommand`, visible while disconnected) — the standalone
Sign In button is gone (TASK 30's single-button rule, completed). `StartSession()` routes: ON-AIR on
→ GoLive dialog then stream; REC on or **nothing armed** → `BeginRecordOnly()` (unarmed
Start lights the REC pill and records — no dead-end no-ops; local recording needs no account).
`IsEditMode` also requires `!IsRecording` (lock scrubbing while recording).
**Top bar order (creator spec 2026-09-01):** `[sign light] REC [pill] [sign light] ON-AIR [pill]`
— each reality lamp sits before its own intent switch. **Stop ends everything:** `StopStream`
clears both pills (see "Stop ends everything; failures roll back" below).
- **State model (`MainViewModel`):** pills `RecordPillOn`/`OnAirPillOn` stay the **routing model**
(radio-exclusive — arming one clears the other, record-OR-live 2026-09-01; the ON-AIR setter stays
armable signed-out (2026-09-20: intent must be able to light while offline)). **Top bar redesign
(2026-09-22):** the bar is ONE surface rendered by the FIRST decision — Record or Stream — and each
world is the whole bar (`ShowIdleWorld`/`ShowRecordAction`/`ShowStreamActions`, wired by
`RaiseTopBarModes()`). The old sign-lights + two pill toggles + status dot + PRIVATE/TEST chips are
gone; the mode is one **segmented REC|ON-AIR switch** (`SegmentToggle`/`SegmentLabel` styles), and
"going back" is one tap on the other segment. Idle worlds: Record = switch + "Start Recording"
(`PrimaryStartButtonLabel` names the armed world) and **no YouTube identity at all**; Stream = switch
+ "Go Live" + Test (Test is a **child of Stream** — visible only stream-armed AND signed-in) plus the
account zone on the right (`ShowSignInButton` = stream world signed-out; `ShowAvatarButton` = the
avatar, right-click Change Account/Logout). Running retires the switch and worlds; one reality line
`● word elapsed` + End replace them (`RunningVisible`/`RunningText`/`RunningDotBrush` — TEST/LIVE/REC,
gold/red/green). The account **status dot is gone** — the avatar/Sign In pair carries connectedness.
`ShowPrimaryStartButton` remains the always-idle face. `StopStream` clears both pills (see "Stop ends
everything; failures roll back" below).
- **Filenames** (pure `Services/RecordingFile`): auto-name `ty-<yyyyMMdd>-<HHmm start>-0000.mp4` at start;
**rename-on-stop** to `ty-…-<hh2mm2 actual length>.mp4` (`FinalizeRecordingAsync` after the pump stops &
the file closes), numeric `-2`/`-3` suffix on collision (`UniquePath`). Length comes from `_liveElapsed`,