Commit Graph
469 Commits
Author SHA1 Message Date
gramps d25dbf58dc design: white ink on any accent fill (on-accent token)
The EDIT button's armed face used on-tertiary (#0b1326 navy) on blue — the creator read it as grey
on blue and could not read it. Same problem, latent, on the armed REC/ON-AIR pills, which used the
error ink.

One rule instead of two exceptions: a saturated accent fill carries WHITE ink. New token
on-accent #ffffff, used by the EDIT trigger and RecordPillToggle; on-tertiary stays for large blue
surfaces where dark ink is right. UiShellTests pins the EDIT foreground to it.
2026-10-30 10:34:19 -07:00
gramps 568586c541 fix(xaml): the app refused to launch — RecordPillToggle was defined before SegmentToggle
`BasedOn="{StaticResource SegmentToggle}"` resolves while the dictionary is being built, so a
forward reference fails at load with "Cannot find resource named 'SegmentToggle'". The style now
sits after SegmentToggle and SegmentLabel, where its base already exists.

Guard: ThemeResourceTests gains the ordering rule — within one dictionary, no StaticResource may
point at a key defined later in the same file. The existence check alone passed this, which is
exactly why the app died on launch while the tests were green.

Root cause of the unreliable test results this session: a stale `testhost` held ytLive.dll locked,
so builds silently kept the previous assembly (MSB3027 on copy). Killed it; the suite now rebuilds
and the two new facts actually run.
2026-10-30 10:29:22 -07:00
gramps 4841c27514 feat(ui): the creator's six corrections
1. Stream Info + YPP rails are YouTube surfaces → new `youtube` token #ff0000. It is documented
   as brand-only: live state stays `primary`, so the two reds never mean the same thing.
2. EDIT toggle: the checked trigger set the SAME colour as the resting face, so clicking it
   changed nothing visible. Resting is now the neutral surface with muted text; armed is blue with
   dark text, and the label follows the fill.
3. Active scene outline is orange again (`attention`) — it marks the scene you work on, not a
   selection; blue made it read as "selected". Live stays red.
4. Scene names wear the same pill as the hotkey chips (black, 10px radius, 8×2 padding), so both
   labels stay readable over any thumbnail.
5. REC / ON-AIR pills and the running stop button read red while armed/live — including test
   mode, where the top bar is gold. New `RecordPillToggle` style (its own template, because
   SegmentToggle hard-codes a transparent face); the stop button's grey `#333` literal is gone.
6. The properties list had `MaxHeight="420"` inside an Auto row, so it scrolled while the window
   had space left. The row is `*` now and the cap is gone.

Guard: UiShellTests pins all six to the markup and names the one that regresses.
2026-10-30 10:27:11 -07:00
gramps df0118ac52 docs(handoff): design conversion complete; spacing is a layout pass 2026-10-30 10:02:32 -07:00
gramps e89cc3682a test(socials): a real preview test — and the Cancel bug it immediately found
The two regressions that reached the creator both lived in preview XAML that no test loads: the
appearance controls painted nothing, then the bar vanished when the plate was off. Every existing
test was green through both.

This test drives the real drawer on a real MainWindow (hermetic temp DB, per the layout-pollution
rule) and reads the real preview elements:
- the bar is VISIBLE, whatever the plate switch says
- a drawer text colour reaches the glyph
- the plate carries its colour and ~50% alpha, and its 18,6 padding only when enabled
- the shadow is an actual DropShadowEffect on the glyph, and off by default
- Cancel restores the pre-edit look

It paid for itself on the first run: Cancel did NOT revert. The snapshot held the same config
object the live preview mutates, so "restore" restored the edited object. The snapshot is now the
six values, written back on cancel — entries untouched.

Also fixes the harness details that made the first attempts fail: everything runs on the app's STA
thread (window creation AND close), and the exception is rethrown with its stack so a failure is
diagnosable next time.
2026-10-30 10:02:06 -07:00
gramps 0822552830 design: the theme's 4px corners come from the radius token
Six control corners used a literal 4px while DESIGN.md defines radius-sm for exactly that role.
Component geometry stays literal on purpose (the pill toggle's 9px is half its 18px height — a
token there would be a lie about what the number means).
2026-10-30 09:59:12 -07:00
gramps ec01a7deb8 design: the last VM-side colours come from tokens (ThemeBrush)
The state colours the view-models hand to bindings — top bar, REC/ON-AIR dots, health banner,
meter bands, toast tints — were hex literals in C#. They now resolve from Themes/Tokens.xaml
through `Helpers/ThemeBrush`, so the values live in one place. Without an Application (unit tests)
the helper returns a neutral grey: visibly wrong rather than silently plausible.

Four tokens the code needed and the document lacked, added to DESIGN.md (so the doc-is-the-contract
guard demanded their brushes): info-container #3a3b52, success-container #1d5c38,
warning-container #b8860b, test-container #9c6f1c (the test-stream gold bar).

Also fixed by this: the top bar still painted the RETIRED brand red #e94560 when live — the
standard's #ff5451 now, and `OnAirSignTests` (which pinned the old value) says so. BarOn/BarOff
became properties rather than static fields, because a static field would resolve tokens before
the Application exists.

Verified: DesignTokensTests, GameMeterHonestyTests, NotificationAreaIntegrationTests,
OnAirSignTests, SessionTeardownTests — 38 tests green.
2026-10-30 09:58:52 -07:00
gramps 7adff685c0 design: the on-stream graphics join the standard (OutputPalette)
The broadcast picture is part of the app, so it stops carrying its own colours. Alert cards, the
ticker and the branding flash now paint from OutputPalette, which Mirrors DESIGN.md for the brand
tokens and owns the alert box's six-event accent language (gold/violet/pink — content, not
chrome; the standard has no purple and should not grow one, but they live in ONE file instead of
57 scattered literals).

It is a separate type because the renderers run on the frame-pump thread and must not touch
application resources. The duplication is made safe by OutputPaletteTests: it parses
Themes/Tokens.xaml and fails if any mirrored token drifts, and fails if a renderer reintroduces a
colour literal or "Segoe UI".

Ticker and flash glyphs now use the bundled Inter instead of Segoe UI. Neon red is the standard
live red — one red in the product, including the credit.

Fixed on the way: the palette's font fields were static pack-URI initialisers, which throw
TypeInitializationException in any host without a WPF Application (the test process caught it);
they are lazy now with a name fallback.
2026-10-30 09:30:56 -07:00
gramps c4642f1e99 design: every window inherits Inter (implicit Window style)
Dialogs are separate WPF windows, so the FontFamily set on MainWindow never reached them —
GoLive, Socials and the four pickers still rendered Segoe UI while the main window used Inter.

The theme now carries an implicit Window style (family + Display text mode + layout rounding).
Guard: DialogTypographyTests opens a real Window inside the real app and asserts it resolves the
bundled Inter family — which fails if the style or the font resource goes missing — and asserts
no dialog sets FontFamily itself, i.e. nobody opts out of the standard.
2026-10-30 09:18:50 -07:00
gramps 51d9dd09c7 fix(socials): the bar disappeared from the preview when the plate was off
My own regression from the previous commit: the plate Border WRAPPED the bar's content and its
Visibility was bound to BarBackgroundEnabled — so with the plate off (the default) the preview
collapsed the entire social bar. Nothing was wrong with the data; the element was there and
invisible.

Fix: the wrapper is always laid out. "Plate off" now means a TRANSPARENT background
(`SocialBarBackgroundBrush` returns Transparent when disabled) and zero padding, "on" means the
colour/opacity plate with 18,6 padding — which also matches what the renderer bakes for the
output, so preview and broadcast agree.

Guard (new fact in SocialBarAppearanceTests): the preview must never visibility-gate the bar on
BarBackgroundEnabled, the wrapper must bind SocialBarBackgroundBrush, and the plate's switching
must be Padding.
2026-10-30 09:06:53 -07:00
gramps c1212644b0 fix(socials): live preview of the bar appearance + the crash on dragging a slider
Two bugs from the creator's first look at the new controls.

1. CRASH — '{DependencyProperty.UnsetValue}' is not a valid value for property 'Fill'.
   The token conversion deleted `YtSliderKnobHoverBrush` while two slider triggers still
   referenced it. WPF does not fail that at build time: it substitutes UnsetValue and throws at
   the next layout, inside `Ellipse.OnRender` — so the first slider touched (the new opacity one)
   killed the app. The brush is back, as a glossier version of the rest state.
   Guard: `ThemeResourceTests` scans every app XAML and proves each {StaticResource} key is
   defined (theme/App resources or the same file). Proven by removing the key again — the test
   names it.

2. PREVIEW DID NOTHING — the preview bar is its own XAML rendering (not the rasterized strip the
   compositor overlays), and it was hard-wired white-on-transparent, so the drawer's controls had
   nothing to move. Now:
   - `SocialBarTextBrush` / `SocialBarBackgroundBrush` come from the same `SocialBarStyle` the
     renderer bakes, so preview and output cannot disagree.
   - the preview draws the plate (same colour + opacity) and the drop shadow.
   - the drawer applies changes to the live config as you edit (`ApplyAppearancePreview`), and
     Cancel restores the pre-edit snapshot — so the preview tracks the controls without
     persisting anything until Save.

46 tests green (theme, socials, design tokens, layout store, real-MainWindow meter test).
2026-10-30 08:57:09 -07:00
gramps c82636e1a9 feat(socials): bar appearance — text colour, background plate, opacity, text shadow
In the Socials drawer, under the show/hide switch: text colour, an optional background plate
with its own colour and opacity, and a text drop shadow. The bar had been hard-wired
white-on-transparent since it shipped.

- `SocialsConfig` gains five additive properties (BarTextColor, BarBackgroundEnabled,
  BarBackgroundColor, BarBackgroundOpacity, BarTextShadow).
- Persistence: five new Socials columns, added by the per-column idempotent migration pattern
  (a half-migrated DB heals instead of failing), written in Save and read back in Load with
  defaults for pre-existing rows.
- `SocialBarRenderer.Render(entries, SocialBarStyle?)` — the look is a separate value type so the
  renderer stays a pure "entries + look" function and the preview can never read a half-applied
  config mid-edit. A malformed hex falls back to white/black instead of throwing inside a render
  tick.
- The plate hugs the entries (rounded 20px, padding) so it reads as a caption bar, not a band
  across the frame. Off by default, because transparent is what creators have today.
- The colour pickers reuse `BorderColorPalette` — one catalogue of the app's colours, per the
  TASK 60/63 rule.

Guard: SocialBarAppearanceTests paints the real strip and inspects pixels — default is white with
no plate, a red text colour reaches the glyphs, a 55% blue plate yields half-transparent blue
pixels, and the shadow adds dark pixels that were not there before.
2026-10-30 08:47:50 -07:00
gramps 9528128ca7 feat(settings): brand-logo thumbnail with a hover enlargement
The creator (2026-10-30): the brand-logo field in App Settings showed only a file NAME, so a
myopic user could not confirm which picture was set. Now a 28x28 thumbnail sits left of the
field showing the actual image, and hovering it opens an enlarged copy (up to 420x320) beside
the panel with the file name underneath. A click collapses it; moving the pointer away closes
it, unless the pointer walked onto the popup itself, where it stays so the creator can look
closely.

Mechanics:
- `MainViewModel.BrandLogoPreviewImage` decodes the stored asset ONCE and freezes it, so the
  thumbnail and the enlargement share one bitmap and no dispatcher hop is needed. A corrupt
  image returns null instead of taking the dialog down.
- The thumbnail is hidden when no image is set (NotNullToVis), so there is nothing to hover and
  no empty frame can appear.
- The popup is a separate window: WPF refuses to open one until its placement target is in a
  live visual tree, which is why the test hosts the control in a real window rather than
  asserting on a detached control.

Guard: BrandLogoPreviewTests — the wiring (image binding, the no-image gate, the four gesture
handlers, the readable enlargement size) and the control's open/close behaviour.
2026-10-30 08:39:42 -07:00
gramps 7a49b38cae docs(handoff): every XAML surface tokenised; VM state colours are the last unit 2026-10-30 08:30:45 -07:00
gramps c885bbd782 design: the code-set state colours now agree with the standard
Last inconsistency from the sweep: several VM-side state colours are still literals, and after
the palette moved they disagreed with everything around them — the top bar's LIVE state would
have painted the retired pink-red while the tile beside it used the standard red.

Values aligned (tokenisation of these sites is its own unit — they are strings bound to
Background/Foreground, so the fix is a VM->Brush refactor, not a find/replace):
- live / on-air / recorder dot: #e94560 -> #ff5451 (the standard red)
- error banner: #8f1f1f -> #93000a (error-container)
- audio meter bands follow the doc's stated thresholds: nominal cyan #38bdf8, caution amber
  #ffb95f, peak #ff5451 — they were green/yellow/red, which contradicted DESIGN.md's own table

GameMeterHonestyTests pinned the old band colours; its intent (the meter scales by volume) is
unchanged, so the two colour expectations now name the new bands.
2026-10-30 08:30:33 -07:00
gramps 8502c1d194 design: app settings + dialogs to tokens (surface 5 of the sweep)
OverlayHost (the App Settings surface) and every dialog — Go Live, Socials, Mic picker, Camera
picker, Hotkey config, Reuse Image, Rename Recording, Keep Broadcast — now resolve from tokens.
That closes the last place the creator sees a dialog: the settings overlay was still wearing the
old palette while the main window had moved on.

Every #e94560 in these files was an ERROR or validation affordance (hotkey conflicts, socials
validation, the remove tooltip, the go-live error line) — six sites all borrowing the retired
brand red to mean "something is wrong". They are `error` now, which is what the standard says
red-adjacent means; live/record keeps `primary`.

Guard list extended to all nine files.
2026-10-30 08:29:28 -07:00
gramps 8415bb5f37 design: preview pane to tokens (surface 4), and one palette list instead of two
110 hardcoded colours gone from the preview — the biggest surface in the app. Nicknames:
- the webcam selection ring is the interactive accent (was the retired brand red)
- the mute chips are `attention` (a muted mic is a caution, not a live state) with a white play
  glyph; the live badge is `primary`
- YPP progress bars/track, TRAX level zones, the error line, the dim strips and the poster veil
  all resolve from tokens (`positive`, `secondary-container`, `error-container`, `scrim-dark`,
  `scrim-dark-strong`)

Plus the duplication this sweep exposed: the webcam's right-click "Border Color" submenu carried
**seven hand-written colours**, a second source of truth beside the properties drop-down that
TASK 63 had already unified onto `Models/BorderColorPalette.cs` — and the two lists disagreed
(7 vs 14). The submenu is now built from the palette when it opens, so every offered colour is
the same colour everywhere, with the palette's name beside it. The click handler is unchanged;
generated rows inherit the menu's SceneElement DataContext.

Guard list extended: Controls/PreviewPane.xaml.
2026-10-30 08:28:21 -07:00
gramps 66b85d3fcc docs(handoff): preview pane next, with the colour-picker trap recorded 2026-10-30 08:03:44 -07:00
gramps 3edb0d10be design: three tokens the sweep needed — positive, scrim-dark, scrim-dark-strong
The palette had no green for status and no black scrim, so two surfaces were about to either
invent literals or misuse an accent. Creator's call (2026-10-30): give green a token rather
than painting "connected" blue, which already means five other things.

- `positive` #22c55e — STATUS ONLY (connected, applied, healthy). Never an accent, never a
  button fill; the doc says so and the guard parses the doc.
- `scrim-dark` #26000000 (15% black) and `scrim-dark-strong` #00000080 (50%) — the preview's dim
  strips and heavy overlays. 18 sites justify a token, and they are structural, not decoration.
- LeftPanel's ✓ confirm now wears `positive` instead of the interactive accent, matching the
  status rule the new token states.

DesignTokensTests still green: the doc is the contract, so the three new rows demanded three
new brushes and got them.
2026-10-30 08:03:18 -07:00
gramps 2dbc35b285 design: left panel to tokens (surface 3 of the sweep) — 101 colours gone
The layers/properties panel is the surface the creator looks at most, and it carried the
largest hardcoded palette in the app: 50 label greys, 15 icon blues, and a scatter of
one-off hexes. All of it resolves from tokens now.

Three sites needed a decision rather than a mapping, because the same hex meant different things:
- the + add icon and the chat author name were the old brand red → interactive accent (blue)
- the webcam lock alert is an ERROR, not decoration → error surface + error border
- the ✕ revert affordance was red, which the standard reserves for live/record → muted. ✓ stays
  a positive confirm, now in the interactive accent rather than a green that exists nowhere else
  in the palette. If green checks are missed, a `positive` token is the way back, not a literal.
- Super Chat gold was #eab308; it is the standard's amber now (game audio + Super Chat only).

Guard list extended: Controls/LeftPanel.xaml can no longer regress to a raw colour.
2026-10-30 08:00:43 -07:00
gramps 8a2cb92d8c docs(handoff): shell converted, left panel is next 2026-10-30 07:56:13 -07:00
gramps 0275e30103 design: shell to tokens — top bar, window, scene strip (surface 2 of the sweep)
- Telemetry readouts (bitrate, FPS, dropped, live duration) now wear JetBrains Mono, so the
  tabular digits stop shifting width as values change — the first place the bundled mono face
  is actually visible.
- Scene tiles: staged = blue (selection), live/program = red. Both were orange, which meant the
  same colour marked two different states and neither was the live colour.
- EDIT toggle: "on" is the interactive accent, not orange. Orange now means warning only.
- Wordmark's two-tone accent, avatar chrome, readout panel, tile borders and the program badge
  all resolve from tokens.

Guard list extended: MainWindow.xaml, Controls/TopBar.xaml, Controls/SceneThumbnailStrip.xaml —
these three can no longer regress to a hardcoded colour.
2026-10-30 07:56:02 -07:00
gramps ffdf3b6fc9 design: wire the sans family into the window; fonts ship as explicit Resource items
The bundled faces were packaged but unused — no style referenced FontFamily.Sans, so the app
still rendered Segoe UI. MainWindow now sets FontFamily from the token, which cascades to every
child (dialogs follow in the sweep).

Font packaging: six explicit <Resource> items instead of the Assets\Fonts\*.ttf glob, so the
guard can name exactly what must ship.

Investigation worth recording: a runtime assertion that the families resolve from
pack://application:,,,/ would have needed an Application instance inside the test host, and
without one WPF resolves the pack authority against the test host — every probe failed,
including the app icon that demonstrably works. The resources ARE embedded (the lowercase
`assets/fonts/inter-regular.ttf` entry is present in ytLive.g.resources); the assertion was
removed rather than faked, and the real confirmation is a launch, which is a visual check.

Guard remains structural: files are real binaries, each is a Resource in the csproj, the theme
uses pack URIs, notices + both OFL texts are present, and no third family sneaks in.
2026-10-30 07:53:37 -07:00
gramps 1defb71568 docs(handoff): design standard landed, token sweep in flight 2026-10-30 07:47:14 -07:00
gramps 4ef84f2a97 design: convert Themes/Controls.xaml to tokens (surface 1 of the sweep)
The theme file that every control inherits is now token-driven: 73 hardcoded colours became
Brush.* references, and the two role changes the standard requires landed here first.

- Red leaves the chrome. The seven #e94560 sites (base button, focus borders, checked box and
  segment backgrounds, the selected-layer accent strip) were all interactive/selection roles,
  so they are blue now. Red stays for live/record only.
- The slider was green: its fill and gloss knob are blue now, with the track on
  SurfaceContainerLowest. The old green fill predated the standard.
- The pill toggle was ON = green, OFF = red — a settings control using the live colour. It is
  now OFF = neutral surface, ON = blue.
- Added scrim-soft/scrim-strong tokens (#22FFFFFF/#26FFFFFF) for the existing white washes, and
  Color.* resources for the two gradients (a GradientStop needs a Color, not a Brush).

Guard: DesignTokensTests gains Converted_surfaces_carry_no_raw_colour — a declared list of
converted files, so each sweep commit is guarded as it lands instead of waiting for one global
grep at the end. Pure white/black highlights are the only allowed literals.

No layout, spacing or behaviour change: this unit is colour only.
2026-10-30 07:47:00 -07:00
gramps 84c335a929 design: bundle Inter + JetBrains Mono (OFL-1.1) as the standard typefaces
The DESIGN.md type standard needs the fonts actually present — the machine's installed faces
are not a design system. Four Inter weights (400/500/600/700) and two JetBrains Mono weights
(500/600) ship as WPF resources from Assets/Fonts; the token families resolve them with the
`/Assets/Fonts/#Family` pack form, so nothing depends on an install or a network fetch.

Licensing (recorded in docs/ai/product.md): OFL-1.1 is a free licence but is NOT GPL-compatible
in the linking sense. These are separate .ttf files loaded through WPF's font stack — an
aggregate, not linked code — so they coexist with the GPL-3.0 app. Obligations carried:
copyright line in THIRD-PARTY-NOTICES.txt (new items 13-14), the OFL text beside the files
(Assets/Fonts/OFL-*.txt), no modification, no reserved-name reuse.

Guard: BundledFontsTests — files exist and are real binaries, the csproj ships them, the theme
uses pack URIs (a bare family name would silently fall back), the notice and both OFL texts are
present, and no third font family sneaks in.

Pending visual confirmation: the first launch should render panel text in Inter and telemetry
in JetBrains Mono.
2026-10-30 07:45:19 -07:00
gramps f2bf577f34 design: Themes/Tokens.xaml — one place a raw value may live, guarded by DESIGN.md
Tokens.xaml carries every colour, type size, spacing step, radius and motion duration from
DESIGN.md; App.xaml merges it BEFORE Controls.xaml so styles can resolve tokens.

Guard (DesignTokensTests, 4 facts): the test PARSES DESIGN.md — the colour tables under
"## Colour" are the contract — and fails if a documented token has no brush, if the hex
disagrees, if the meter bands drift, if Tokens ever merges after Controls, or if the retired
brand red #e94560 comes back as a token. Adding a colour to the standard now requires
implementing it, and changing the theme without the document is caught.

Inter/JetBrains Mono are declared as token families with system fallbacks; the bundled font
assets are the next unit.

No surface consumes tokens yet — this unit is the foundation only.
2026-10-30 07:43:17 -07:00
gramps 565b8dbd6f docs(design): DESIGN.md becomes the visual standard (tokens merged, controls rejected)
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.
2026-10-30 07:41:43 -07:00
gramps 5a102a4a37 docs: decisions log, queue trim, HANDOFF rewrite — the map stops being a treadmill
TASKS.md 467 → 66 lines: one line per live item, detail stays in TASKS/
task-*.md. The old catalogue (63 rows of paragraph status prose plus the
historical narration) is preserved verbatim as TASKS/archive.md — a
2026-10-29 snapshot, not the live queue. The "out of product" list and every
retired decision moved to docs/ai/decisions.md with cause of death.

HANDOFF.md 440 → 40 lines, and it is now OVERWRITTEN, not appended: branch,
what's in flight, next action, live landmines, pending creator checks. History
belongs to git log; the previous handoff is reachable at ee3c592:HANDOFF.md.

docs/ai/decisions.md (new) collects what no longer governs: the paid tier and
its pricing, Polar, Store IAP, the Store route and why it died, the deleted
Velopack updater, signing economics, the retired monetization ideas, and the
permanent out-of-product list.

Facts that lived in the old ai.md "Current limitations" section were re-homed
rather than dropped with the router rewrite: OAuth/DPAPI/session behaviour →
docs/ai/youtube-api.md; paths, persistence and the Google project number →
docs/ai/environment.md.

verify.sh caps now cover all four always-read files (AGENTS 210, ai 100,
HANDOFF 40, TASKS 120).

Docs-only unit: scope-check run, no build.
2026-10-29 11:10:51 -07:00
gramps c9ac348976 docs(ai): ai.md becomes a router; invariants move to AGENTS.md; budgets gated
ai.md 228 → 64 lines. It now carries only: what the product is, the build
commands, the architecture table, a "where the truth lives" table, and a
pointer to HANDOFF. No narrative, no status, no duplicated invariants.

AGENTS.md (195 lines) is now the single always-read file:
- the 20 invariants that break output, wipe user data, or ship a legal/trust
  breach — one line each, pointing at the doc that explains it (they replace
  110 lines of invariant stubs that duplicated docs/ai/*.md verbatim)
- Response style + No-Fluff Mode moved in from ai.md (rules belong in the rules file)
- onboarding is now: HANDOFF → ai.md router → the ONE docs/ai file → dir index;
  TASKS.md is read when the question is "what's next", not to write code
- memory rides with the change but is no longer a unit of its own; HANDOFF is
  ≤40 lines and overwritten, never appended

Anti-rebloat gate: scripts/verify.sh gains step 1/4, a doc-budget check
(AGENTS ≤210, ai ≤100; HANDOFF/TASKS caps land with the queue trim). A commit
that pads a capped file instead of moving detail fails the gate.

schema.md and docs/index.md corrected for the new roles and reading order.
Docs-only unit: scope-check run, no build.
2026-10-29 11:08:07 -07:00
gramps a311c8feb1 docs(ai): partition ai.md into five subsystem files (verbatim moves)
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).
2026-10-29 11:05:53 -07:00
gramps ee3c59210d Settings: group preferences and open from gear
Group App Settings by purpose and let right-click on the gear open settings directly while preserving the left-click menu.

WPF event reference: https://learn.microsoft.com/en-us/dotnet/api/system.windows.uielement.previewmouserightbuttondown?view=windowsdesktop-10.0
2026-10-28 17:57:02 -07:00
gramps 769c616706 Preview: promote selected layer for hit testing
Temporarily raise the selected preview item and prioritize it in the custom hit test without changing scene order. Refresh integration-test expectations for Brand logo layers and explicit thumbnail assets.

WPF references: https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.panel.zindex?view=windowsdesktop-10.0 https://learn.microsoft.com/en-us/dotnet/desktop/wpf/graphics-multimedia/hit-testing-in-the-visual-layer
2026-10-28 13:09:15 -07:00
gramps 7da287244b Transitions in edit mode and on output: program transitions run while recording or streaming; the preview animates the outgoing picture (fade/move fade, wipe shrinks) in every mode
Test: PreviewTransitionTests (with the transition, wipe, and brand logo tests).
2026-10-27 10:20:53 -07:00
gramps 841c905c06 Brand logo: one locked image layer per scene above the background, image from App Settings
- Source.IsBrandLogo persisted (additive migration); layer healed on load, removal blocked
- Image stored as an asset; settings keep the asset id and file name
- App Settings: Brand Logo file field with Browse
- Default placement top-left; per-scene eye uses the existing element visibility
Test: BrandLogoTests; layout/background tests pass (34). Not pushed.
2026-10-27 09:25:49 -07:00
gramps b86b6bb601 Transitions: Wipe (left-to-right reveal), added to the App Settings picker
Test: WipeTransitionTests. Not pushed.
2026-10-27 09:12:59 -07:00
gramps f67a263ccd App Settings: transition type and length picker
Cut/Fade/Move and a 100-2000 ms length, bound to the view model; changes save with the layout.
Test: TransitionPickerTests. Not pushed.
2026-10-27 09:12:37 -07:00
gramps 90cd039d98 Transitions: fade starts from the outgoing picture (was an instant cut)
The transition started with no from-frame, so BlendFrame returned the new scene at once.
Start now requests a capture and the frame pump supplies its last output on the next tick.
Test: TransitionFadeTests. Docs: ai.md, TASKS.md.

Not pushed.
2026-10-27 08:39:49 -07:00
gramps 7ae30d4cfc Right rail, top bar, bottom bar and chat border: Socials slide-out drawer, tab icons and colors, EDIT toggle, stream readouts moved to the top-left cell, webcam border restored, chat border controls, 30fps vertical tier
- Socials: the dialog is a right-rail drawer (same pattern as Stream Info/YPP/TEST); the live-screen button is removed
- Right rail: Stream Info (was Stream Settings) and YPP show the YouTube logo; Socials and TEST get icons; tab colors and label colors per spec
- Top bar: EDIT toggle beside REC (orange when on, blue when off); going live on YouTube turns it off
- Stream readouts (bitrate, FPS, dropped n (x.y%), Live hh:mm:ss, health) and the resolution dropdown moved to the top-left cell; bottom bar emptied at the same height
- Webcam border drawing restored (it was removed with the CRT bezel change)
- YouTube chat layer: border color, thickness, and opacity controls
- Vertical 1080p30 tier added

Not related to the fps gate yet; no gating is enforced.
2026-10-26 18:24:09 -07:00
gramps a8814b42d0 Top bar and scene strip: EDIT toggle beside REC (orange on, blue off); fixed-size centered mini screens; fixed 360px left column so the empty cell matches the layers panel
- TopBar: EDIT ToggleButton, spaced from the REC/ON-AIR segments
- MainViewModel: explicit EditingActive drives IsEditMode; YouTube go-live turns edit off, stop turns it on
- SceneThumbnailStrip: tiles fixed 196x130 and centered; dark background fills the column so its edge does not move on resize; strip row fixed 154px
- MainWindow: left column fixed at 360px; WIDTHS diagnostic log on resize

Not pushed.
2026-10-26 14:26:23 -07:00
gramps 3239808a73 Layers panel and scene strip: scene layers header with icons, per-type layer icons, PROGRAM badge and selected/live orange borders, scene name and hotkey overlays, Starting as default scene
- LeftPanel: SCENE LAYERS header with layer count; per-type blue icons; properties header gear; layers list min/max height; properties placeholder; list and properties indent
- SceneThumbnailStrip: names/hotkeys over the tile; PROGRAM badge with blinking red dot on the selected tile; orange border on selected and live tiles; fixed the element border override that hid the style border; reflag tiles on load
- LiveScene is now set on go-live, record start, and live transitions (it was never assigned, so IsLive never fired)
- Startup stages the scene named Starting
- DESIGN.md: color roles (red = YouTube/on-air/record, orange #ff8c3a = attention, amber = game audio/Super Chat)
- Staged live tile uses background art instead of the green LIVE placeholder

Not pushed.
2026-10-26 14:00:23 -07:00
gramps 24aca7d04a Scene strip: move top row over the preview column and scale it; remove CRT bezel art and on-air seed
- PreviewPane.xaml: drop the OBS-style border Rectangle/Ellipse over webcam elements
- SceneThumbnailStrip: Viewbox scales the row; height follows width so it fills the preview column; staged+live tile has no border
- MainWindow.xaml: strip moved to column 1 only, leaving the layers cell empty; Margin 8,0 matches the preview
- MainViewModel.Background: stop seeding the CRT scene tiles and the on-air image
- ytLive.csproj: per-build counter (BuildCounter.txt, gitignored) replaces the commit-count stamp

Build stamp now increments on every build.
2026-10-26 13:09:13 -07:00
gramps b7ab169718 docs(design): capture color + type tokens from the Stitch redesign exploration
DESIGN.md, elements only (no layout): the dark navy surface scale, the
single-role accent rule (blue = interactive, red = on-air/recording only,
amber = game audio + Super Chat), and the Inter/JetBrains Mono type split.
Sourced from the Google Stitch HTML export of the proposed edit screen,
2026-10-26. Not yet reconciled against Themes/Controls.xaml by design -
that comparison is deferred until the redesign direction is locked.
2026-10-26 11:00:51 -07:00
gramps 818d316477 docs(handoff): record the ai.md split progress (3 units, not pushed)
Session summary at the top: 3 docs commits split frame-pipeline, capture, and
output out of ai.md into docs/ai/*.md (verbatim moves + invariant stubs);
ai.md 2,702 -> 1,289 lines. Prior 2026-10-22 state preserved below unchanged,
headed as 'Prior state'.
2026-10-26 10:32:21 -07:00
gramps a4dfca6b6c docs(memory): split the output path (ffmpeg, encoder, recording, audio) out of ai.md
Unit 3. Verbatim move of 'FFmpeg locator', 'Live encoder + RTMP push',
'Local recording' and 'Live audio capture' into docs/ai/output.md, grouped as
encoder / recording / audio with a 'Related' footer.

ai.md's stub keeps the decisions that must not be re-litigated or rediscovered:
ffmpeg is bundled (never download/fetch; no-network test), args pure and no -re,
probe order NVENC->QSV->AMF->OpenH264 and never libx264, REC/ON-AIR radio-
exclusive by creator ruling, a modal's falsy result must never fall through into
its side effect, full StopStream teardown on every failure, audio pipe closes
BEFORE the video pump, and honest-by-measurement audio (true dBFS meters,
SpeechLeveler + -1 dBFS limiter, live Func gains).

- ai.md Services row and one 'FFmpeg locator section above' pointer repointed;
  MyMistakes.md dead-Pin entry repointed.
- docs/ai/index.md gains the output.md row.

ai.md 1,558 -> 1,289 lines. No source touched; no push.
2026-10-26 10:28:01 -07:00
gramps 096bf6debe docs(memory): split capture (backdrop, webcam, effects, vertical tier) out of ai.md
Unit 2 of slimming the mandatory read. Verbatim move again - the four capture
blocks leave ai.md unchanged in content, and one stub carries the law.

- docs/ai/capture.md: 'Screen backdrop capture', 'Webcam capture', 'Webcam
  background effects' and 'Vertical (9:16) tier camera placement', byte-
  identical, grouped under backdrop / webcam / vertical headings, with a
  'Related in ai.md' footer.
- ai.md stub keeps: the locked-Background model and NormalizeBackgrounds;
  ActiveBackgroundImage is fed by static art only (2026-08-23 regression);
  window:/picker: keys are session-scoped and a live pin beats auto-detect;
  the settled WinRT interop paths (TryGetDataUnsafe, from-DXGIDevice); the
  VideoFrame seam, own-catch subtype ladder and no fallback camera; one app-
  level webcam identity with live-lock attainability; CameraManager as the one
  effects processing point with per-session mask smoothing; geometry-only
  vertical tier and the model-level rect lock.
- docs/ai/index.md gains the capture.md row; Services/index.md (2) and
  TASKS/task-38-window-backdrop.md repointed.

ai.md 1,975 -> 1,558 lines. No source touched; no push.
2026-10-26 10:26:53 -07:00
gramps 804b1d2f5f docs(memory): split the frame pipeline out of ai.md into docs/ai
ai.md is the mandatory read every session; at 2,702 lines / ~64k tokens it
costs the whole subsystem history even when the task never touches it. This
is a verbatim move, not a compression - nothing is deleted and each stub is
written from the moved text.

- docs/ai/frame-pipeline.md: 'Frame cadence', 'Per-element composite timing +
  scene dump', 'What the first instrumented take said', 'Render budget',
  'Scene compositor' and 'Live frame pipeline', byte-identical, plus a short
  'Related in ai.md' footer and two stale 'below' pointers corrected.
- ai.md keeps two stubs carrying the invariants that must not be missed
  (overlay on every render path, VideoFrame.Epoch, StripPool threads,
  Release-only render numbers; arrival-stamped rawvideo, non-blocking submit,
  stop order, burned _outputIndex judge) and links the new file.
- docs/ai/index.md + docs/index.md route the split; schema.md inventory lists
  the new tier.
- 4 index-row references and 2 TASKS.md anchors repointed; ai.md's Services
  row too.

Measured: ai.md 63.6k -> 47.1k tokens, 2,702 -> 1,975 lines. Progressive
disclosure per schema.md convention 5. No source touched; no push.
2026-10-26 10:25:21 -07:00
gramps ef60f1f3b6 TASK 48: distribution & packaging is deferred until we are near v1
Creator ruling 2026-10-26: the remaining programming work is far from
release and a complete UX redesign is about to start, so packaging,
signing, installer and updater decisions would be chasing scope that is
about to move. The task is deferred, not cancelled.

Recorded what is frozen: TASK 48 items 6-8, the signing decision, the
updater choice (Velopack's deletion left no update path, and
StorePackagingTests currently vetoes a new one), and the route question
itself - ai.md says the Store route is dead (direct + Steam) while
TASKS.md/task-48/HANDOFF still read Store MSIX + Store IAP. That
contradiction is to be settled in ONE sweep at the revisit, not patched
piecemeal now.
2026-10-26 10:20:04 -07:00
gramps c768a8ee49 fix(border-color): keep ComboBox popup clicks inside LeftPanel (TASK 63) 2026-10-22 12:02:05 -07:00
gramps daac7689fd TASK 48 slice 2: the app packs as a Microsoft Store MSIX
The manifest the Store route needs, and a real package built from it.

Packaging/Package.appxmanifest is a full-trust packagedClassicApp: EntryPoint
Windows.FullTrustApplication, uap10:TrustLevel=mediumIL,
uap10:RuntimeBehavior=packagedClassicApp, the rescap runFullTrust capability, and
webcam + microphone DeviceCapabilities. That combination is what lets the packaged
app launch the bundled ffmpeg as a child process and write to the user's profile;
an appContainer package would virtualise the recordings folder instead. x64 only,
en-US only (policy 10.7).

Packaging/Assets/ carries the four tiles the packer requires, generated from the
app's own Assets/llamacasty-logo.jpg by scripts/make-store-logos.py - listing art
and app art cannot drift.

scripts/package-msix.sh publishes Release win-x64 self-contained into
artifacts/msix-stage (a RID-less publish would copy every RID's ONNX native),
stages the manifest as AppxManifest.xml + Assets, and runs the Windows SDK's
makeappx. MSIX_PFX/MSIX_PFX_PASSWORD sign a package for a local test install; the
Store signs submissions. artifacts/ is gitignored.

First real package: artifacts/LlamaCasty.msix, 159 MB, 509 files, 'Package
creation succeeded'.

Schema traps found by packing (makeappx reports one error at a time):
Square310x310Logo and Wide310x150Logo are NOT uap:VisualElements attributes - they
live on uap:DefaultTile.

Guard: StorePackagingTests.The_package_manifest_declares_full_trust_camera_and_
microphone - identity/architecture, the full-trust application element, the three
capabilities, no appContainer VALUE (not the file text: the manifest's own comment
says the word), en-US only, and every referenced logo present.

Full suite 438/438, 0 warnings. Cold-start smoke test after the Velopack removal:
MainWindow loaded, no exceptions.
2026-10-22 09:05:50 -07:00