Repository: microsoft/terminal (language: cpp)

Issue:
[Terminal Chat AI] Allow company wide disabling this feature 
<!-- 
🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨

I ACKNOWLEDGE THE FOLLOWING BEFORE PROCEEDING:
1. If I delete this entire template and go my own path, the core team may close my issue without further explanation or engagement.
2. If I list multiple bugs/concerns in this one issue, the core team may close my issue without further explanation or engagement.
3. If I write an issue that has many duplicates, the core team may close my issue without further explanation or engagement (and without necessarily spending time to find the exact duplicate ID number).
4. If I leave the title incomplete when filing the issue, the core team may close my issue without further explanation or engagement.
5. If I file something completely blank in the body, the core team may close my issue without further explanation or engagement.

All good? Then proceed!
-->

# Description of the new feature/enhancement

Some companies don't allow the usage of AI features or software that has AI feature for privacy reasons. 

Please implement a way to centrally force disable this feature.

# Proposed technical implementation details (optional)

This can be a policy or HKLM registry value/setting that admins can use to easily disabled this feature for all users.

This setting should work for portable, store and manually installed versions and on all chanels.



Discussion / hints:
Perfectly reasonable ask. I'll link it up to #15000, thanks!

My understanding of GPO is that it can be used to set a reg key, so we can easily query that regardless of what distribution method used. 
@zadjii-msft 
I suggest you to implement a common class where you do the registry reading and that return standardized values like we did in PowerToys (https://github.com/microsoft/PowerToys/blob/main/src/common/utils/gpo.h).

Either you evaluate the Policy ones at application startup or every time the settings window and chat window gets opened.

The default expected behavior of policies is that computer scope has a higher priority than user scoope. And that you are able to set it either in user or in computer scope.

I think the category in the ADMX file should be `Microsoft\Windows Components\Windows Terminal`. The Registry key can be `Software\Policies\Microsoft\Windows Terminal` or `Software\Policies\Microsoft\Windows\Terminal`.
Given the current setup process requires custom azure resources, isn't that sufficient for an approval process at least short term?
> Given the current setup process requires custom azure resources, isn't that sufficient for an approval process at least short term?

I thought everyone is able to create such an required account including private persons. Is that wrong? Don't know about this.

If I require a special company account/license/configuration then it should be enough for short term.

Fix patch:
diff --git a/policies/WindowsTerminal.admx b/policies/WindowsTerminal.admx
index 48c36aec7eb..ccda3e0ffd7 100644
--- a/policies/WindowsTerminal.admx
+++ b/policies/WindowsTerminal.admx
@@ -9,6 +9,7 @@
   <supportedOn>
     <definitions>
       <definition name="SUPPORTED_WindowsTerminal_1_21" displayName="$(string.SUPPORTED_WindowsTerminal_1_21)" />
+      <definition name="SUPPORTED_WindowsTerminalCanary_1_23" displayName="$(string.SUPPORTED_WindowsTerminalCanary_1_23)" />
     </definitions>
   </supportedOn>
   <categories>
@@ -24,5 +25,12 @@
         <multiText id="DisabledProfileSources" valueName="DisabledProfileSources" required="true" />
       </elements>
     </policy>
+    <policy name="EnabledLMProviders" class="Both" displayName="$(string.EnabledLMProviders)" explainText="$(string.EnabledLMProvidersText)" presentation="$(presentation.EnabledLMProviders)" key="Software\Policies\Microsoft\Windows Terminal">
+      <parentCategory ref="WindowsTerminal" />
+      <supportedOn ref="SUPPORTED_WindowsTerminalCanary_1_23" />
+      <elements>
+        <multiText id="EnabledLMProviders" valueName="EnabledLMProviders" required="false" />
+      </elements>
+    </policy>
   </policies>
 </policyDefinitions>
diff --git a/policies/en-US/WindowsTerminal.adml b/policies/en-US/WindowsTerminal.adml
index 5516292e123..089b4bdd7e2 100644
--- a/policies/en-US/WindowsTerminal.adml
+++ b/policies/en-US/WindowsTerminal.adml
@@ -7,6 +7,7 @@
     <stringTable>
       <string id="WindowsTerminal">Windows Terminal</string>
       <string id="SUPPORTED_WindowsTerminal_1_21">At least Windows Terminal 1.21</string>
+      <string id="SUPPORTED_WindowsTerminalCanary_1_23">At least Windows Terminal Canary 1.23</string>
       <string id="DisabledProfileSources">Disabled Profile Sources</string>
       <string id="DisabledProfileSourcesText">Profiles will not be generated from any sources listed here. Source names can be arbitrary strings. Potential candidates can be found as the "source" property on profile definitions in Windows Terminal's settings.json file.
 
@@ -18,11 +19,25 @@ Common sources are:
 For instance, setting this policy to Windows.Terminal.Wsl will disable the builtin WSL integration of Windows Terminal.
 
 Note: Existing profiles will disappear from Windows Terminal after adding their source to this policy.</string>
+      <string id="EnabledLMProviders">Enabled Language Model/AI Providers</string>
+      <string id="EnabledLMProvidersText">The listed Language Models/AI Providers will be available for use in Terminal Chat.
+
+Enabling the policy but leaving the list empty disallows all providers and therefore disables the Terminal Chat feature completely.
+
+Common providers are:
+- AzureOpenAI
+- OpenAI
+- GitHubCopilot
+
+For instance, setting this policy to GitHubCopilot will allow the use of GitHubCopilot in Terminal Chat.</string>
     </stringTable>
     <presentationTable>
       <presentation id="DisabledProfileSources">
         <multiTextBox refId="DisabledProfileSources">List of disabled sources (one per line)</multiTextBox>
       </presentation>
+      <presentation id="EnabledLMProviders">
+        <multiTextBox refId="EnabledLMProviders">List of enabled Language Model/AI Providers (one per line)</multiTextBox>
+      </presentation>
     </presentationTable>
   </resources>
 </policyDefinitionResources>
diff --git a/src/cascadia/QueryExtension/ExtensionPalette.cpp b/src/cascadia/QueryExtension/ExtensionPalette.cpp
index 29fec0e9c0d..9606a96d1ea 100644
--- a/src/cascadia/QueryExtension/ExtensionPalette.cpp
+++ b/src/cascadia/QueryExtension/ExtensionPalette.cpp
@@ -99,16 +99,16 @@ namespace winrt::Microsoft::Terminal::Query::Extension::implementation
         _lmProvider = lmProvider;
         _clearAndInitializeMessages(nullptr, nullptr);
 
-        const auto brandingData = _lmProvider.BrandingData();
-        const auto headerIconPath = brandingData.HeaderIconPath().empty() ? terminalChatLogoPath : brandingData.HeaderIconPath();
+        const auto brandingData = _lmProvider ? _lmProvider.BrandingData() : nullptr;
+        const auto headerIconPath = (!brandingData || brandingData.HeaderIconPath().empty()) ? terminalChatLogoPath : brandingData.HeaderIconPath();
         Windows::Foundation::Uri headerImageSourceUri{ headerIconPath };
         Media::Imaging::BitmapImage headerImageSource{ headerImageSourceUri };
         HeaderIcon().Source(headerImageSource);
 
-        const auto headerText = brandingData.HeaderText().empty() ? RS_(L"IntroText/Text") : brandingData.HeaderText();
+        const auto headerText = (!brandingData || brandingData.HeaderText().empty()) ? RS_(L"IntroText/Text") : brandingData.HeaderText();
         QueryIntro().Text(headerText);
 
-        const auto subheaderText = brandingData.SubheaderText().empty() ? RS_(L"TitleSubheader/Text") : brandingData.SubheaderText();
+        const auto subheaderText = (!brandingData || brandingData.SubheaderText().empty()) ? RS_(L"TitleSubheader/Text") : brandingData.SubheaderText();
         TitleSubheader().Text(subheaderText);
     }
 
@@ -234,9 +234,9 @@ namespace winrt::Microsoft::Terminal::Query::Extension::implementation
             }
         }
 
-        const auto brandingData = _lmProvider.BrandingData();
+        const auto brandingData = _lmProvider ? _lmProvider.BrandingData() : nullptr;
         const auto responseAttribution = response.ResponseAttribution().empty() ? _ProfileName : response.ResponseAttribution();
-        const auto badgeUriPath = _lmProvider ? brandingData.BadgeIconPath() : winrt::hstring{};
+        const auto badgeUriPath = brandingData ? brandingData.BadgeIconPath() : L"";
         const auto responseGroupedMessages = winrt::make<GroupedChatMessages>(time, false, winrt::single_threaded_vector(std::move(messageParts)), responseAttribution, badgeUriPath);
         _messages.Append(responseGroupedMessages);
 
diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp
index 36a2b20b5bf..1aeb8cb013e 100644
--- a/src/cascadia/TerminalApp/AppActionHandlers.cpp
+++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp
@@ -662,18 +662,23 @@ namespace winrt::TerminalApp::implementation
     void TerminalPage::_HandleToggleAIChat(const IInspectable& /*sender*/,
                                            const ActionEventArgs& args)
     {
-        if (ExtensionPresenter().Visibility() == Visibility::Collapsed)
-        {
-            _loadQueryExtension();
-            ExtensionPresenter().Visibility(Visibility::Visible);
-            _extensionPalette.Visibility(Visibility::Visible);
-        }
-        else
+        args.Handled(false);
+        // only handle this if the feature is allowed
+        if (WI_IsAnyFlagSet(AIConfig::AllowedLMProviders(), EnabledLMProviders::All))
         {
-            _extensionPalette.Visibility(Visibility::Collapsed);
-            ExtensionPresenter().Visibility(Visibility::Collapsed);
+            if (ExtensionPresenter().Visibility() == Visibility::Collapsed)
+            {
+                _loadQueryExtension();
+                ExtensionPresenter().Visibility(Visibility::Visible);
+                _extensionPalette.Visibility(Visibility::Visible);
+            }
+            else
+            {
+                _extensionPalette.Visibility(Visibility::Collapsed);
+                ExtensionPresenter().Visibility(Visibility::Collapsed);
+            }
+            args.Handled(true);
         }
-        args.Handled(true);
     }
 
     void TerminalPage::_HandleSetColorScheme(const IInspectable& /*sender*/,
diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp
index f17d32cdea5..fbb1d62c829 100644
--- a/src/cascadia/TerminalApp/TerminalPage.cpp
+++ b/src/cascadia/TerminalApp/TerminalPage.cpp
@@ -989,58 +989,61 @@ namespace winrt::TerminalApp::implementation
                 _SetAcceleratorForMenuItem(commandPaletteFlyout, commandPaletteKeyChord);
             }
 
-            // Create the AI chat button.
-            auto AIChatFlyout = WUX::Controls::MenuFlyoutItem{};
-            AIChatFlyout.Text(RS_(L"AIChatMenuItem"));
-            const auto AIChatToolTip = RS_(L"AIChatToolTip");
-
-            WUX::Controls::ToolTipService::SetToolTip(AIChatFlyout, box_value(AIChatToolTip));
-            Automation::AutomationProperties::SetHelpText(AIChatFlyout, AIChatToolTip);
-
-            // BODGY
-            // Manually load this icon from an SVG path; it is ironically much more humane this way.
-            // The XAML resource loader can't resolve theme-light/theme-dark for us, for... well, reasons.
-            // But also, you can't load a PathIcon with a *string* using the WinRT API... well. Reasons.
-            {
-                static constexpr wil::zwstring_view pathSVG{
-                    L"m11.799 0c1.4358 0 2.5997 1.1639 2.5997 2.5997"
-                    "v4.6161c-0.3705-0.2371-0.7731-0.42843-1.1998-0.56618"
-                    "v-2.2501h-11.999v7.3991c0 0.7731 0.62673 1.3999 1.3998 1.3999"
-                    "h4.0503c0.06775 0.2097 0.14838 0.4137 0.24109 0.6109l-0.17934 0.5889"
-                    "h-4.1121c-1.4358 0-2.5997-1.1639-2.5997-2.5997"
-                    "v-9.1989c0-1.4358 1.1639-2.5997 2.5997-2.5997"
-                    "h9.1989zm0 1.1999h-9.1989c-0.77311 0-1.3998 0.62673-1.3998 1.3998"
-                    "v0.59993h11.999v-0.59993c0-0.77311-0.6267-1.3998-1.3999-1.3998"
-                    "zm1.3999 6.2987c0.4385 0.1711 0.8428 0.41052 1.1998 0.70512 0.9782 "
-                    "0.80711 1.6017 2.0287 1.6017 3.3959 0 2.4304-1.9702 4.4005-4.4005 "
-                    "4.4005-0.7739 0-1.5013-0.1998-2.1332-0.5508l-1.7496 0.5325c-0.30612 "
-                    "0.0931-0.59233-0.1931-0.49914-0.4993l0.53258-1.749c-0.35108-0.6321-0.55106-1.3596-0.55106-2.1339 "
-                    "0-2.3834 1.8949-4.3243 4.2604-4.3983 0.0395-0.0012 0.0792-0.00192 "
-                    "0.1191-0.00208 0.0069-8e-5 0.0139-8e-5 0.0208-8e-5 0.5641 0 1.1034 "
-                    "0.10607 1.599 0.2994zm0.0012 3.701c0.2209 0 0.4-0.1791 0.4-0.4 "
-                    "0-0.221-0.1791-0.4001-0.4-0.4001h-3.2003c-0.22094 0-0.40003 0.1791-0.40003 "
-                    "0.4001 0 0.2209 0.17909 0.4 0.40003 0.4h3.2003zm-3.2003 1.6001h1.6001c0.221 "
-                    "0 0.4001-0.1791 0.4001-0.4s-0.1791-0.4-0.4001-0.4h-1.6001c-0.22094 0-0.40003 "
-                    "0.1791-0.40003 0.4s0.17909 0.4 0.40003 0.4z"
-                };
-                try
+            // Create the AI chat button if AI features are allowed
+            if (WI_IsAnyFlagSet(_settings.GlobalSettings().AIInfo().AllowedLMProviders(), EnabledLMProviders::All))
+            {
+                auto AIChatFlyout = WUX::Controls::MenuFlyoutItem{};
+                AIChatFlyout.Text(RS_(L"AIChatMenuItem"));
+                const auto AIChatToolTip = RS_(L"AIChatToolTip");
+
+                WUX::Controls::ToolTipService::SetToolTip(AIChatFlyout, box_value(AIChatToolTip));
+                Automation::AutomationProperties::SetHelpText(AIChatFlyout, AIChatToolTip);
+
+                // BODGY
+                // Manually load this icon from an SVG path; it is ironically much more humane this way.
+                // The XAML resource loader can't resolve theme-light/theme-dark for us, for... well, reasons.
+                // But also, you can't load a PathIcon with a *string* using the WinRT API... well. Reasons.
                 {
-                    hstring hsPathSVG{ pathSVG };
-                    auto geometry = Markup::XamlBindingHelper::ConvertValue(winrt::xaml_typename<WUX::Media::Geometry>(), winrt::box_value(hsPathSVG));
-                    WUX::Controls::PathIcon pathIcon;
-                    pathIcon.Data(geometry.try_as<WUX::Media::Geometry>());
-                    AIChatFlyout.Icon(pathIcon);
+                    static constexpr wil::zwstring_view pathSVG{
+                        L"m11.799 0c1.4358 0 2.5997 1.1639 2.5997 2.5997"
+                        "v4.6161c-0.3705-0.2371-0.7731-0.42843-1.1998-0.56618"
+                        "v-2.2501h-11.999v7.3991c0 0.7731 0.62673 1.3999 1.3998 1.3999"
+                        "h4.0503c0.06775 0.2097 0.14838 0.4137 0.24109 0.6109l-0.17934 0.5889"
+                        "h-4.1121c-1.4358 0-2.5997-1.1639-2.5997-2.5997"
+                        "v-9.1989c0-1.4358 1.1639-2.5997 2.5997-2.5997"
+                        "h9.1989zm0 1.1999h-9.1989c-0.77311 0-1.3998 0.62673-1.3998 1.3998"
+                        "v0.59993h11.999v-0.59993c0-0.77311-0.6267-1.3998-1.3999-1.3998"
+                        "zm1.3999 6.2987c0.4385 0.1711 0.8428 0.41052 1.1998 0.70512 0.9782 "
+                        "0.80711 1.6017 2.0287 1.6017 3.3959 0 2.4304-1.9702 4.4005-4.4005 "
+                        "4.4005-0.7739 0-1.5013-0.1998-2.1332-0.5508l-1.7496 0.5325c-0.30612 "
+                        "0.0931-0.59233-0.1931-0.49914-0.4993l0.53258-1.749c-0.35108-0.6321-0.55106-1.3596-0.55106-2.1339 "
+                        "0-2.3834 1.8949-4.3243 4.2604-4.3983 0.0395-0.0012 0.0792-0.00192 "
+                        "0.1191-0.00208 0.0069-8e-5 0.0139-8e-5 0.0208-8e-5 0.5641 0 1.1034 "
+                        "0.10607 1.599 0.2994zm0.0012 3.701c0.2209 0 0.4-0.1791 0.4-0.4 "
+                        "0-0.221-0.1791-0.4001-0.4-0.4001h-3.2003c-0.22094 0-0.40003 0.1791-0.40003 "
+                        "0.4001 0 0.2209 0.17909 0.4 0.40003 0.4h3.2003zm-3.2003 1.6001h1.6001c0.221 "
+                        "0 0.4001-0.1791 0.4001-0.4s-0.1791-0.4-0.4001-0.4h-1.6001c-0.22094 0-0.40003 "
+                        "0.1791-0.40003 0.4s0.17909 0.4 0.40003 0.4z"
+                    };
+                    try
+                    {
+                        hstring hsPathSVG{ pathSVG };
+                        auto geometry = Markup::XamlBindingHelper::ConvertValue(winrt::xaml_typename<WUX::Media::Geometry>(), winrt::box_value(hsPathSVG));
+                        WUX::Controls::PathIcon pathIcon;
+                        pathIcon.Data(geometry.try_as<WUX::Media::Geometry>());
+                        AIChatFlyout.Icon(pathIcon);
+                    }
+                    CATCH_LOG();
                 }
-                CATCH_LOG();
-            }
 
-            AIChatFlyout.Click({ this, &TerminalPage::_AIChatButtonOnClick });
-            newTabFlyout.Items().Append(AIChatFlyout);
+                AIChatFlyout.Click({ this, &TerminalPage::_AIChatButtonOnClick });
+                newTabFlyout.Items().Append(AIChatFlyout);
 
-            const auto AIChatKeyChord{ actionMap.GetKeyBindingForAction(L"Terminal.OpenTerminalChat") };
-            if (AIChatKeyChord)
-            {
-                _SetAcceleratorForMenuItem(AIChatFlyout, AIChatKeyChord);
+                const auto AIChatKeyChord{ actionMap.GetKeyBindingForAction(L"Terminal.OpenTerminalChat") };
+                if (AIChatKeyChord)
+                {
+                    _SetAcceleratorForMenuItem(AIChatFlyout, AIChatKeyChord);
+                }
             }
 
             // Create the about button.
@@ -5755,31 +5758,34 @@ namespace winrt::TerminalApp::implementation
             }
         }
 
-        // we now have a provider of the correct type, update that
-        winrt::hstring newAuthValues = authValuesString;
-        if (newAuthValues.empty())
+        if (_lmProvider)
         {
-            Windows::Data::Json::JsonObject authValuesJson;
-            const auto settingsAIInfo = _settings.GlobalSettings().AIInfo();
-            switch (providerType)
+            // we now have a provider of the correct type, update that
+            winrt::hstring newAuthValues = authValuesString;
+            if (newAuthValues.empty())
             {
-            case LLMProvider::AzureOpenAI:
-                authValuesJson.SetNamedValue(L"endpoint", WDJ::JsonValue::CreateStringValue(settingsAIInfo.AzureOpenAIEndpoint()));
-                authValuesJson.SetNamedValue(L"key", WDJ::JsonValue::CreateStringValue(settingsAIInfo.AzureOpenAIKey()));
-                newAuthValues = authValuesJson.ToString();
-                break;
-            case LLMProvider::OpenAI:
-                authValuesJson.SetNamedValue(L"key", WDJ::JsonValue::CreateStringValue(settingsAIInfo.OpenAIKey()));
-                newAuthValues = authValuesJson.ToString();
-                break;
-            case LLMProvider::GithubCopilot:
-                newAuthValues = settingsAIInfo.GithubCopilotAuthValues();
-                break;
-            default:
-                break;
+                Windows::Data::Json::JsonObject authValuesJson;
+                const auto settingsAIInfo = _settings.GlobalSettings().AIInfo();
+                switch (providerType)
+                {
+                case LLMProvider::AzureOpenAI:
+                    authValuesJson.SetNamedValue(L"endpoint", WDJ::JsonValue::CreateStringValue(settingsAIInfo.AzureOpenAIEndpoint()));
+                    authValuesJson.SetNamedValue(L"key", WDJ::JsonValue::CreateStringValue(settingsAIInfo.AzureOpenAIKey()));
+                    newAuthValues = authValuesJson.ToString();
+                    break;
+                case LLMProvider::OpenAI:
+                    authValuesJson.SetNamedValue(L"key", WDJ::JsonValue::CreateStringValue(settingsAIInfo.OpenAIKey()));
+                    newAuthValues = authValuesJson.ToString();
+                    break;
+                case LLMProvider::GithubCopilot:
+                    newAuthValues = settingsAIInfo.GithubCopilotAuthValues();
+                    break;
+                default:
+                    break;
+                }
             }
+            _lmProvider.SetAuthentication(newAuthValues);
         }
-        _lmProvider.SetAuthentication(newAuthValues);
 
         if (_extensionPalette)
         {
diff --git a/src/cascadia/TerminalSettingsEditor/AISettings.xaml b/src/cascadia/TerminalSettingsEditor/AISettings.xaml
index 676071a0930..c99020d6b97 100644
--- a/src/cascadia/TerminalSettingsEditor/AISettings.xaml
+++ b/src/cascadia/TerminalSettingsEditor/AISettings.xaml
@@ -40,8 +40,9 @@
                            Style="{StaticResource TextBlockSubHeaderStyle}" />
                 <!--  GitHub Copilot  -->
                 <local:SettingContainer x:Uid="AISettings_GithubCopilot"
-                                        Style="{StaticResource ExpanderSettingContainerStyle}"
-                                        Visibility="{x:Bind ViewModel.GithubCopilotFeatureEnabled}">
+                                        CurrentValue="{x:Bind ViewModel.GithubCopilotStatus, Mode=OneWay}"
+                                        IsEnabled="{x:Bind ViewModel.GithubCopilotAllowed}"
+                                        Style="{StaticResource ExpanderSettingContainerStyle}">
                     <StackPanel>
                         <Grid Visibility="{x:Bind ViewModel.AreGithubCopilotTokensSet, Mode=OneWay}">
                             <Grid.ColumnDefinitions>
@@ -137,6 +138,8 @@
                 </local:SettingContainer>
                 <!--  Azure OpenAI  -->
                 <local:SettingContainer x:Uid="AISettings_AzureOpenAIKeyAndEndpoint"
+                                        CurrentValue="{x:Bind ViewModel.AzureOpenAIStatus, Mode=OneWay}"
+                                        IsEnabled="{x:Bind ViewModel.AzureOpenAIAllowed}"
                                         Style="{StaticResource ExpanderSettingContainerStyle}">
                     <StackPanel>
                         <Grid Visibility="{x:Bind ViewModel.AreAzureOpenAIKeyAndEndpointSet, Mode=OneWay}">
@@ -261,6 +264,8 @@
                 </local:SettingContainer>
                 <!--  OpenAI  -->
                 <local:SettingContainer x:Uid="AISettings_OpenAIKey"
+                                        CurrentValue="{x:Bind ViewModel.OpenAIStatus, Mode=OneWay}"
+                                        IsEnabled="{x:Bind ViewModel.OpenAIAllowed}"
                                         Style="{StaticResource ExpanderSettingContainerStyle}">
                     <StackPanel>
                         <Grid Visibility="{x:Bind ViewModel.IsOpenAIKeySet, Mode=OneWay}">
diff --git a/src/cascadia/TerminalSettingsEditor/AISettingsViewModel.cpp b/src/cascadia/TerminalSettingsEditor/AISettingsViewModel.cpp
index 85ac83dc138..9a7df4b771c 100644
--- a/src/cascadia/TerminalSettingsEditor/AISettingsViewModel.cpp
+++ b/src/cascadia/TerminalSettingsEditor/AISettingsViewModel.cpp
@@ -17,6 +17,8 @@ using namespace winrt::Windows::UI::Xaml::Controls;
 using namespace winrt::Microsoft::Terminal::Settings::Model;
 using namespace winrt::Windows::Foundation::Collections;
 
+static constexpr std::wstring_view lockGlyph{ L"\uE72E" };
+
 namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
 {
     AISettingsViewModel::AISettingsViewModel(Model::CascadiaSettings settings) :
@@ -38,7 +40,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
     void AISettingsViewModel::AzureOpenAIEndpoint(winrt::hstring endpoint)
     {
         _Settings.GlobalSettings().AIInfo().AzureOpenAIEndpoint(endpoint);
-        _NotifyChanges(L"AreAzureOpenAIKeyAndEndpointSet");
+        _NotifyChanges(L"AreAzureOpenAIKeyAndEndpointSet", L"AzureOpenAIStatus");
     }
 
     winrt::hstring AISettingsViewModel::AzureOpenAIKey()
@@ -49,7 +51,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
     void AISettingsViewModel::AzureOpenAIKey(winrt::hstring key)
     {
         _Settings.GlobalSettings().AIInfo().AzureOpenAIKey(key);
-        _NotifyChanges(L"AreAzureOpenAIKeyAndEndpointSet");
+        _NotifyChanges(L"AreAzureOpenAIKeyAndEndpointSet", L"AzureOpenAIStatus");
+    }
+
+    bool AISettingsViewModel::AzureOpenAIAllowed() const noexcept
+    {
+        return WI_IsFlagSet(AIConfig::AllowedLMProviders(), EnabledLMProviders::AzureOpenAI);
+    }
+
+    winrt::hstring AISettingsViewModel::AzureOpenAIStatus()
+    {
+        return _getStatusHelper(Model::LLMProvider::AzureOpenAI);
     }
 
     bool AISettingsViewModel::IsOpenAIKeySet()
@@ -65,7 +77,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
     void AISettingsViewModel::OpenAIKey(winrt::hstring key)
     {
         _Settings.GlobalSettings().AIInfo().OpenAIKey(key);
-        _NotifyChanges(L"IsOpenAIKeySet");
+        _NotifyChanges(L"IsOpenAIKeySet", L"OpenAIStatus");
+    }
+
+    bool AISettingsViewModel::OpenAIAllowed() const noexcept
+    {
+        return WI_IsFlagSet(AIConfig::AllowedLMProviders(), EnabledLMProviders::OpenAI);
+    }
+
+    winrt::hstring AISettingsViewModel::OpenAIStatus()
+    {
+        return _getStatusHelper(Model::LLMProvider::OpenAI);
     }
 
     bool AISettingsViewModel::AzureOpenAIActive()
@@ -78,8 +100,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         if (active)
         {
             _Settings.GlobalSettings().AIInfo().ActiveProvider(Model::LLMProvider::AzureOpenAI);
-            _NotifyChanges(L"AzureOpenAIActive", L"OpenAIActive", L"GithubCopilotActive");
         }
+        else
+        {
+            _Settings.GlobalSettings().AIInfo().ActiveProvider(Model::LLMProvider::None);
+        }
+        _NotifyChanges(L"AzureOpenAIActive", L"OpenAIActive", L"GithubCopilotActive", L"AzureOpenAIStatus", L"OpenAIStatus", L"GithubCopilotStatus");
     }
 
     bool AISettingsViewModel::OpenAIActive()
@@ -92,8 +118,12 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         if (active)
         {
             _Settings.GlobalSettings().AIInfo().ActiveProvider(Model::LLMProvider::OpenAI);
-            _NotifyChanges(L"AzureOpenAIActive", L"OpenAIActive", L"GithubCopilotActive");
         }
+        else
+        {
+            _Settings.GlobalSettings().AIInfo().ActiveProvider(Model::LLMProvider::None);
+        }
+        _NotifyChanges(L"AzureOpenAIActive", L"OpenAIActive", L"GithubCopilotActive", L"AzureOpenAIStatus", L"OpenAIStatus", L"GithubCopilotStatus");
     }
 
     bool AISettingsViewModel::AreGithubCopilotTokensSet()
@@ -109,7 +139,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
     void AISettingsViewModel::GithubCopilotAuthValues(winrt::hstring authValues)
     {
         _Settings.GlobalSettings().AIInfo().GithubCopilotAuthValues(authValues);
-        _NotifyChanges(L"AreGithubCopilotTokensSet");
+        _NotifyChanges(L"AreGithubCopilotTokensSet", L"GithubCopilotStatus");
     }
 
     bool AISettingsViewModel::GithubCopilotActive()
@@ -122,13 +152,22 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         if (active)
         {
             _Settings.GlobalSettings().AIInfo().ActiveProvider(Model::LLMProvider::GithubCopilot);
-            _NotifyChanges(L"AzureOpenAIActive", L"OpenAIActive", L"GithubCopilotActive");
         }
+        else
+        {
+            _Settings.GlobalSettings().AIInfo().ActiveProvider(Model::LLMProvider::None);
+        }
+        _NotifyChanges(L"AzureOpenAIActive", L"OpenAIActive", L"GithubCopilotActive", L"AzureOpenAIStatus", L"OpenAIStatus", L"GithubCopilotStatus");
     }
 
-    bool AISettingsViewModel::GithubCopilotFeatureEnabled()
+    bool AISettingsViewModel::GithubCopilotAllowed() const noexcept
     {
-        return Feature_GithubCopilot::IsEnabled();
+        return Feature_GithubCopilot::IsEnabled() && WI_IsFlagSet(AIConfig::AllowedLMProviders(), EnabledLMProviders::GithubCopilot);
+    }
+
+    winrt::hstring AISettingsViewModel::GithubCopilotStatus()
+    {
+        return _getStatusHelper(Model::LLMProvider::GithubCopilot);
     }
 
     bool AISettingsViewModel::IsTerminalPackaged()
@@ -152,6 +191,55 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
     void AISettingsViewModel::_OnGithubAuthCompleted(const winrt::hstring& message)
     {
         _githubCopilotAuthMessage = message;
-        _NotifyChanges(L"AreGithubCopilotTokensSet", L"GithubCopilotAuthMessage");
+        _NotifyChanges(L"AreGithubCopilotTokensSet", L"GithubCopilotAuthMessage", L"GithubCopilotStatus");
+    }
+
+    winrt::hstring AISettingsViewModel::_getStatusHelper(const Model::LLMProvider provider)
+    {
+        bool allowed;
+        bool active;
+        bool loggedIn;
+        switch (provider)
+        {
+        case LLMProvider::AzureOpenAI:
+            allowed = AzureOpenAIAllowed();
+            active = AzureOpenAIActive();
+            loggedIn = AreAzureOpenAIKeyAndEndpointSet();
+            break;
+        case LLMProvider::OpenAI:
+            allowed = OpenAIAllowed();
+            active = OpenAIActive();
+            loggedIn = IsOpenAIKeySet();
+            break;
+        case LLMProvider::GithubCopilot:
+            allowed = GithubCopilotAllowed();
+            active = GithubCopilotActive();
+            loggedIn = AreGithubCopilotTokensSet();
+            break;
+        default:
+            return L"";
+        }
+
+        if (!allowed)
+        {
+            // not allowed, display the lock glyph
+            return winrt::hstring{ lockGlyph } + L" " + RS_(L"AISettings_ProviderNotAllowed");
+        }
+        else if (active && loggedIn)
+        {
+            // active provider and logged in
+            return RS_(L"AISettings_ActiveLoggedIn");
+        }
+        else if (active)
+        {
+            // active provider, not logged in
+            return RS_(L"AISettings_Active");
+        }
+        else if (loggedIn)
+        {
+            // logged in, not active provider
+            return RS_(L"AISettings_LoggedIn");
+        }
+        return L"";
     }
 }
diff --git a/src/cascadia/TerminalSettingsEditor/AISettingsViewModel.h b/src/cascadia/TerminalSettingsEditor/AISettingsViewModel.h
index 41e8e7379b5..a0822dc65a3 100644
--- a/src/cascadia/TerminalSettingsEditor/AISettingsViewModel.h
+++ b/src/cascadia/TerminalSettingsEditor/AISettingsViewModel.h
@@ -24,27 +24,34 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         void AzureOpenAIKey(winrt::hstring key);
         bool AzureOpenAIActive();
         void AzureOpenAIActive(bool active);
+        bool AzureOpenAIAllowed() const noexcept;
+        winrt::hstring AzureOpenAIStatus();
 
         bool IsOpenAIKeySet();
         winrt::hstring OpenAIKey();
         void OpenAIKey(winrt::hstring key);
         bool OpenAIActive();
         void OpenAIActive(bool active);
+        bool OpenAIAllowed() const noexcept;
+        winrt::hstring OpenAIStatus();
 
         bool AreGithubCopilotTokensSet();
         winrt::hstring GithubCopilotAuthMessage();
         void GithubCopilotAuthValues(winrt::hstring authValues);
         bool GithubCopilotActive();
         void GithubCopilotActive(bool active);
-        bool GithubCopilotFeatureEnabled();
+        bool GithubCopilotAllowed() const noexcept;
         bool IsTerminalPackaged();
         void InitiateGithubAuth_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& e);
         til::typed_event<Windows::Foundation::IInspectable, Windows::Foundation::IInspectable> GithubAuthRequested;
+        winrt::hstring GithubCopilotStatus();
 
     private:
         Model::CascadiaSettings _Settings;
         winrt::hstring _githubCopilotAuthMessage;
 
+        winrt::hstring _getStatusHelper(const winrt::Microsoft::Terminal::Settings::Model::LLMProvider provider);
+
         winrt::Microsoft::Terminal::Settings::Editor::MainPage::GithubAuthCompleted_revoker _githubAuthCompleteRevoker;
 
         void _OnGithubAuthCompleted(const winrt::hstring& message);
diff --git a/src/cascadia/TerminalSettingsEditor/AISettingsViewModel.idl b/src/cascadia/TerminalSettingsEditor/AISettingsViewModel.idl
index 6a31438ae84..3844b549800 100644
--- a/src/cascadia/TerminalSettingsEditor/AISettingsViewModel.idl
+++ b/src/cascadia/TerminalSettingsEditor/AISettingsViewModel.idl
@@ -15,16 +15,21 @@ namespace Microsoft.Terminal.Settings.Editor
         String AzureOpenAIEndpoint;
         String AzureOpenAIKey;
         Boolean AzureOpenAIActive;
+        Boolean AzureOpenAIAllowed { get; };
+        String AzureOpenAIStatus { get; };
 
         Boolean IsOpenAIKeySet { get; };
         String OpenAIKey;
         Boolean OpenAIActive;
+        Boolean OpenAIAllowed { get; };
+        String OpenAIStatus { get; };
 
         Boolean AreGithubCopilotTokensSet { get; };
         String GithubCopilotAuthMessage { get; };
         void GithubCopilotAuthValues(String authValues);
         Boolean GithubCopilotActive;
-        Boolean GithubCopilotFeatureEnabled { get; };
+        Boolean GithubCopilotAllowed { get; };
+        String GithubCopilotStatus { get; };
         Boolean IsTerminalPackaged { get; };
 
         void InitiateGithubAuth_Click(IInspectable sender, Windows.UI.Xaml.RoutedEventArgs args);
diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
index 61c71d8a176..1eb01d86b1e 100644
--- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
@@ -792,6 +792,22 @@
     <value>Awaiting authentication completion from browser...</value>
     <comment>Text displayed after the user clicks the button to initiate the GitHub authentication flow in their browser.</comment>
   </data>
+  <data name="AISettings_Active" xml:space="preserve">
+    <value>Active</value>
+    <comment>Text displayed when the service provider is active.</comment>
+  </data>
+  <data name="AISettings_LoggedIn" xml:space="preserve">
+    <value>Logged in</value>
+    <comment>Text displayed when the user is logged in to the service provider.</comment>
+  </data>
+  <data name="AISettings_ActiveLoggedIn" xml:space="preserve">
+    <value>Active, Logged in</value>
+    <comment>Text displayed when the user is logged in to the service provider and that service provider is active.</comment>
+  </data>
+  <data name="AISettings_ProviderNotAllowed" xml:space="preserve">
+    <value>Disabled by your administrator</value>
+    <comment>Text displayed when the service provider has been disabled by the administrator.</comment>
+  </data>
   <data name="AISettings_PortableModeDisclaimer.Text" xml:space="preserve">
     <value>Unable to authenticate to GitHub in unpackaged mode. Please launch Terminal as a packaged application to authenticate.</value>
     <comment>Text displayed to the user when Terminal is un unpackaged mode.</comment>
@@ -2109,4 +2125,4 @@
     <value>Display a shield in the title bar when Windows Terminal is running as Administrator</value>
     <comment>Header for a control to toggle displaying a shield in the title bar of the app. "Admin" refers to elevated sessions like "run as Admin"</comment>
   </data>
-</root>
\ No newline at end of file
+</root>
diff --git a/src/cascadia/TerminalSettingsModel/AIConfig.cpp b/src/cascadia/TerminalSettingsModel/AIConfig.cpp
index 7af8678a3b4..562592b05d0 100644
--- a/src/cascadia/TerminalSettingsModel/AIConfig.cpp
+++ b/src/cascadia/TerminalSettingsModel/AIConfig.cpp
@@ -19,6 +19,45 @@ static constexpr wil::zwstring_view PasswordVaultAIEndpoint = L"TerminalAIEndpoi
 static constexpr wil::zwstring_view PasswordVaultOpenAIKey = L"TerminalOpenAIKey";
 static constexpr wil::zwstring_view PasswordVaultGithubCopilotAuthValues = L"TerminalGithubCopilotAuthValues";
 
+// When new LM providers are added here, make sure you also update the admx/adml!
+static constexpr wil::zwstring_view AzureOpenAIPolicyKey = L"AzureOpenAI";
+static constexpr wil::zwstring_view OpenAIPolicyKey = L"OpenAI";
+static constexpr wil::zwstring_view GitHubCopilotPolicyKey = L"GitHubCopilot";
+
+winrt::Microsoft::Terminal::Settings::Model::EnabledLMProviders AIConfig::AllowedLMProviders() noexcept
+{
+    Model::EnabledLMProviders enabledLMProviders{ Model::EnabledLMProviders::All };
+    // get our allowed list of LM providers from the registry
+    for (const auto key : { HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER })
+    {
+        wchar_t buffer[512]; // "640K ought to be enough for anyone"
+        DWORD bufferSize = sizeof(buffer);
+        if (RegGetValueW(key, LR"(Software\Policies\Microsoft\Windows Terminal)", L"EnabledLMProviders", RRF_RT_REG_MULTI_SZ, nullptr, buffer, &bufferSize) == 0)
+        {
+            WI_ClearAllFlags(enabledLMProviders, Model::EnabledLMProviders::All);
+            for (auto p = buffer; *p;)
+            {
+                const std::wstring_view value{ p };
+                if (value == AzureOpenAIPolicyKey)
+                {
+                    WI_SetFlag(enabledLMProviders, Model::EnabledLMProviders::AzureOpenAI);
+                }
+                else if (value == OpenAIPolicyKey)
+                {
+                    WI_SetFlag(enabledLMProviders, Model::EnabledLMProviders::OpenAI);
+                }
+                else if (value == GitHubCopilotPolicyKey)
+                {
+                    WI_SetFlag(enabledLMProviders, Model::EnabledLMProviders::GithubCopilot);
+                }
+                p += value.size() + 1;
+            }
+            break;
+        }
+    }
+    return enabledLMProviders;
+}
+
 winrt::com_ptr<AIConfig> AIConfig::CopyAIConfig(const AIConfig* source)
 {
     auto aiConfig{ winrt::make_self<AIConfig>() };
@@ -108,16 +147,36 @@ winrt::hstring AIConfig::GithubCopilotAuthValues()
 
 winrt::Microsoft::Terminal::Settings::Model::LLMProvider AIConfig::ActiveProvider()
 {
+    const auto allowedLMProviders = AllowedLMProviders();
     const auto val{ _getActiveProviderImpl() };
     if (val)
     {
-        // an active provider was explicitly set, return that
-        // special case: only allow github copilot if the feature is enabled
-        if (*val == LLMProvider::GithubCopilot && !Feature_GithubCopilot::IsEnabled())
+        const auto setProvider = *val;
+        // an active provider was explicitly set, return that as long as it is allowed
+        switch (setProvider)
         {
-            return LLMProvider{};
+        case LLMProvider::GithubCopilot:
+            if (Feature_GithubCopilot::IsEnabled() && WI_IsFlagSet(allowedLMProviders, EnabledLMProviders::GithubCopilot))
+            {
+                return setProvider;
+            }
+            break;
+        case LLMProvider::AzureOpenAI:
+            if (WI_IsFlagSet(allowedLMProviders, EnabledLMProviders::AzureOpenAI))
+            {
+                return setProvider;
+            }
+            break;
+        case LLMProvider::OpenAI:
+            if (WI_IsFlagSet(allowedLMProviders, EnabledLMProviders::OpenAI))
+            {
+                return setProvider;
+            }
+            break;
+        default:
+            break;
         }
-        return *val;
+        return LLMProvider{};
     }
     else if (!AzureOpenAIEndpoint().empty() && !AzureOpenAIKey().empty())
     {
diff --git a/src/cascadia/TerminalSettingsModel/AIConfig.h b/src/cascadia/TerminalSettingsModel/AIConfig.h
index 5bcf5868af4..5d1cf9e1767 100644
--- a/src/cascadia/TerminalSettingsModel/AIConfig.h
+++ b/src/cascadia/TerminalSettingsModel/AIConfig.h
@@ -33,6 +33,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         Json::Value ToJson() const;
         void LayerJson(const Json::Value& json);
 
+        static Model::EnabledLMProviders AllowedLMProviders() noexcept;
+
         // Key and endpoint storage
         // These are not written to the json, they are stored in the Windows Security Storage Vault
         winrt::hstring AzureOpenAIEndpoint() noexcept;
@@ -58,6 +60,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         _BASE_INHERITABLE_SETTING(Model::AIConfig, std::optional<LLMProvider>, ActiveProvider);
 
     private:
+        Model::EnabledLMProviders _enabledLMProviders{ Model::EnabledLMProviders::All };
         winrt::hstring _RetrieveCredential(const wil::zwstring_view credential);
         void _SetCredential(const wil::zwstring_view credential, const winrt::hstring& value);
         std::unordered_map<std::wstring, std::wstring> _credentialCache;
diff --git a/src/cascadia/TerminalSettingsModel/AIConfig.idl b/src/cascadia/TerminalSettingsModel/AIConfig.idl
index e3f15435591..f4ee727eece 100644
--- a/src/cascadia/TerminalSettingsModel/AIConfig.idl
+++ b/src/cascadia/TerminalSettingsModel/AIConfig.idl
@@ -7,17 +7,28 @@ namespace Microsoft.Terminal.Settings.Model
 {
     enum LLMProvider
     {
+        None,
         AzureOpenAI,
         OpenAI,
         GithubCopilot
     };
 
+    [flags] enum EnabledLMProviders
+    {
+        AzureOpenAI = 0x1,
+        OpenAI = 0x2,
+        GithubCopilot = 0x4,
+        All = 0xffffffff
+    };
+
     delegate void AzureOpenAISettingChangedHandler();
     delegate void OpenAISettingChangedHandler();
 
     [default_interface] runtimeclass AIConfig {
         INHERITABLE_SETTING(LLMProvider, ActiveProvider);
 
+        static EnabledLMProviders AllowedLMProviders { get; };
+
         String AzureOpenAIEndpoint;
         String AzureOpenAIKey;
         static event AzureOpenAISettingChangedHandler AzureOpenAISettingChanged;
diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp
index bebe9ac76bd..4a212aa4edd 100644
--- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp
@@ -348,11 +348,18 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
     // - Actions of the parents are overridden by the children
     void ActionMap::_PopulateCumulativeActionMap(std::unordered_map<hstring, Model::Command>& actionMap)
     {
+        // special case: don't add any ToggleAIChat actions if the feature has been disabled
+        // note: we only refuse to add these actions to the cumulative action map, that way we don't remove them from the user's settings file
+        // this means that these actions won't show up in the command palette, but any keybindings/modifications/etc to them will persist
+        const auto terminalChatDisabled = !WI_IsAnyFlagSet(AIConfig::AllowedLMProviders(), EnabledLMProviders::All);
         for (const auto& [cmdID, cmd] : _ActionMap)
         {
             if (!actionMap.contains(cmdID))
             {
-                actionMap.emplace(cmdID, cmd);
+                if (!terminalChatDisabled || cmd.ActionAndArgs().Action() != ShortcutAction::ToggleAIChat)
+                {
+                    actionMap.emplace(cmdID, cmd);
+                }
             }
         }
 


Test patch: