Add SSH folder to NTM for dynamic SSH profiles (#19239)

Automatically generates an "SSH" folder in the new tab menu that
contains all profiles generated by the SSH profile generator. This
folder is created if the SSH generator created some profiles and the
folder hasn't been created before. Detecting if the folder was generated
is done via the new `bool ApplicationState::SSHFolderGenerated`. The
logic is similar to `SettingsLoader::DisableDeletedProfiles()`.

Found a bug on new tab menu's folder inlining feature where we were
counting the number of raw entries to determine whether to inline or
not. Since the folder only contained the match profiles entry, this bug
made it so that the profile entries would always be inlined. The fix was
very simple: count the number of _resolved_ entries instead of the raw
entries. This can be pulled into its own PR and serviced, if desired.

## References and Relevant Issues
#18814 
#14042 

## Validation Steps Performed
 Existing users get an SSH folder if profiles were generated

## PR Checklist
Closes #19043
This commit is contained in:
Carlos Zamora 2025-08-13 15:43:27 -07:00 committed by GitHub
parent abaa9488d9
commit 1b2aad6504
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 37 additions and 3 deletions

View File

@ -1069,7 +1069,7 @@ namespace winrt::TerminalApp::implementation
auto folderEntryItems = _CreateNewTabFlyoutItems(folderEntries);
// If the folder should auto-inline and there is only one item, do so.
if (folderEntry.Inlining() == FolderEntryInlining::Auto && folderEntries.Size() == 1)
if (folderEntry.Inlining() == FolderEntryInlining::Auto && folderEntryItems.size() == 1)
{
for (auto const& folderEntryItem : folderEntryItems)
{

View File

@ -41,7 +41,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
X(FileSource::Shared, Windows::Foundation::Collections::IVector<hstring>, RecentCommands, "recentCommands") \
X(FileSource::Shared, Windows::Foundation::Collections::IVector<winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage>, DismissedMessages, "dismissedMessages") \
X(FileSource::Local, Windows::Foundation::Collections::IVector<hstring>, AllowedCommandlines, "allowedCommandlines") \
X(FileSource::Local, std::unordered_set<hstring>, DismissedBadges, "dismissedBadges")
X(FileSource::Local, std::unordered_set<hstring>, DismissedBadges, "dismissedBadges") \
X(FileSource::Shared, bool, SSHFolderGenerated, "sshFolderGenerated", false)
struct WindowLayout : WindowLayoutT<WindowLayout>
{

View File

@ -93,6 +93,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void MergeFragmentIntoUserSettings(const winrt::hstring& source, const winrt::hstring& basePath, const std::string_view& content);
void FinalizeLayering();
bool DisableDeletedProfiles();
bool AddDynamicProfileFolders();
bool RemapColorSchemeForProfile(const winrt::com_ptr<winrt::Microsoft::Terminal::Settings::Model::implementation::Profile>& profile);
bool FixupUserSettings();
@ -100,6 +101,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
ParsedSettings userSettings;
std::unordered_map<hstring, winrt::com_ptr<implementation::ExtensionPackage>> extensionPackageMap;
bool duplicateProfile = false;
bool sshProfilesGenerated = false;
private:
struct JsonSettings

View File

@ -213,8 +213,11 @@ void SettingsLoader::GenerateProfiles()
auto generateProfiles = [&](const IDynamicProfileGenerator& generator) {
if (!_ignoredNamespaces.contains(generator.GetNamespace()))
{
const auto oldProfileCount = inboxSettings.profiles.size();
_executeGenerator(generator, inboxSettings.profiles);
return oldProfileCount != inboxSettings.profiles.size();
}
return false;
};
// Generate profiles for each generator and add them to the inbox settings.
@ -224,7 +227,7 @@ void SettingsLoader::GenerateProfiles()
generateProfiles(AzureCloudShellGenerator{});
generateProfiles(VisualStudioGenerator{});
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
generateProfiles(SshHostGenerator{});
sshProfilesGenerated = generateProfiles(SshHostGenerator{});
#endif
}
@ -536,6 +539,33 @@ bool SettingsLoader::DisableDeletedProfiles()
return newGeneratedProfiles;
}
// Returns true if something got changed and
// the settings need to be saved to disk.
bool SettingsLoader::AddDynamicProfileFolders()
{
// Keep track of generated folders to avoid regenerating them
const auto state = get_self<ApplicationState>(ApplicationState::SharedInstance());
// If the SSH generator is enabled, try to create an "SSH" folder with all the generated profiles
if (sshProfilesGenerated && !state->SSHFolderGenerated())
{
SshHostGenerator sshGenerator;
auto matchProfilesEntry = make_self<implementation::MatchProfilesEntry>();
matchProfilesEntry->Source(hstring{ sshGenerator.GetNamespace() });
auto folderEntry = make_self<implementation::FolderEntry>();
folderEntry->Name(L"SSH");
folderEntry->Icon(MediaResource::FromString(hstring{ sshGenerator.GetIcon() }));
folderEntry->Inlining(FolderEntryInlining::Auto);
folderEntry->RawEntries(winrt::single_threaded_vector<Model::NewTabMenuEntry>({ *matchProfilesEntry }));
userSettings.globals->NewTabMenu().Append(folderEntry.as<Model::NewTabMenuEntry>());
state->SSHFolderGenerated(true);
return true;
}
return false;
}
bool winrt::Microsoft::Terminal::Settings::Model::implementation::SettingsLoader::RemapColorSchemeForProfile(const winrt::com_ptr<winrt::Microsoft::Terminal::Settings::Model::implementation::Profile>& profile)
{
bool modified{ false };
@ -1229,6 +1259,7 @@ try
// DisableDeletedProfiles returns true whenever we encountered any new generated/dynamic profiles.
// Similarly FixupUserSettings returns true, when it encountered settings that were patched up.
mustWriteToDisk |= loader.DisableDeletedProfiles();
mustWriteToDisk |= loader.AddDynamicProfileFolders();
mustWriteToDisk |= loader.FixupUserSettings();
// If this throws, the app will catch it and use the default settings.