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
Model the AlertBox and standalone ticker with shared settings; scene tickers render the in-memory active and queued events without saving history. Keep AlertBox content active-event-only.
Ref: https://obsproject.com/kb/obs-studio-overview
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
The earlier pass checked wiring only; this is the typography sweep. Raw Foreground/FontSize
labels are gone from the elements panel:
- Opacity, Timer (minutes) and URI are headings above their control (PropertyHeader), with the
control row on the panel's 10px indent — the same shape as Border Thickness/Opacity.
- Scroll speed, scoreboard Opacity/Text size and their value readouts are PropertyFieldLabel.
- The drag hint and the scoreboard sign-in note are PropertyHint.
- Simulate (dev) keeps its orange, gains the field-label typography.
- Edge, Text colour and Background were PropertySubHeader/SubHeader: ONE heading rank now, so
PropertySubHeader and the legacy SubHeader are deleted from Themes/Controls.xaml.
- DeveloperPanel — the last panel with raw typography and an unstyled CheckBox — now uses the
same styles, and PropertiesStyleTests accepts PropertyPillCheck (the guard went stale when
every check box became a pill switch). 18 tests pass; Release 0 warnings.
Their rank is carried by size and colour, not by an indent — so Video, Sound byte and Ticker all
line up on the left edge exactly like each other, and only the controls under them tab in.
The brand exemption for the universal blur put a BrandLogoOverlay above the blur host while the
canvas ItemsControl kept drawing the same element, so two logos sat on top of each other.
- The canvas presenter for a brand logo is collapsed: BrandLogoOverlay is the only render, and it
now carries the element's Opacity so the canvas copy is not missed.
- Hit-testing follows the eye: a click inside the logo's bounds selects the logo, because it is the
topmost layer in the compositor too. Without that you saw the logo on top and grabbed the webcam
underneath it.
- SelectedLayerPreviewHitTestingTests updated to the new contract (collapsed presenter, logo wins
its bounds, list selection still works). 3 tests pass; Release 0 warnings.
The preview draws two background images — BackgroundImage and, above it, ActiveBackgroundImage
(the static art) — while the mini-pane reads the scene snapshot. The default-restore path only
called UpdateBackgroundImage, so the thumbnails updated and the preview did not.
It now calls UpdateActiveBackground as well.
Also adds the generic cure behind the Background bug: OnSelectedElementPropertyChanged invalidates
the scene bake for ANY property change. A static layer's pixels are cached by element identity, so
a property edit without invalidation repaints nothing and the control looks dead — the failure was
never specific to UseDefaultBackground, only first noticed there.
Background + thumbnail tests pass (21); Release 0 warnings.
Browse… writes the chosen picture into BOTH CustomBackgroundPath and AssetId, and the default
stamp bails when AssetId is already set. So flipping "Use default background" back on changed a
bool while the preview kept showing the custom image — it looked like the switch did nothing.
Turning the default on now clears the custom pointer and the asset, re-runs
StampDefaultBackgroundAsset for the scene's shipped art, and only then invalidates the bake,
updates the preview and refreshes thumbnails.
BackgroundTests pass (17); Release 0 warnings.
Review findings from a screenshot of the Stream Alerts panel, each addressed:
- "Reset to built-in" and "Browse…" were YtButton-derived (white, semi-bold, 13px), so a reset
action outranked the section header. Property-panel actions now use PropertyButton: bordered,
11px, normal weight, OnSurface text.
- Explanation text and field labels were the same muted blue-gray. PropertyHint is now
OnSurfaceDisabled, one clear step below a label.
- Groups inside a category ran together. PropertyDivider (1px OutlineVariant) separates Video,
Sound byte and Message ticker.
- The disabled loop switch was near-invisible. Disabled now keeps the label at OnSurfaceVariant
and dims the whole control to 0.55 — readable, obviously inactive.
- Switch "on" was full-strength accent, shouting about static state. The track now sits at 0.6
opacity when on, which also stops it competing with the slider's accent fill.
No new palette entries: every change uses an existing token, so DESIGN.md stays the one colour
source. Release 0 warnings; token and typography tests pass.
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.
New `youTubeTrainingVideos.md`: the feature list worth filming, split into "ready now" and
"after the surge lands" so nothing gets filmed before it exists, plus the format decisions
settled on 2026-11-01.
Format: 16:9 long-form is the library (search intent, walkthroughs, conversion); vertical Shorts
are trailers cut from the same pass. A 16:9 video cannot be a Short, so it is a per-video choice.
TikTok is a mirror of the verticals, never a strategy.
The numbers behind it: Shorts need 1,000 subs + 10M views in 90 days for YPP versus 4,000 watch
hours in a year; Shorts RPM is $0.02-0.08 per 1,000 against $3-8 for tutorials; and one $79 sale
nets ~$55.30, which is what 700k-2M Shorts views pay. Shorts are an install engine, not revenue.
Film first: privacy blur, auto-ducking, webcam blur.
Creator ruling 2026-11-01. One SKU, one price, Steam does the selling.
- Pro is a one-time Steam DLC on the free app: $79 introductory, $99 on a stated date, announced
where the price is shown and actually raised.
- The subscription is retired. The reasoning is in decisions.md: no service behind a monthly fee
(no servers, no cloud, no content), ~1-3 subscribers per 1,000 installs against 5-8% churn, and
it carried the most expensive code in the product.
- Polar goes dormant. Its customer-portal design is recorded, not built. The "freemium on Steam,
convert on Polar, Valve gets nothing" proposal is rejected: in-app purchases must use the Steam
Wallet, so it risks the listing to save ~$24 a sale.
- Entitlement collapses to `steamDlcOwned`. No lease, no key, no portal, no device limit; the
greyed tier opens the Steam store overlay, so every purchase stays inside Valve's own surface.
- The site publishes the model and links the store page. It takes no payment and hosts no binaries.
- The $400 lifetime died with the subscription it anchored. Founding 200 died with the coupons and
passes it needed.
Costs: $115 fixed year one; $79 nets $55.30 after Steam, so three sales cover the year.
Mic and desktop gains run -60 to 0 dB and that is the ceiling: 0 dB is full input level, and
nothing past it is offered or drawn. The unity tick is gone with the zone it explained.
The faders run -60 to +6 dB but the ladder tops out at 0 dBFS, so the last 6 dB looked like
dead track. The tick marks unity at 60/66 of the track: what is left of it is boost, and now it
reads that way. Both mic and desktop faders; the lip-sync slider is milliseconds and gets none.
Creator request: a sound-byte browse + label under Built-in video, with an option to loop the
sound while the clip plays. The video itself is always one pass to completion.
- Source gains AlertAudioPath and AlertAudioLoop, persisted (column + migration + INSERT +
SELECT + reader, the whole chain — that chain is where the blur crashed once).
- The sound byte plays through MediaPlayer on the default output device; MediaEnded re-seeks to
zero while looping, and StopClip kills it so a tail cannot bleed into the next alert.
- The clip's OWN soundtrack moved off the mixer and onto the device as well
(Services/Audio/AlertAudioOutput: one WaveOutEvent + buffered float provider).
Why the move: the creator asked that alert audio — the clip's track or the added sound byte —
appear in the desktop sound bar. Only audio that reaches the default device is picked up by the
WASAPI loopback that feeds both the meter and the stream. The old EnqueueAlertAudio route put the
sound in the stream but left the meter flat and the room silent.
Tests: the sound byte and its loop flag round-trip through SQLite, and a fresh alert layer starts
with no sound byte and no loop. Release 0 warnings.
TestAlert called Enqueue, which is the pure test seam: it fills the queue and returns. The
alert layer needs three more things to show anything — its scene list, the playback ticker
running, and the preview refreshed — and all three live in OnMessageReceived, the same entry
the chat feed uses (MainViewModel.Chat.cs).
Now it calls OnMessageReceived(message, Scenes), so the clip advances on the ticker and writes
frames to the box's VideoImageSource, which is what the preview pane displays.
Sits to the right of the Built-in video pill, labelled "Test alert". It enqueues one synthetic
Super Chat through the real AlertOverlayLayer, so what plays is exactly what a viewer triggers —
the custom clip when one is set, the built-in clip otherwise — and it goes through the same
output path (preview and encoder) as a live alert. Local only: nothing reaches YouTube and no
chat row is invented.
MIC's device name was the only header label at full-strength OnSurface (it was brightened
because it shows a choice), and the lip-sync reading was Tertiary — three different values
across four bays in the same row. All four header labels are now Brush.OnSurfaceVariant, the
standard secondary-text token. No other colours touched.
Every bay is now header / ladder / controls. Lip-sync has no ladder, so it holds an empty 20px
row (10px meter + the two margins) that keeps its slider exactly level with the other three,
and MIC's ladder gained the missing 4px bottom margin that had its slider sitting 4px high.
Presets stay under the slider in the narrow lip-sync bay.
Not under it: the offset slider now rides the header line beside the LIP-SYNC badge and the
reading, and the preset row is what stays below. The box is narrow, so a slider on its own line
was the only thing making it two-and-a-bit rows tall.
GenerateBuildStamp read BuildCounter.txt, added 1 and wrote it back on every CoreCompile, so
the number tracked BUILDS: a day of iteration reached #1122 against 563 commits. It now uses
git rev-list count of HEAD, which is what the comment always claimed (and why the file existed
in .gitignore). No counter file, no drift; a missing git still falls back to the start.
Also moves the MIC and lip-sync volume bars up one line: both were pinned to the floor of their
bay by a star row plus VerticalAlignment=Bottom. They now sit directly under their headers,
with the auto rows sized to content.
Capping the three bay meters at 320px and left-aligning them made each ladder look cut off
mid-bay, which is what the creator saw as deleted bars. The caps are gone; the master ladder
keeps its own 420-900px range so it stays the longest without shortening anything else.
The master meter was a fixed 420px, but each sound bay's ladder fills its column — on a wide
window a bay could be longer than the master, which is the opposite of what was asked.
Master now stretches between 420 and 760 px, and the three bay ladders cap at 320 with the same
cap for each, so they stay the same relative size to one another and stay shorter than the
master at every window width.
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.
The blur is universal, so it would swallow the logo along with everything else. The compositor
now defers every visible brand logo: skipped in both element loops, blitted immediately AFTER
the blur group, before the flash/social/ticker overlays. One logo per scene, so it is cheap.
Preview mirrors it: the staged scene's brand logo is drawn above the blur host (StagedBrandLogo),
so what you see in the editor is what the encoder gets.
Test: a blur covering the logo's area leaves the logo's pixels exactly white while the coloured
background beneath it blends.
The creator found the use case that reframes the feature: a moment of privacy while the webcam is
on. A blur that can only cover the desktop cannot do that.
- SceneCompositor applies the blur group AFTER every element is composited (still before the
flash / social bar / ticker / zoom overlays), so it covers webcam, chat, alerts — anything. Its
position in the layer list no longer affects the output, so it is added at the top and the
reorder clamp is gone.
- Supersedes the earlier "blur sits below the brand" rule: a universal blur does obscure the logo
when it covers it. Called out in frame-pipeline.md so it is not re-litigated.
- Preview mirrors it exactly: the blur host moved OUT of CanvasGrid to a sibling above it, so the
VisualBrush samples every layer (and cannot sample itself). It used to sample only the backdrop,
which is why a circle over the webcam would have shown sharp pixels in the editor and blurred
ones in the stream.
Two holes in the blur's property panel, both of which also affected other layers:
- X/Y/W/H were gated on IsWebcamSelected, so a blur (or an image, chat box or alert zone)
had no numeric position or size at all — only dragging. The block is generic: it always
edited SelectedElement's own values, so the gate was the only thing excluding them.
- Opacity showed for a blur, but the compositor's blur reads BlurAmount — the slider would
have moved a number that changes nothing in the output. Hidden for blur layers.
Blur panel is now: Box | Circle, strength, and the shared geometry fields.
The Blur item's Visibility is bound to CanAddBlur, which depends on the staged scene. The scene
setter never raised it, so the binding evaluated once (collapsed) and stayed collapsed for the
session — build 1082: the option simply wasn't there.
StagedScene now notifies CanAddBlur, with a test that pins both halves: Live true, Chat false,
and a notification on every staged-scene change.
Source.IsDraggable listed Image/Web/Chat/Alert only, so a blur had no selection box, no corner
handle, and no hit-test — the rectangle existed in the render but could not be grabbed. Adding
Blur to that list gives it the same selection chrome and resize path every placeable layer has
(drag to move, corner dot to resize, one undo snapshot per gesture).
It is selected on add (AddSource already does that), so the rectangle appears already outlined
and ready to size.
The preview canvas is WPF visuals; a blur layer contributes no visual of its own, so until now
the creator saw nothing where the blur was and could not tell what it was hiding.
Each visible blur now draws a rectangle (or ellipse) filled with a VisualBrush of the whole
BackdropLayer — the desktop capture plus the background art — sampled at the blur's own rect and
carrying a BlurEffect at the same strength the compositor uses. The host canvas sits after the
backdrop and before the element layers, which is the model's own z-rule, so the preview shows
exactly what the output will: desktop blurred, brand and everything above it untouched.
Rebuilt on scene change, on add/remove, and on any blur property change (position, size, shape,
strength) — the brush samples the live backdrop, so the picture keeps moving underneath.
LayoutStore.Load threw ArgumentOutOfRangeException on GetOrdinal("BlurAmount") during the
MainViewModel constructor, so the app died at startup rather than when a blur was used. The
migration adds the column and Save writes it; the reading query still listed its columns
explicitly and BlurAmount was missing from that list.
Adds LayoutBlurPersistenceTests: a blur saved and reloaded, asserting the amount and geometry
survive. That is the test that should have existed with the feature — the startup path it
covers has no other guard.
Creator request 2026-11-01: a layer that hides part of the desktop from viewers. Box or circle,
drag/resize like any layer, one strength slider, several allowed.
**Z-order is policy, not preference.** `AddSource` inserts a blur directly above the background
and therefore below the brand logo, and `OnSceneElementsReordered` pushes any blur that ends up
above the brand back down. Nothing else in the app can move it into the brand's way.
**Overlap = the strongest single blur, never a stack.** `SceneCompositor.ApplyBlurLayers` collects
every visible blur the first time one is reached in the z-order, sorts them strongest-first, and
shares one coverage map: a pixel already covered by a stronger blur is skipped. Blurring
back-to-front would have produced something stronger than any slider setting allows, which is
exactly what was ruled out.
**One blur curve, shared with the webcam.** `RegionBlur` carries the constants the webcam blur
already used — downscale 4, radius 1–6 downscaled px, `RadiusFor(strength)` — and
`BackgroundEffectProcessor` now delegates to it instead of holding its own copy. The layer's
slider is the same 0–1 scale with the same 0.5 default, so "half strength" means the same blur in
both features. Cost is proportional to the blur's own rect, not the frame.
**Live scene only**, enforced in `AddSource` as well as the (+) menu (`CanAddBlur`), so a stale
menu item or a scripted call cannot plant one on BRB. `ElementKind.Dynamic`, so it is never baked
into the static base and every render path gets it — pump, preview, thumbnails, scene dump.
Persistence: `Source.BlurAmount` column + migration; shape rides the existing `ClipShape`.
Properties panel: Box | Circle plus the strength slider, gated on `IsBlurSelected`.
4 tests: outside the mask is byte-identical, inside loses its hard edges, a circle leaves its
box corners alone, and two identical stacked blurs equal one (the coverage map). Release builds
at 0 warnings.
The five drawers each sized their content Grid to Width=320 inside a panel that itself
animates to 320, with 14px margins — so the content box was 348 wide in a 320 window and its
right edge was clipped. That is why every drawer's content jammed into the right margin
regardless of the margin value.
The Grid now fills the panel (no fixed width) with Margin 14,10,20,12: 14 on the left, 20
between the content and the right edge of the div, which is the requested gutter.
Two build-1047 notes. The settings content gets a 20px right margin (plus 4 left) inside the
scroll viewer, so text and controls stop running into the panel edge and the scrollbar has
somewhere to live. The rail tab stack is VerticalAlignment=Center instead of Stretch, so the
whole group of tabs sits centred against the preview rather than hanging from the top.
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.
The shell is MaxHeight 700 and the settings panel had grown past it with no ScrollViewer,
so everything below roughly the transition section was unreachable. That is why 'Tablet
(Horizontal Rectangle)' existed in the XAML and in the model but never appeared in the app:
the shape control itself was cut off, not the option.
Panel row is now * with an Auto ScrollViewer, so every section is reachable and the next
one added cannot silently fall off the bottom.
While zoom is active a left drag moves the box and never selects or moves a layer beneath
it (creator ruling 2026-11-01). MouseMove already owned the zoom drag; the missing guard was
in MouseLeftButtonDown, which happily hit-tested a layer first and left a stray selection
plus a half-started drag behind the moving box.
Creation holds Ctrl (Ctrl+right-click), and Windows repeats a held modifier. The repeat
tick landed after the box existed, so the dismiss handler saw an open box and closed it:
appear, then vanish ~500ms later. Guarded with e.IsRepeat, which only ever skips the
repeat events — a genuine second press still closes the box.