First step of Phase E.1 from the new spec at docs/superpowers/specs/2026-05-08-embedded-teams-orchestration.md: a third icon in the left rail launches the Microsoft Teams desktop client as a subprocess of TeamsISO so the operator doesn't have to leave the app to start a meeting.
Services/TeamsLauncher tries the ms-teams: URI first, falls back to %LOCALAPPDATA%\\Microsoft\\WindowsApps\\ms-teams.exe (new Teams), then the classic Update.exe handoff. On failure surfaces a friendly MessageBox with the install link.
The spec doc lays out the full three-phase roadmap (launcher -> window orchestration -> in-app meeting controls via Graph API or UIAutomation) and explicitly calls out what's out of scope (replacing Teams' media stack).
_NEXT.md updated to mark Phase D done and queue Phase E + remaining polish items (code-signing, Inter/JetBrains Mono font bundling, real Wild Dragon dragon-mark, drops counter, running-fps display).
Code review on d14a33a..bab29b0 turned up three real issues, fixed here.
1. EngineLogging.CreateDefault no longer mutates Serilog.Log.Logger. The static set was a belt-and-suspenders attempt to catch any code path that reaches for the singleton, but it doesn't matter (engine code uses ILogger<T>, never Serilog.Log.*) and it raced under xUnit's parallel test execution.
2. IsoPipeline stops holding a RawFrame reference for stats. The receiver-side TappedChannelWriter callback now snapshots only Width/Height into volatile ints — frame's pixel buffer is allowed to GC on its normal schedule and a late stats poll can never resurrect a dropped frame. (Today the buffer is fully managed so a use-after-free wasn't actually possible, but the snapshot pattern is the right ownership shape.)
3. App.xaml.cs's ComponentDispatcher.ThreadFilterMessage subscription now lives in a field and is unsubscribed in OnExit. Mutex release is gated on a new _ownsSingleInstanceMutex flag so the 'lost the race; shut down silently' path doesn't accidentally try to release a handle it never owned.
Plus a load-bearing comment in NdiInteropPInvoke.CreateFinder explaining why we free the UTF-8 group buffers right after the native call returns — same lifetime contract Phase B-2's CreateReceiver / CreateSender have always relied on; if it's wrong, those would fail too. The loopback discovery integration test would catch a regression.
Tests: 74/74 unit + 9/9 NDI integration green.
MainWindow drops the standard Windows title bar (WindowStyle=None + WindowChrome with CaptionHeight=44, ResizeBorderThickness=6, UseAeroCaptionButtons=False) and draws its own minimize / maximize-restore / close buttons inline in the existing header strip. The custom buttons opt into shell:WindowChrome.IsHitTestVisibleInChrome=True so clicks fire on them rather than starting a window drag.
Result: the entire top of the window is now ours, matching the Microsoft Teams desktop client's flush header look. The 'TeamsISO + by Wild Dragon' branding sits at the same baseline as the engine-status pill and the caption controls, and dragging anywhere not occupied by an interactive widget moves the window.
Caption-button styles in the theme: 46x32 hover-tinted, with the close button turning the Windows 11 #C42B1C red on hover. Maximize-button glyph swaps between the single-rectangle and overlapping-rectangles variants on StateChanged.
Drive-by: ParticipantViewModel.{FramesIn,FramesOut,IncomingResolution} setters dropped from private to public so {Run Text=...} bindings (which default to TwoWay on Run) can attach without WPF throwing 'cannot work on read-only property'.
IsoPipeline now publishes refs to its currently-live NdiReceiver and NdiSender (set by RunInnerPipelineAsync, cleared on exit) so a stats poll from any thread can read FramesCaptured / FramesSent without entangling the pipeline's lifetime with its observer. The receiver's raw-frame channel is wrapped with a TappedChannelWriter so the most recent RawFrame is captured for source-resolution display, again without changing the receiver's contract.
IsoController.GetStats() drops the stub return-Empty and instead reads the live pipeline.GetStats() outside the gate so a slow stats read can't serialize the controller's other operations.
WPF: MainViewModel runs a 1 Hz DispatcherTimer that pulls stats for every participant view-model and pushes them via UpdateStats(). ParticipantViewModel grows three displayable properties — FramesIn, FramesOut, IncomingResolution — bound into the participants DataGrid as a new 'Live' column showing the down/up frame counts and the source resolution underneath the machine name.
Tests: 74/74 unit + 9/9 NDI integration green; the existing round-trip integration test exercises the new wiring at runtime (live receiver/sender refs are set, frames flow, channels close cleanly).
Adds Serilog.Sinks.File to TeamsISO.Engine and a new EngineLogging.CreateDefault() factory that writes to BOTH the existing console sink and a rolling daily file at %LOCALAPPDATA%\\TeamsISO\\Logs\\teamsiso<date>.log. The WPF host (TeamsISO.exe is a WinExe with no console attached at runtime) now uses CreateDefault so support has something to ask for when users file an issue. The Console build keeps using CreateConsole — stdout is the right surface there and shell redirection beats a competing on-disk sink.
Files roll daily, cap at 10 MB before mid-day rollover, and only the most recent 14 are retained. Disk flush interval is 250 ms so a tail -f from another tool sees lines promptly. Path is announced via the first log line on every startup.
Two unit tests gate the wiring: AllLoggers_WriteToFile (verifies both typed and named CreateLogger() reach the file) and LogsAtBelowMinimumLevel_AreSuppressed (regression guard for level filtering). 74/74 unit tests pass (was 72).
Also adds a startup breadcrumb log line in App.OnStartup carrying the build version + PID so we can correlate a user's log file with a specific commit.
Replaces the Stone theme with Wild Dragon branding (canvas #0A0A0A, accent cyan #97EDF0, secondary #9AE0FD, coral alert #FB819C — sourced from wilddragon.net) and reorganizes MainWindow into a Microsoft Teams-style three-column layout: a 72px left rail (logo + Participants/Settings nav + engine-status indicator), a center content area (header + participants card), and a right settings panel.
Adds InitialsConverter so participant avatars render real initials (Brendon Power -> 'BP', '(Local)' -> 'L') instead of a generic glyph. Drops the obsolete StoneTheme.xaml; the project now ships exactly one theme dictionary.
Typography: Inter (with Segoe UI Variable Display fallback) for the sans stack, JetBrains Mono (Cascadia Mono fallback) for machine names and timecodes — matching the wilddragon.net site.
Verified live against the running Teams meeting: app launches, participant 'Brendon Power' displays with avatar, settings panel surfaces NDI groups + Hide-Local toggle, engine status pill shows green/live.
Two simultaneous TeamsISO processes contend over the NDI runtime, the same default sender names, and %APPDATA%\\TeamsISO\\config.json — observed during testing when launchers / shortcuts produced duplicate windows. Add a Local namespace per-user-keyed mutex (Local\\WildDragon.TeamsISO.SingleInstance.<username>) at startup; if a second instance can't claim it, broadcast a registered window message ('WildDragon.TeamsISO.BringToFront') and Shutdown(0). The running instance subscribes to that message via ComponentDispatcher.ThreadFilterMessage and surfaces its main window when received.
Per-user keying lets two different Windows users on the same machine each run their own TeamsISO. Mutex is released and disposed on OnExit.
Verified: Start-Process the exe twice in a row -> only one process remains, with the original window surfaced.
Adopts the design language from Dammyjay93/interface-design: warm Stone neutrals, accent orange (#EA580C), borders-only depth (no shadows), 8px spacing grid, all-caps section labels, mono typography for machine names and timecodes.
Themes/StoneTheme.xaml is the single source of truth for design tokens (color brushes, typography styles, spacing) plus restyled control templates (Button, TextBox, ComboBox, ComboBoxItem, CheckBox, DataGrid + DataGridColumnHeader / DataGridRow / DataGridCell, ScrollBar). MainWindow consumes the tokens and is laid out with a header (title + status pill), section-headed Settings sidebar (Output Format / NDI Network / Display), card-wrapped participant list, and a mono status footer.
Settings sidebar surfaces the new NDI group configuration (discovery + output) and a Hide-(Local) checkbox. The latter filters the user's own self-preview from the participants list at the MainViewModel layer (HideLocalSelf=true by default) so operators don't accidentally route their own preview as an ISO. Apply Changes round-trips both FrameProcessingSettings and NdiGroupSettings through the controller in one go.
- TeamsISO.App: hand-rolled net8.0-windows WPF csproj since the WPF
template isn't shipped on linux-arm64 .NET SDK; UI is a placeholder
for Phase C.
- TeamsISO.Engine.IntegrationTests: cross-platform xunit project with a
skipped scaffold fact tagged [Trait("requires", "ndi")] for Phase B.
- TeamsISO.Linux.slnf: solution filter for non-Windows CI that excludes
the WPF project (which can only build on Windows).