Auto-launch + auto-hide Teams: 'I only see TeamsISO' experience
Some checks failed
CI / build-and-test (push) Failing after 28s

Two new persisted preferences in DISPLAY settings, paired to give operators the 'launch TeamsISO, never see Teams' experience the user asked for:

- LaunchTeamsOnStartup: TeamsISO auto-starts Teams in the background each launch (fire-and-forget background task in App.OnStartup, after the main window has materialized so a slow Teams launch doesn't delay the UI).

- AutoHideTeamsWindows: as soon as Teams' windows materialize after launch, hide them. New TeamsLauncher.AutoHideAfterLaunchAsync runs a polling loop (250ms / up to 15s) that catches the splash, main window, and any follow-up panels Teams opens. Teams takes 2-5s to render its main window and the splash arrives separately, so a one-shot hide right after launch wouldn't be enough.

When TeamsISO starts and Teams is already running (from a prior session), the auto-hide path still fires so the 'I only see TeamsISO' rule applies even when Teams was launched externally.

Operator drives everything through the IN-CALL bar (mute / camera / share / leave / marker) + participants DataGrid (ISO routing). Eye-toggle in the rail still restores Teams windows on demand.

Both toggles default to off — opt-in. Persisted via UIPreferences so they survive process restart.
This commit is contained in:
Zac Gaetano 2026-05-10 20:35:00 -04:00
parent 598938ede5
commit d8186c5eb8
6 changed files with 163 additions and 4 deletions

View file

@ -221,6 +221,42 @@ public partial class App : Application
await _viewModel.InitializeAsync(CancellationToken.None);
// Auto-launch Teams in the background if the operator has opted in.
// Combined with AutoHideTeamsWindows this gives the "I only see
// TeamsISO" experience — Teams runs but never appears on screen,
// and all interaction routes through the IN-CALL bar + participants
// DataGrid. Fire-and-forget so a slow Teams launch doesn't delay
// TeamsISO's window from appearing.
if (_viewModel.Settings.LaunchTeamsOnStartup && !Services.TeamsLauncher.IsRunning())
{
_ = Task.Run(() =>
{
try
{
if (Services.TeamsLauncher.TryLaunch(out var launchError))
{
if (_viewModel.Settings.AutoHideTeamsWindows)
_ = Services.TeamsLauncher.AutoHideAfterLaunchAsync();
}
else
{
logger.LogWarning("Auto-launch Teams on startup failed: {Error}", launchError);
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "Auto-launch Teams on startup threw");
}
});
}
else if (_viewModel.Settings.AutoHideTeamsWindows && Services.TeamsLauncher.IsRunning())
{
// Teams is already up from a previous session. If auto-hide is
// on, hide it now so the operator's "I only see TeamsISO" rule
// applies even when Teams was launched externally.
_ = Services.TeamsLauncher.AutoHideAfterLaunchAsync();
}
// Background update check, throttled to once per 24h. Fire-and-forget
// so a slow / offline update server never delays startup. Surfaces a
// banner via UpdateBanner if newer; failures just log.

View file

@ -1251,6 +1251,20 @@
Margin="0,12,0,0"
ToolTip="When checked, minimizing the window hides it and shows a tray icon. Useful for long unattended shows. Double-click the tray icon to restore."/>
<!-- Phase E.1/E.2 "I only see TeamsISO" pair. With both ticked,
the operator launches TeamsISO and never sees the Teams UI —
Teams runs in the background and all interaction routes
through the IN-CALL bar + participants DataGrid. -->
<CheckBox Content="Launch Microsoft Teams on TeamsISO startup"
IsChecked="{Binding Settings.LaunchTeamsOnStartup}"
Margin="0,16,0,0"
ToolTip="When checked, TeamsISO will auto-launch Microsoft Teams in the background each time it starts. Combine with 'Auto-hide Teams windows' for the 'I only see TeamsISO' experience."/>
<CheckBox Content="Auto-hide Teams windows when launched"
IsChecked="{Binding Settings.AutoHideTeamsWindows}"
Margin="0,8,0,0"
ToolTip="When checked, Teams' windows are hidden as soon as they materialize after launch. Use the eye-icon button in the rail to restore them when needed. Drives Teams via the IN-CALL bar (mute / camera / share / leave / marker) and the participants DataGrid for ISO routing."/>
<Separator Margin="0,16,0,8"/>
<CheckBox Content="Record ISOs to disk"

View file

@ -141,8 +141,19 @@ public partial class MainWindow : Window
}
else
{
toast?.Show("Launching Microsoft Teams…");
_teamsWindowsHidden = false;
var autoHide = (DataContext as MainViewModel)?.Settings.AutoHideTeamsWindows ?? false;
toast?.Show(autoHide
? "Launching Microsoft Teams (will hide windows automatically)…"
: "Launching Microsoft Teams…");
if (autoHide)
{
_ = TeamsLauncher.AutoHideAfterLaunchAsync();
_teamsWindowsHidden = true;
}
else
{
_teamsWindowsHidden = false;
}
}
return;
}

View file

@ -251,6 +251,60 @@ public static class TeamsLauncher
return windows.Count;
}
/// <summary>
/// Fire-and-forget background watcher that polls every 250ms for up to
/// <paramref name="timeout"/> and hides any visible top-level Teams
/// windows it finds. Used after launch so the operator never sees the
/// Teams UI flash on screen — Teams takes 2-5s to splash + render its
/// main window, and the splash arrives separately from the main window
/// (so we keep polling past the first hide to catch follow-up windows).
///
/// Returns the Task so callers can await completion if they want, but
/// production code should fire-and-forget. Exceptions are swallowed —
/// failure to hide is harmless (user just sees Teams briefly).
/// </summary>
public static Task AutoHideAfterLaunchAsync(TimeSpan? timeout = null, CancellationToken ct = default)
{
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(15));
return Task.Run(async () =>
{
try
{
var hiddenAny = false;
while (!ct.IsCancellationRequested && DateTime.UtcNow < deadline)
{
// Poll for visible windows. Each iteration may catch new
// ones — Teams sometimes opens a small splash, then a
// larger main window 1-2s later, then a "What's new"
// banner. Keep hiding until we've gone a full second
// with nothing new appearing.
var hidden = HideWindows();
if (hidden > 0)
{
hiddenAny = true;
// Settling delay: after we hide windows, wait a beat
// before polling again so we don't busy-loop while
// Teams' window manager catches up.
await Task.Delay(750, ct).ConfigureAwait(false);
}
else if (hiddenAny)
{
// We hid at least once; if the next poll finds
// nothing, Teams has settled. Bail early.
return;
}
else
{
// Teams hasn't materialized yet; keep waiting.
await Task.Delay(250, ct).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException) { /* expected on cancel */ }
catch { /* defensive — auto-hide is best-effort, never breaks the app */ }
}, ct);
}
// ────────────────────────────────────────────────────────────────────
// Keyboard-shortcut forwarding (PostMessage path).
//

View file

@ -39,7 +39,13 @@ public static class UIPreferences
bool AutoDisableOnDeparture = false,
SortMode ParticipantSort = SortMode.JoinOrder,
bool MinimizeToTray = false,
bool ControlSurfaceLanReachable = false);
bool ControlSurfaceLanReachable = false,
// Phase E.1 / E.2 quality-of-life. With both true, the operator launches
// TeamsISO and never sees the Teams UI — Teams auto-starts in the
// background and its windows are auto-hidden as soon as they materialize.
// All control happens via the IN-CALL bar + participants DataGrid.
bool LaunchTeamsOnStartup = false,
bool AutoHideTeamsWindows = false);
public static Prefs Load()
{

View file

@ -34,6 +34,8 @@ public sealed class GlobalSettingsViewModel : ObservableObject
private UIPreferences.SortMode _participantSort = UIPreferences.SortMode.JoinOrder;
private bool _minimizeToTray;
private bool _controlSurfaceLanReachable;
private bool _launchTeamsOnStartup;
private bool _autoHideTeamsWindows;
public GlobalSettingsViewModel(IIsoController controller, ToastViewModel? toast = null)
{
@ -59,6 +61,8 @@ public sealed class GlobalSettingsViewModel : ObservableObject
_participantSort = uiPrefs.ParticipantSort;
_minimizeToTray = uiPrefs.MinimizeToTray;
_controlSurfaceLanReachable = uiPrefs.ControlSurfaceLanReachable;
_launchTeamsOnStartup = uiPrefs.LaunchTeamsOnStartup;
_autoHideTeamsWindows = uiPrefs.AutoHideTeamsWindows;
// Bring the auto-apply flag in from the presets store so the checkbox
// reflects the user's prior choice when the settings panel opens.
@ -222,7 +226,41 @@ public sealed class GlobalSettingsViewModel : ObservableObject
AutoDisableOnDeparture: _autoDisableOnDeparture,
ParticipantSort: _participantSort,
MinimizeToTray: _minimizeToTray,
ControlSurfaceLanReachable: _controlSurfaceLanReachable));
ControlSurfaceLanReachable: _controlSurfaceLanReachable,
LaunchTeamsOnStartup: _launchTeamsOnStartup,
AutoHideTeamsWindows: _autoHideTeamsWindows));
/// <summary>
/// Auto-launch the Microsoft Teams desktop client when TeamsISO starts.
/// Paired with <see cref="AutoHideTeamsWindows"/> gives the operator a
/// "TeamsISO is the only window I see" experience — Teams runs in the
/// background, all interaction happens through the participants DataGrid
/// + IN-CALL bar.
/// </summary>
public bool LaunchTeamsOnStartup
{
get => _launchTeamsOnStartup;
set
{
if (SetField(ref _launchTeamsOnStartup, value)) PersistUiPrefs();
}
}
/// <summary>
/// Auto-hide Teams' top-level windows as soon as they materialize after
/// a launch (whether triggered via <see cref="LaunchTeamsOnStartup"/>,
/// the rail button, or the eye-toggle). Runs a brief background poll
/// that calls <c>TeamsLauncher.HideWindows</c> every ~250ms for up to
/// 15 seconds, catching splash + main + follow-up panels.
/// </summary>
public bool AutoHideTeamsWindows
{
get => _autoHideTeamsWindows;
set
{
if (SetField(ref _autoHideTeamsWindows, value)) PersistUiPrefs();
}
}
/// <summary>
/// Record each newly-enabled ISO's normalized output to disk under