Every 1080 tier carried 8 Mbps, inherited from TASK 4's "mainstream ceiling,
upload headroom stays comfortable". It was never derived from anything: the same
number served 30 fps and 60 fps, landscape and vertical, which no per-tier
derivation would produce. With CBR now actually holding the rate (700c7a8) that
number becomes the picture quality, so it has to be chosen rather than inherited.
New defaults sit between YouTube's H.264 recommendation (17 at 1080p60, 14 at
1080p30, 8 at 720p — support.google.com/youtube/answer/2853702) and the rate
practitioner OBS guides converge on for YouTube (9–12 Mbps at 1080p60). Vertical
mirrors its landscape pair: 1080x1920 is the same pixel count as 1920x1080.
The free-profile fallback in OutputProfile.Resolve moves with it (8.0 -> 10.0) so
a build with no shipped tiers still emits the 1080p30 rate rather than a stale
one.
668 tests (1 audio-pipe flake under full-suite load, passes alone), 0 warnings.
Add a persisted global App Settings switch. Off keeps the existing per-severity
lifetimes (errors remain sticky); on makes every alert non-expiring and closes
it when its body is clicked. The NotificationService supplies the current
setting to each toast request.
StickyNotificationIntegrationTests covers persistence and both request modes.
618 tests green, clean build with 0 warnings.
Reference: Notification.Wpf exposes a CloseOnClick request option:
https://github.com/G-Stas/Notification.Wpf
App Settings now chooses one of six ToastArea positions (left/right ×
top/center/bottom), defaulting to the existing bottom-right. The setting is
persisted per profile and the toast area moves immediately; edge gutters keep
bottom notifications above the footer.
ToastPositionIntegrationTests covers live repositioning and restart persistence.
617 tests green, clean build with 0 warnings.
Reference scan: OBS Notifier supports user-configured notification placement:
https://github.com/DmitriySalnikov/OBSNotifier
Design settled with the creator. Two findings drove it:
- **Resolution is not a bandwidth lever.** 1080p30 and 720p60 are both 6 Mbps in the shipped table,
so "step down to 720p" relieves nothing against congestion; and 60→30 at the same bitrate relieves
nothing either. Congestion therefore steps **bitrate only** (8.0 → 6.4 → 5.1 → 4.1, ~20%), which is
what OBS Dynamic Bitrate does — cited, adopted, with its climb-back discarded for v1.
- **The other half has no OBS equivalent**: machine starvation (the game and the app on one PC) steps
fps, then resolution, and sheds LlamaCasty's own capture cost *first*. That is the app taking
responsibility for a stream it is helping to starve.
Budgets as ruled: 3 steps for a 1080 start, 1 for 720 and vertical, hard floors at 720p30 /
Vertical 1080p30, hold-and-warn at the floor, never disconnect. Trigger ≥5% drops over 15 s twice,
30 s cooldown, ambiguous evidence does nothing.
New rule in `ui-surface.md`: a toast names the cause, the culprit and the remedy. Mechanism strings
("All pipe instances are busy", HTTP codes, exit codes) are log findings, never user-facing copy.
Creator: system messages are limited to two lines; the label above the message ('STEAM ID NOT
PRESENT', 'STREAM') was repeating what the row already said and pushing the stack past its budget.
AttentionAlert loses Label; rows render Text only. The licence row is now exactly:
Steam ID Limit: 30fps
Click for More Info
(60fps with a close control once owned). Rows are identified by Id internally, so nothing in the UI
depends on the removed text. Tests updated to assert position/text rather than the heading.
Creator design (2026-11-05). A stack of red rows at the top of the left panel, above both mode grids
so it shows while editing and while live, pushing the panel's content down.
- Row 1 is the **Steam licence indicator** and nothing displaces it. It reads as status, not a
warning: "Entitlement 30 fps · Maximum 60 fps", both numbers derived from QualityOptions and
HasProOutput. While higher tiers are unavailable it shows a **pin** (standing indicator, no close
control); once ownership is verified the pin becomes a **close control** and the dismissal is
persisted (`Steam.LicenceIndicatorDismissed`), permanent while ownership holds. Losing ownership
restores it as pinned — a change of state, not a preference. Click → About.
- **Event rows stack beneath, newest first**, one row per episode (an episode advancing its state
updates its own row rather than adding another). Each is dismissible. Click → the Logs tab opens
with that event's sequence range painted red (`HighlightedLogFrom/To` + `LogInRangeConverter`; two
numbers rather than a collection because the log list is virtualised and realised rows do not
re-evaluate on membership changes).
- Replaces the old red panel under the quality picker, which is removed.
- The Steam diagnostic text loses "Contact the creator or support" for a neutral
"Steam licensing is not configured in this build."
Tests: `AttentionAlertStackTests` — pinned first with the status text; owned → closable and the close
survives a reload but ownership loss restores it pinned; pinned cannot be dismissed; event rows LIFO
with in-place episode updates; clicking an event opens Logs and sets its range. AboutHub + Entitlement
tests updated for the removed panel and the new wording. FAST gate: 532 passed, 0 failed, 0 warnings.
Three reports from the creator, plus two bugs the real clip exposed.
1. A video layer was INVISIBLE in the preview. The pane's per-layer Image is Collapsed by
default and switched on by a whitelist of DataTriggers; MediaSource matched none, so the clip
decoded into a bitmap that nothing displayed. Same defect the alert box hit on 2026-09-26 and
the scoreboard after it — now recorded at the top of docs/ai/ui-surface.md with a test that
asserts the bound Image is Visible (`MediaPreviewDisplayTests`).
2. Play is no longer tied to Loop: the button is always enabled (creator ruling — Loop must not
toggle Play). Loop still restarts a finished clip, which is the behaviour that was broken.
3. Geometry is now the LAST section of the properties list for every draggable element, with
opacity at the top; the web source's duplicate geometry block is gone (it now uses the shared
one, which had been sitting above the media panel).
4. The physical mouse-drag test is deleted. It injected real cursor movement, hijacked whatever
machine ran it, and failed whenever the desktop was busy. The arithmetic it covered now lives
in `Helpers/DragReorderMath.cs` — pure, and covered by `DragReorderMathTests` (threshold,
up/down moves, no-op and out-of-range). The two persistence tests were already synthetic.
5. Real-clip check (creator's `test_greenscreen_moving_subject.mp4`, 1920×1080): the detector
REJECTED it — a fully saturated green sits at value 1.0 and the detector's 0.98 brightness
ceiling threw it away. White highlights are excluded by the saturation floor, which is the
property that actually separates them, so the ceiling is gone. With it fixed, the clip is
detected as green (high confidence), and keyed through our own pipe 97.8% of pixels go
transparent. Measured: keying with the canonical #00B140 keys 0.0% of this clip — which is why
the key colour comes from the measured screen.
Tests: 567 passed, 0 failed, clean build 0 warnings. The gate's scope check flags
`Assets/Videos/test_greenscreen_moving_subject.mp4` — the creator's working copy, deliberately not
committed; the fixture lives under ytLive.Tests/Fixtures.
Add independent AppLog message types and explicit Class 1/2 severity. Wrap and color log rows, pin Class 1 above Class 2, and let acknowledgement return entries to the current-launch timeline without changing file format or rotation.
Ref: https://logtape.org/manual/levels
Add Live Chat and Logs tabs to the non-edit left panel. Expose a bounded process-local AppLog feed so the newest messages appear first without importing earlier startup.log contents or changing file logging.
Ref: https://obsproject.com/forum/resources/understanding-obs-studio-log.473
Keep non-owner resolution choices visible but muted. Selecting one preserves the free tier and opens About; external navigation is only the explicit llamacasty.com link. Ref: https://streamlabs.com/desktop
Creator directive 2026-11-01: the properties panels had 14 hand-rolled heading styles, 37
hand-rolled labels, four literal grey hex values, and 14 square checkboxes.
- PropertyHeader / PropertySubHeader / PropertyFieldLabel / PropertyHint in Themes/Controls.xaml
now carry the whole hierarchy: category bright and 11px, labels and hints dim, size and weight
doing the level. Colour groups the panel — bright for what you edit, dim for its labels and
help — and no new palette entry was needed; DESIGN.md stays the only colour source.
- Every checkbox in the panel is a pill switch (PropertyPillCheck, 32x16 animated thumb).
IsChecked semantics unchanged, so no binding moved.
- Literal #888/#666/#555 became OnSurfaceVariant / OnSurfaceDisabled.
- 13 headings, 2 sub-headers and 31 in-row labels now reference a style instead of repeating
colour + size + alignment inline.
Controls audited for backing: every Button/Slider/ComboBox/CheckBox/ToggleButton/TextBox in the
panel has a Command or Click, a Value binding, an ItemsSource or Selected*, IsChecked, or Text —
no dead controls. Release 0 warnings; typography and token tests pass.
Creator rulings, 2026-11-01.
- Master strip is 44px (from 36) and its meter is now the largest on screen: 420x14 with 32
segments, centred under the preview, with a 12px MASTER label — bigger than the 9px bay
labels. L/R imbalance is a flag on the left, MONO and the session clock on the right.
- The kbps / fps / dropped counters are gone from that line: the live readouts already sit
above the preview, and the duplicate was noise rather than telemetry.
- TRAX is its own bay (MIC | GAME | TRAX | LIP-SYNC) with its own meter bound to
TraxMeterFraction. It lived inside the game bay with no ladder, which meant whether the bed
was actually playing could not be answered from the dock — and the player already measures
its own output.
- Lip-sync is narrow (0.45 star) since it is one slider and three presets; the three sound bays
are equal width, so their meters keep the same relative size.
AudioDockLayout moves with it (156 = 112 + 44; collapsed 44) and the layout test is updated.
TraxBayTests now asserts the opposite of the old ruling: the bay HAS a meter, and the game bay
no longer carries it.
Creator directive, 2026-11-01. Settings stopped being a centred modal.
- **Drawer** (Controls/SettingsDrawer): hosted in PreviewPane's pull-out stack, 320px, the same
animated-width pattern as Stream Settings / YPP / Socials / TEST, opened by the gear rail tab
on a single left click. SettingsDrawerOpen is exclusive with the other four, both directions.
This also removes the modal's MaxHeight-700 scroll problem for good: the drawer is the height
of the preview.
- **Categories**: headings with controls indented beneath them. Zoom now owns *Zoom view shape*
(Circle / Phone / Tablet) and *Zoom box follows*; Recordings, Brand logo and Scene transition
are their own categories.
- **The app menu moved to the LlamaCasty wordmark** (right-click → Report Bug / Feature Request /
About). Left-click still opens About. The gear tab no longer carries a menu at all, so clicking
it does exactly one thing.
OverlayHost keeps only the Bug / Feature / About panels; its brand-logo handlers and the dev-panel
injection moved with the settings block. BrandLogoPreviewTests follows the control (same assertions,
new path). Release 0 warnings; 15 tests pass across brand logo, zoom, entitlement and preview
history.
Two creator directives, 2026-11-01.
**Ctrl-down dismisses an open zoom box.** Window-level PreviewKeyDown, so the same hand that
opened it can put it away. It cannot cancel itself at creation: Ctrl is already held before
the right-click that makes the box, so the keydown fires while nothing is open.
**Follow mode, default Anchor.** A PillToggle in App Settings ("Follow the box"), persisted
as Zoom.FollowBox. Off — the default — keeps the 2026-10-08 model: the capture stays on the
clicked point and dragging only moves the box over it. On, UpdateZoomMove re-anchors the
capture to the box centre (half the box's size, clamped per axis), so the magnifier shows
whatever it hovers. Flipping to Follow while the box is open snaps it immediately.
Three tests: anchor drags leave CaptureX alone, follow moves it to the expected quarter-offset
centre, and the setting round-trips through LayoutStore (tested against the store, not via two
view models — two live VMs fight over the temp DB, which is a fixture problem, not a bug).
Both new test classes share a collection tag: MainViewModel.LayoutPathOverride is static, and
xUnit runs classes in parallel, so without it the two view models collided (one failure in a
combined run, clean when run apart).
Release build 0 warnings; 14 tests pass across zoom, entitlement and preview history. Debug
build is blocked by the running app (PID 47388) — close it and the exe updates.
Creator report: Ctrl+right in edit mode and left-click in preview did nothing when
neither streaming nor recording. The bindings were correct; the action was not.
Two causes, both in the model rather than the handler:
- ActivateZoom opened with `if (!(IsLive || IsRecording)) return;` — the box could not be
created outside a session at all.
- ToggleZoom only ever called DismissZoom, so "toggle" could turn it off and never on.
Now ToggleZoom(x, y) activates at the cursor or dismisses if already open, and the live-only
guard is gone — the magnifier is an editing aid as much as a live one. Both call sites pass
the canvas point, so the box still anchors on the click rather than the frame centre.
Debug 0 warnings; ZoomBox + ZoomMagnifier tests pass (3).
Two event-pairing bugs found by validating every handler instead of trusting the table.
1. Ctrl+right-click opened the context menu in edit mode. Cause: WPF's PopupControlService
opens menus on right-button-UP, and only the DOWN event was being handled — so the zoom
toggled and the menu appeared under it. Both events are now handled when Ctrl is down.
(The creator saw this in edit mode; preview mode had the same latent bug.)
2. The social bar's left-click was dead in preview mode and dragged the layer beneath it in
edit mode. Cause: PreviewGrid handles the TUNNELLING PreviewMouseLeftButtonDown, which
fires before the bar's bubbling handler — and since preview-mode left-click now toggles
zoom, that handler swallowed the bar's click too. The preview handler now steps aside when
the point is inside the bar's bounds (SocialBarElement.RenderSize, translated to canvas).
Audited and left alone because they are correct: left-down → resize handle / element drag /
deselect; move → zoom drag before edit drag (by design); left-up → EndPreviewDrag; right-up
→ zoom-drag release then the Ctrl swallow.
Build Debug 0 warnings, 22 tests pass across ZoomBox, PreviewHistory and SocialBar.
Creator ruling 2026-11-01, and the reason is live-stream ergonomics: no two-handed
chords while a broadcast or recording is running.
- Edit mode: left selects / drags / resizes, right opens the context menu, Ctrl+right
toggles the zoom box. Chords are acceptable here because editing is a stopped activity.
- Preview mode: left toggles the zoom box, right opens the context menu, and Ctrl+right
is swallowed outright — handled rather than left to fall through, so Ctrl+right cannot
open a menu by accident while one hand is on the mouse.
This also removes the old live behaviour where right-click activated the magnifier at the
cursor point (and dismissed it). Zoom in preview is now a plain toggle; if point-zoom is
wanted back it needs a one-button gesture, not a chord.
Build: Release 0 warnings. Debug is currently blocked by the running app holding the exe
(PID 47252) — close it and the Debug build copies normally.
Three creator directives (2026-11-01), done ahead of the surge.
**Gear moved to the preview rail** — first tab, orange (Brush.Attention), gear icon
only. Left-click opens the same app menu the top bar gear owned (Settings / Bug /
Feature / About), right-click goes straight to App Settings; both behaviours moved
with it, so nothing lost an entry point. TopBar keeps the wordmark and EDIT.
**EDIT right edge aligned to the layers panel.** The work area is margin 8 + a 360px
column, the top bar is padding 16, so the shared edge is 352 inside the bar. TopBar
column 0 is now a fixed 352 grid with EDIT right-aligned in it — alignment by
arithmetic instead of by eye.
**Preview undo/redo: Ctrl+Z / Ctrl+Shift+Z.** Models/PreviewHistory snapshots element
REFERENCES plus geometry, visibility, order and membership — a deep clone would mirror
every property of a dozen element subclasses and quietly miss the next one added.
One snapshot per gesture (pushed at drag/resize start, never in the move loop), bounded
at 50, redo survives multiple undos, cleared on layout load. Gated on EditingActive:
undo while the preview shows program output would rewrite a scene nobody is looking at.
Honest scope, in the type's doc comment: layout edits only. Colour/opacity/border/font
edits are not captured yet, so Ctrl+Z after one of those restores the last layout change.
6 tests in PreviewHistoryTests; 19 pass across it and the neighbouring UI classes; Debug
and Release both build at 0 warnings.
The paid delta is output quality, so the rule needs to be measurable rather than a word:
921,600 pixels and 30 fps, fail-closed. Models/OutputProfile.cs is that rule as pure
policy — a permitted tier passes through untouched (Pro, or the free tier's own 720p30),
a disallowed one resolves to the best tier the budget allows and carries ITS bitrate, so
a capped user is never offered an 8 Mbps budget for a 720p frame.
Applied once, in BuildEncoderOptions. That is genuinely one place: the stream and the
local recording are two output blocks of ONE ffmpeg process fed by one EncoderOptions,
so there is no second encoder to forget. (commercial.md claimed "both encoders — never
two checks"; the code is stronger than the doc, and the doc is corrected.)
Developer override, as asked: a "Developer mode — all resolutions" checkbox in App
Settings, bound to MainViewModel.DeveloperMode, which feeds HasProOutput. It is not
hidden in a release build — Controls/DeveloperPanel's Page and Compile items are removed
when Configuration != Debug, and OverlayHost adds it at runtime inside #if DEBUG, so a
shipped exe has no BAML for it at all. The setter also refuses the value when
DevBuild.IsDevelopment is false, which is now the single source for that question (the
scoreboard partial had its own duplicate const).
Two traps worth their lines: a `const bool` dev flag makes `if (!Flag) return;` an
unreachable-code warning in one configuration or the other (CS0162), so DevBuild uses
static readonly; and a Condition on an ItemGroup is invisible to a test reading
item.Attribute("Condition") — read the parent.
Tests: 14 pass across OutputProfileTests, AssemblyIdentityTests and StorePackagingTests.
The invariant test is the one that matters: iterate every shipped tier under a free
entitlement and assert none emits more than the budget, so a future 4K tier fails here
instead of shipping to everyone. App builds Debug and Release at 0 warnings.
Merges the 2026-10-29 Stitch export into the existing 2026-10-26 token sheet and promotes the
file from "redesign exploration" to the normative standard:
- type scale (32→10px), spacing, radii and elevation levels — the three scales the old
DESIGN.md listed as missing
- meter thresholds: cyan ≤ -12 dB, amber -12..-3, red > -3
- interaction rules: focus ring, hover step, 150ms/90ms easing, no springs
Four export tokens rejected as Material-3 warm-seed artifacts (recorded with reasons):
tertiary #4cd7f6 (kept #38bdf8), outline #ab8986, outline-variant #5b403e,
on-surface-variant #e4beba (kept slate #94a3b8).
Product rulings recorded: #e94560 retired as the app's general accent (blue takes every
interactive role, red is live/record only, wordmark artwork untouched); error gets its own
surface (#93000a) so it cannot be mistaken for on-air red.
The export's dock, four bays, page-nav sidebar, CPU/GPU/NVENC readouts and acrylic blur are
explicitly marked exploratory, not spec — the design principle is a simpler surface, not
hardware cosplay. On-stream output styling stays outside this theme.
Falsified facts fixed in the same commit: product.md brand section now points at DESIGN.md for UI
tokens (MARCOM.md stays the marketing palette), and ui-surface.md no longer calls DESIGN.md an
unreconciled exploration.
Docs-only unit: scope-check run, no build.
ai.md 1,308 → 228 lines. No prose rewritten: every block was moved as-is so
nothing could be lost in translation, then replaced by a pointer.
New files:
- docs/ai/environment.md — LAN/SSH + Gitea push recipe, Windows dotnet
build/run, test-suite map, hermetic real-MainWindow test rule
- docs/ai/product.md — brand/naming, AI transparency, GPL licensing
prohibitions, free-forever monetization, auth/account assumptions,
packaging & distribution
- docs/ai/youtube-api.md — go-live ordering, the broadcast-binding fix, the
private-only lock, health parsing, end-stream pre-flight, Test mode,
chat insert shape
- docs/ai/overlays.md — social bar, native alert box (animations, clip,
ticker), chat box conventions
- docs/ai/ui-surface.md — control surface, scene strip, preview rail,
hotkeys, broadcast drawer, toasts, transitions, brand logo, launch
geometry, theme/properties conventions
ai.md keeps the response/derivation rules, the architecture table, the four
existing invariant stubs, and one pointer per moved subsystem.
docs/ai/index.md routes every file with a "read when" column.
References falsified by the move were fixed in the same commit: AGENTS.md
build note → environment.md → Run, and the "Related" footers in
frame-pipeline.md / output.md / capture.md now point at the owning files.
Docs-only unit: scope-check run, no build (no source touched).