Repository: microsoft/terminal (language: cpp)

Issue:
Allow dropdown menu customization in profiles.json
> We'll probably need a settings redesign that allows you to specify the contents of the menu instead of adding a new profile type.

---

### original content

#### Summary of the new feature/enhancement
Allow `MenuFlyoutSeparator `to be programmed in profiles.json
<!-- 
A clear and concise description of what the problem is that the new feature would solve.
Describe why and how a user would use this new functionality (if applicable).
-->

#### Proposed technical implementation details (optional)
My thoughts are to have a flag that allows a "profile" in the profiles.json to be interpreted as a `MenuFlyoutSeparator `in `App::_CreateNewTabFlyout()`
<!-- 
A clear and concise description of what you want to happen.
-->



Discussion / hints:
We'll probably need a settings redesign that allows you to specify the contents of the menu instead of adding a new profile type.

I'm just gonna mark this as out of scope for 1.0. :smile:
(directly including the request from #2330 here cause it's a great idea)

> 
> Right now I have 5 profiles I use. I just played a bit with adding a ssh-profile and it works fine. But when I want to add all the profiles I have in Putty in WT I need to add another 20+ ssh-profiles. This means the list is going to be really long.
> 
> So being able to make submenus and put my profiles in a multiple submenus would be much welcomed.
> 
> Initially the menu looks like this:
> 
> ```
> PowerShell
> CMD
> WSL
> SSH-Group1 > 
> SSH-Group2 > 
> ```
> 
> When a submenu is selected, something like this
> 
> ```
> PowerShell
> CMD
> WSL
> SSH-Group1 >
> SSH-Group2 > +--------+
>              | Host 1 |
>              | Host 2 |
>              | Host 3 |
>              +--------+
> ```


I was just about to create a new ticket when I stumbled upon this one. I wish to add "mee too" on this - it's very important to a subgroup of the Terminal power-users - namely the administrators. Currently we are using MobaXTerm which does a wonderful job at storing 100s of distinct host entries (profiles) each with their own access and communication parameters. On MacOS we have a toolbar menu and on Linux simply a very large aliases file. In Terminal, I'd love to see this change:

Instead of doing like this:
```json
"profiles":
    [
        {
            "guid": "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee0}",
            "hidden": false,
            "name": "Company 1, Server X",
            "commandline": "ssh root@serverX.company1.com",
        },
        {
            "guid": "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee1}",
            "hidden": false,
            "name": "Company 1, Server Y",
            "commandline": "ssh root@serverY.company1.com",
        },
        {
            "guid": "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee2}",
            "hidden": false,
            "name": "Company 2, Server X",
            "commandline": "ssh root@serverX.company2.com",
        },
        {
            "guid": "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee3}",
            "hidden": false,
            "name": "Company 2, Server Y",
            "commandline": "ssh root@serverY.company2.com",
        },
        {
            "guid": "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee4}",
            "hidden": false,
            "name": "PowerShell 7 Preview",
            "source": "Windows.Terminal.PowershellCore"
        }
    ],
```

I would rather like to do (something like) this:
```json
    "profiles":
    [
        {
            "folder": "Company 1",
            "items": [
                {
                    "guid": "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee0}",
                    "hidden": false,
                    "name": "Server X",
                    "commandline": "ssh root@serverX.company1.com",
                },
                {
                    "guid": "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee1}",
                    "hidden": false,
                    "name": "Server Y",
                    "commandline": "ssh root@serverY.company1.com",
                }
            ]
        },
        {
            "folder": "Company 2",
            "items": [
                {
                    "guid": "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee2}",
                    "hidden": false,
                    "name": "Server X",
                    "commandline": "ssh root@serverX.company2.com",
                },
                {
                    "guid": "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee3}",
                    "hidden": false,
                    "name": "Server Y",
                    "commandline": "ssh root@serverY.company2.com",
                }
            ]
        },
        {
            "guid": "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeee4}",
            "hidden": false,
            "name": "PowerShell 7 Preview",
            "source": "Windows.Terminal.PowershellCore"
        }
    ],
````

Which would help very much in organizing and accessing a configuration with tens or hundreds of defined servers and several companies/categories of servers.
I would propose something like this

```JSON
{
    "profiles": {
        "defaults": {},
        "list": [
            {
                "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}",
                "name": "Windows PowerShell",
                "commandline": "powershell.exe",
                "hidden": false
            },
            {
                "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
                "name": "Command Prompt",
                "commandline": "cmd.exe",
                "hidden": false
            },
            {
                "guid": "{574e775e-4f2a-5b96-ac1e-a2962a402336}",
                "hidden": false,
                "name": "PowerShell",
                "source": "Windows.Terminal.PowershellCore"
            },
            {
                "guid": "{58ad8b0c-3ef8-5f4d-bc6f-13e4c00f2530}",
                "hidden": false,
                "name": "Debian",
                "source": "Windows.Terminal.Wsl"
            },
            {
                "name": "Company Project 1",
                "hidden": false,
                "defaults": {
                    // Put settings here that you want to apply to all profiles in 'Company Project 1'.
                    // Proposal - inherit from parent "defaults"
                },
                "list": [
                    {
                        "guid": "{a3a2e83a-884a-5379-baa8-16f193a13b21}",
                        "hidden": false,
                        "name": "PowerShell 7 Preview",
                        "source": "Windows.Terminal.PowershellCore"
                    },
                    {
                        "commandline": "nu.exe",
                        "guid": "{2b372ca1-1ee2-403d-a839-6d63077ad871}",
                        "hidden": false,
                        "name": "Nu Shell",
                    },
                    {
                        "commandline": "elvish.exe",
                        "guid": "{6ddf0352-a8c2-46c3-9734-0034762b954c}",
                        "hidden": false,
                        "name": "Elvish"
                    }
                ]
            }
        ]
    }
}
```

By doing so each group/folder will pretty much be like "profiles" section containing defaults (which will be inherited hierarchically) and "list" of profiles.

We can have groups inside of groups.
Forked repo, implementing it in my fork
Partial (working) implementation exists [in this branch](/tiksn/terminal/tree/feature/dropdown-customization). Branch is bit lagging from master.
I'll add another 'me too' for this one - and a twist... I'd like to be able to reference one or more external files for my 'profiles.list' section.  My need is similar to several examples above, but I'd like to be able to share 'customer' folders with other users without having to share out my entire configuration (and, since I have the information I need in a database already, I'd like to generate this JSON from there instead of having to edit it manually)
Hello there. I've been searching for the possibility to save my panes in a profile and ended up with the following:
```json
{
    "command": {
        "action": "wt",
        "commandline": "wsl.exe ~ ; sp -H ssh bruno@10.254.0.254; mf up; sp -V wsl.exe ~ ; sp -V; mf down; sp -H; mf up; sp -V ssh bruno@10.254.0.254; mf down; sp -V; sp -V; sp -H"
    },
    "keys": "ctrl+shift+w",
    "name": "Work Layout"
}
```

There are nine panes in there which is quite complex, and even though this works fine as a shortcut, I still need to close the original tab since this opens a new one and there is no `resizePane` subcommand for `wt`.
I was wondering when we're going to be able to test the proposed profile structure in a preview build and if there's going to be a solution for programmatically resizing panes.
Thanks in advance, love your open-source work!
LOL I just found out that there's an argument for `splitPane` which resizes it.
> I was wondering when we're going to be able to test the proposed profile structure in a preview build

Probably not for a long time. One of the key features we want is for people to be able to nest profile entries, but there's currently a bug in Xaml Islands that causes the whole application to crash when you use nested menuflyouts. (see #8238 for details) We'll need to move over to WinUI 3 to get the fix for that, which will take time and engineering effort that we're not prepared to commit to before polishing up and finishing 2.0. 
> > I was wondering when we're going to be able to test the proposed profile structure in a preview build
> 
> Probably not for a long time. One of the key features we want is for people to be able to nest profile entries, but there's currently a bug in Xaml Islands that causes the whole application to crash when you use nested menuflyouts. (see #8238 for details) We'll need to move over to WinUI 3 to get the fix for that, which will take time and engineering effort that we're not prepared to commit to before polishing up and finishing 2.0.

Could this feature perhaps be MVP'd in a simpler way? For my personal use case I would already be mostly unblocked if I could change the single existing menu entry to not open my default profile since my default profile is my wsl distro and I would not want to use this on the windows filesystem. If we could set a default for terminal open and a separate default for the explorer context menu (powershell/powershell core) I think a lot of use cases are already covered.

Holding off on this until it can be fully implemented for power users with submenu's and the like would be a shame imo.
@rouke-broersma You know, the part of this that you're looking for is actually better tracked over at #6111. 

In other news, I think the OS-side of this bug should be getting serviced to earlier Windows versions sometime this month, so hopefully we can get a start on this sometime early next year. 
@zadjii-msft I got here from there! Because the first note in that issue links to this I was assuming that changing the context menu was depending on being able to configure the dropdown, and since this is on-hold due to some OS bug I thought the 'main' issue was also waiting on that bug. That's why I thought I would ask for a more MVP solution to the problem. But from your reaction I gather that it was the wrong assumption :) 
Ah, yea, so the two are a little intertwined. There's a bit of architecture we want to do beneath _both_ issues, which is described over at [#6899 - Action IDs.md](https://github.com/microsoft/terminal/blob/main/doc/specs/%236899%20-%20Action%20IDs/%236899%20-%20Action%20IDs.md). I suppose #6899 is the common parent to both these features ☺️ 
When this feature can be implemented? I am having 20+ profiles and list is continuously growing, I am desperately waiting a solution for this.

Thanks! Kevin
I'd love to get to work on this one, but unfortunately there are some higher priorities at the moment. We're in a bugfix push right now. After that I'm hoping to get back to tear out (#1256). Once that's made an appreciable level of progress then I'm planning on getting to #6899 and this. So it's pretty high up on the backlog, but still a few months away at least. 

If someone in the community is interested in working on this sooner than that I can try and give them some pointers. Thinking about it more, all the non-actions bits of [New Tab Menu Customization](https://github.com/microsoft/terminal/blob/main/doc/specs/%231571%20-%20New%20Tab%20Menu%20Customization/%231571%20-%20New%20Tab%20Menu%20Customization.md) are probably doable without #6899. So that might be even easier to start with.
Hello to all. I hope everything is fine.

Do you know when this feature will be available/implemented? I really need it because I've like a thousand profiles.
If I can help with anything.

Thank you very much.
I don't believe that anyone on the team will have the bandwidth to get to this any time soon, unfortunately 😕. If anyone would like to help contribute this, I'd be more than happy to give pointers on how I'd go about this. 
Is it possible to manually re-arrange the profiles by editing the JSON file? If so, I'm assuming Windows Terminal would need to be closed first in order to do so?
> Is it possible to manually re-arrange the profiles by editing the JSON file? If so, I'm assuming Windows Terminal would need to be closed first in order to do so?

Yes, that's always been possible. You shouldn't need to close the Terminal either - that should hot-reload
As a follow up to my [previous comment](https://github.com/microsoft/terminal/issues/1571#issuecomment-1049223151):
* [ ] add enum for `NewTabMenuEntryType`
* [ ] add deserialization for enum to `TerminalSettingsSerializationHelpers`
* [ ] add winrt class for `NewTabMenuEntry { NewTabMenuEntryType Type; }`
* [ ] class for `ProfileEntry : NewTabMenuEntry { String Profile; }` (.cpp, .h, .idl)
* [ ] class for `SeparatorEntry : NewTabMenuEntry` (.cpp, .h, .idl)
* [ ] class for `FolderEntry : NewTabMenuEntry { IVector<NewTabMenuEntry> Entries; }` (.cpp, .h, .idl)
* You might be able to get away with all those classes being in one `idl`, but you'd probably want separate files for the `h`'s and `cpp`'s
* [ ] cpp implementations for those `Entry`'s
  - Not sure there's too much to do for those. `WINRT_PROPERTY` should likely take care of most of that
* [ ] `ConversionTrait`'s for entries (a la the one in `ApplicationState.cpp`)
* [ ] a `newTabMenu` property in `GlobalAppSettings` of type `IVector<NewTabMenuEntry>`
* [ ] Build the actual menu in `TerminalPage::_CreateNewTabFlyout` if that property is set (or fall back to the one we currently have if it isn't)
  * [ ] parsing the string names -> profiles might be the hardest part

That would be enough for an initial PR, I'd think

Fix patch:
diff --git a/.github/actions/spelling/allow/names.txt b/.github/actions/spelling/allow/names.txt
index 1c6ef9a373c..311a09750d5 100644
--- a/.github/actions/spelling/allow/names.txt
+++ b/.github/actions/spelling/allow/names.txt
@@ -80,6 +80,7 @@ thereses
 Walisch
 WDX
 Wellons
+Westerman
 Wirt
 Wojciech
 zadjii
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index f9739216ff8..5ed0fa56dc6 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -251,6 +251,7 @@ conattrs
 conbufferout
 concfg
 conclnt
+concretizations
 conddkrefs
 condrv
 conechokey
@@ -2108,6 +2109,7 @@ webpage
 websites
 websockets
 wekyb
+WEOF
 wex
 wextest
 wextestclass
diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json
index e20f8545681..a92ed350d17 100644
--- a/doc/cascadia/profiles.schema.json
+++ b/doc/cascadia/profiles.schema.json
@@ -561,6 +561,160 @@
       },
       "type": "object"
     },
+    "NewTabMenuEntryType": {
+      "enum": [
+        "source",
+        "profile",
+        "folder",
+        "separator",
+        "remainingProfiles",
+        "matchProfiles"
+      ]
+    },
+    "NewTabMenuEntry": {
+      "properties": {
+        "type": {
+          "description": "The type of menu entry",
+          "$ref": "#/$defs/NewTabMenuEntryType"
+        }
+      },
+      "required": [
+        "type"
+      ],
+      "type": "object"
+    },
+    "FolderEntryInlining": {
+      "enum": [
+        "never",
+        "auto"
+      ]
+    },
+    "FolderEntry": {
+      "description": "A folder entry in the new tab dropdown",
+      "allOf": [
+        {
+          "$ref": "#/$defs/NewTabMenuEntry"
+        },
+        {
+          "properties": {
+            "type": {
+              "type": "string",
+              "const": "folder"
+            },
+            "name": {
+              "type": "string",
+              "default": "",
+              "description": "The name of the folder to show in the menu"
+            },
+            "icon": {
+              "$ref": "#/$defs/Icon"
+            },
+            "entries": {
+              "type": "array",
+              "description": "The entries to put inside this folder",
+              "minItems": 1,
+              "items": {
+                "$ref": "#/$defs/NewTabMenuEntry"
+              }
+            },
+            "inline": {
+              "description": "When set to auto and the folder only has a single entry, the entry will show directly and no folder will be rendered",
+              "default": "never",
+              "$ref": "#/$defs/FolderEntryInlining"
+            },
+            "allowEmpty": {
+              "description": "Whether to render a folder without entries, or to hide it",
+              "default": "false",
+              "type": "boolean"
+            }
+          }
+        }
+      ]
+    },
+    "SeparatorEntry": {
+      "description": "A separator in the new tab dropdown",
+      "allOf": [
+        {
+          "$ref": "#/$defs/NewTabMenuEntry"
+        },
+        {
+          "properties": {
+            "type": {
+              "type": "string",
+              "const": "separator"
+            }
+          }
+        }
+      ]
+    },
+    "ProfileEntry": {
+      "description": "A profile in the new tab dropdown",
+      "allOf": [
+        {
+          "$ref": "#/$defs/NewTabMenuEntry"
+        },
+        {
+          "properties": {
+            "type": {
+              "type": "string",
+              "const": "profile"
+            },
+            "profile": {
+              "type": "string",
+              "default": "",
+              "description": "The name or GUID of the profile to show in this entry"
+            }
+          }
+        }
+      ]
+    },
+    "RemainingProfilesEntry": {
+      "description": "The set of profiles that are not yet explicitly included in another entry, such as the profile or source entries. This entry can be used at most one time!",
+      "allOf": [
+        {
+          "$ref": "#/$defs/NewTabMenuEntry"
+        },
+        {
+          "properties": {
+            "type": {
+              "type": "string",
+              "const": "remainingProfiles"
+            }
+          }
+        }
+      ]
+    },
+    "MatchProfilesEntry": {
+      "description": "A set of profiles all matching the given name, source, or command line, to show in the new tab dropdown",
+      "allOf": [
+        {
+          "$ref": "#/$defs/NewTabMenuEntry"
+        },
+        {
+          "properties": {
+            "type": {
+              "type": "string",
+              "const": "matchProfiles"
+            },
+            "name": {
+              "type": "string",
+              "default": "",
+              "description": "The name of the profiles to match"
+            },
+            "source": {
+              "type": "string",
+              "default": "",
+              "description": "The source of the profiles to match"
+            },
+            "commandline": {
+              "type": "string",
+              "default": "",
+              "description": "The command line of the profiles to match"
+            }
+          }
+        }
+      ]
+    },
     "SwitchToAdjacentTabArgs": {
       "oneOf": [
         {
@@ -1568,6 +1722,29 @@
         }
       ]
     },
+    "NewTabMenu": {
+      "description": "Defines the order and structure of the 'new tab' menu. It can consist of e.g. profiles, folders, and separators.",
+      "type": "array",
+      "items": {
+        "oneOf": [
+          {
+            "$ref": "#/$defs/FolderEntry"
+          },
+          {
+            "$ref": "#/$defs/SeparatorEntry"
+          },
+          {
+            "$ref": "#/$defs/ProfileEntry"
+          },
+          {
+            "$ref": "#/$defs/MatchProfilesEntry"
+          },
+          {
+            "$ref": "#/$defs/RemainingProfilesEntry"
+          }
+        ]
+      }
+    },
     "Keybinding": {
       "additionalProperties": false,
       "properties": {
@@ -1946,6 +2123,9 @@
           },
           "type": "array"
         },
+        "newTabMenu": {
+          "$ref": "#/$defs/NewTabMenu"
+        },
         "language": {
           "default": "",
           "description": "Sets an override for the app's preferred language, expressed as a BCP-47 language tag like en-US.",
diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp
index 040c39d29d1..4381e24e0b1 100644
--- a/src/cascadia/TerminalApp/AppLogic.cpp
+++ b/src/cascadia/TerminalApp/AppLogic.cpp
@@ -52,6 +52,7 @@ static const std::array settingsLoadWarningsLabels {
     USES_RESOURCE(L"FailedToParseStartupActions"),
     USES_RESOURCE(L"FailedToParseSubCommands"),
     USES_RESOURCE(L"UnknownTheme"),
+    USES_RESOURCE(L"DuplicateRemainingProfilesEntry"),
 };
 static const std::array settingsLoadErrorsLabels {
     USES_RESOURCE(L"NoProfilesText"),
diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw
index d6a6772b698..fa5194db7ab 100644
--- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw
@@ -232,19 +232,19 @@
     <value>Warnings were found while parsing your keybindings:</value>
   </data>
   <data name="TooManyKeysForChord" xml:space="preserve">
-    <value>&#x2022; Found a keybinding with too many strings for the "keys" array. There should only be one string value in the "keys" array.</value>
-    <comment>{Locked="\"keys\"","&#x2022;"} This glyph is a bullet, used in a bulleted list.</comment>
+    <value>• Found a keybinding with too many strings for the "keys" array. There should only be one string value in the "keys" array.</value>
+    <comment>{Locked="\"keys\"","•"} This glyph is a bullet, used in a bulleted list.</comment>
   </data>
   <data name="FailedToParseSubCommands" xml:space="preserve">
-    <value>&#x2022; Failed to parse all subcommands of nested command.</value>
+    <value>• Failed to parse all subcommands of nested command.</value>
   </data>
   <data name="MissingRequiredParameter" xml:space="preserve">
-    <value>&#x2022; Found a keybinding that was missing a required parameter value. This keybinding will be ignored.</value>
-    <comment>{Locked="&#x2022;"} This glyph is a bullet, used in a bulleted list.</comment>
+    <value>• Found a keybinding that was missing a required parameter value. This keybinding will be ignored.</value>
+    <comment>{Locked="•"} This glyph is a bullet, used in a bulleted list.</comment>
   </data>
   <data name="UnknownTheme" xml:space="preserve">
-    <value>&#x2022; The specified "theme" was not found in the list of themes. Temporarily falling back to the default value.</value>
-    <comment>{Locked="&#x2022;"} This glyph is a bullet, used in a bulleted list.</comment>
+    <value>• The specified "theme" was not found in the list of themes. Temporarily falling back to the default value.</value>
+    <comment>{Locked="•"} This glyph is a bullet, used in a bulleted list.</comment>
   </data>
   <data name="LegacyGlobalsProperty" xml:space="preserve">
     <value>The "globals" property is deprecated - your settings might need updating. </value>
@@ -752,6 +752,10 @@
     <value>Suggestions found: {0}</value>
     <comment>{0} will be replaced with a number.</comment>
   </data>
+  <data name="DuplicateRemainingProfilesEntry" xml:space="preserve">
+    <value>The "newTabMenu" field contains more than one entry of type "remainingProfiles". Only the first one will be considered.</value>
+    <comment>{Locked="newTabMenu"} {Locked="remainingProfiles"}</comment>
+  </data>
   <data name="AboutToolTip" xml:space="preserve">
     <value>Open a dialog containing product information</value>
   </data>
@@ -788,4 +792,7 @@
   <data name="TabCloseToolTip" xml:space="preserve">
     <value>Close this tab</value>
   </data>
-</root>
+  <data name="NewTabMenuFolderEmpty" xml:space="preserve">
+    <value>Empty...</value>
+  </data>
+</root>
\ No newline at end of file
diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp
index 327347f5caf..1a8ee779568 100644
--- a/src/cascadia/TerminalApp/TerminalPage.cpp
+++ b/src/cascadia/TerminalApp/TerminalPage.cpp
@@ -824,79 +824,14 @@ namespace winrt::TerminalApp::implementation
         auto newTabFlyout = WUX::Controls::MenuFlyout{};
         newTabFlyout.Placement(WUX::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedLeft);
 
-        auto actionMap = _settings.ActionMap();
-        const auto defaultProfileGuid = _settings.GlobalSettings().DefaultProfile();
-        // the number of profiles should not change in the loop for this to work
-        const auto profileCount = gsl::narrow_cast<int>(_settings.ActiveProfiles().Size());
-        for (auto profileIndex = 0; profileIndex < profileCount; profileIndex++)
-        {
-            const auto profile = _settings.ActiveProfiles().GetAt(profileIndex);
-            auto profileMenuItem = WUX::Controls::MenuFlyoutItem{};
-
-            // Add the keyboard shortcuts based on the number of profiles defined
-            // Look for a keychord that is bound to the equivalent
-            // NewTab(ProfileIndex=N) action
-            NewTerminalArgs newTerminalArgs{ profileIndex };
-            NewTabArgs newTabArgs{ newTerminalArgs };
-            auto profileKeyChord{ actionMap.GetKeyBindingForAction(ShortcutAction::NewTab, newTabArgs) };
-
-            // make sure we find one to display
-            if (profileKeyChord)
-            {
-                _SetAcceleratorForMenuItem(profileMenuItem, profileKeyChord);
-            }
-
-            auto profileName = profile.Name();
-            profileMenuItem.Text(profileName);
-
-            // If there's an icon set for this profile, set it as the icon for
-            // this flyout item.
-            if (!profile.Icon().empty())
-            {
-                auto icon = IconPathConverter::IconWUX(profile.Icon());
-                Automation::AutomationProperties::SetAccessibilityView(icon, Automation::Peers::AccessibilityView::Raw);
-                profileMenuItem.Icon(icon);
-            }
-
-            if (profile.Guid() == defaultProfileGuid)
-            {
-                // Contrast the default profile with others in font weight.
-                profileMenuItem.FontWeight(FontWeights::Bold());
-            }
-
-            auto newTabRun = WUX::Documents::Run();
-            newTabRun.Text(RS_(L"NewTabRun/Text"));
-            auto newPaneRun = WUX::Documents::Run();
-            newPaneRun.Text(RS_(L"NewPaneRun/Text"));
-            newPaneRun.FontStyle(FontStyle::Italic);
-            auto newWindowRun = WUX::Documents::Run();
-            newWindowRun.Text(RS_(L"NewWindowRun/Text"));
-            newWindowRun.FontStyle(FontStyle::Italic);
-            auto elevatedRun = WUX::Documents::Run();
-            elevatedRun.Text(RS_(L"ElevatedRun/Text"));
-            elevatedRun.FontStyle(FontStyle::Italic);
-
-            auto textBlock = WUX::Controls::TextBlock{};
-            textBlock.Inlines().Append(newTabRun);
-            textBlock.Inlines().Append(WUX::Documents::LineBreak{});
-            textBlock.Inlines().Append(newPaneRun);
-            textBlock.Inlines().Append(WUX::Documents::LineBreak{});
-            textBlock.Inlines().Append(newWindowRun);
-            textBlock.Inlines().Append(WUX::Documents::LineBreak{});
-            textBlock.Inlines().Append(elevatedRun);
-
-            auto toolTip = WUX::Controls::ToolTip{};
-            toolTip.Content(textBlock);
-            WUX::Controls::ToolTipService::SetToolTip(profileMenuItem, toolTip);
-
-            profileMenuItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) {
-                if (auto page{ weakThis.get() })
-                {
-                    NewTerminalArgs newTerminalArgs{ profileIndex };
-                    page->_OpenNewTerminalViaDropdown(newTerminalArgs);
-                }
-            });
-            newTabFlyout.Items().Append(profileMenuItem);
+        // Create profile entries from the NewTabMenu configuration using a
+        // recursive helper function. This returns a std::vector of FlyoutItemBases,
+        // that we then add to our Flyout.
+        auto entries = _settings.GlobalSettings().NewTabMenu();
+        auto items = _CreateNewTabFlyoutItems(entries);
+        for (const auto& item : items)
+        {
+            newTabFlyout.Items().Append(item);
         }
 
         // add menu separator
@@ -932,6 +867,7 @@ namespace winrt::TerminalApp::implementation
                 settingsItem.Click({ this, &TerminalPage::_SettingsButtonOnClick });
                 newTabFlyout.Items().Append(settingsItem);
 
+                auto actionMap = _settings.ActionMap();
                 const auto settingsKeyChord{ actionMap.GetKeyBindingForAction(ShortcutAction::OpenSettings, OpenSettingsArgs{ SettingsTarget::SettingsUI }) };
                 if (settingsKeyChord)
                 {
@@ -991,6 +927,215 @@ namespace winrt::TerminalApp::implementation
         _newTabButton.Flyout(newTabFlyout);
     }
 
+    // Method Description:
+    // - For a given list of tab menu entries, this method will create the corresponding
+    //   list of flyout items. This is a recursive method that calls itself when it comes
+    //   across a folder entry.
+    std::vector<WUX::Controls::MenuFlyoutItemBase> TerminalPage::_CreateNewTabFlyoutItems(IVector<NewTabMenuEntry> entries)
+    {
+        std::vector<WUX::Controls::MenuFlyoutItemBase> items;
+
+        if (entries == nullptr || entries.Size() == 0)
+        {
+            return items;
+        }
+
+        for (const auto& entry : entries)
+        {
+            if (entry == nullptr)
+            {
+                continue;
+            }
+
+            switch (entry.Type())
+            {
+            case NewTabMenuEntryType::Separator:
+            {
+                items.push_back(WUX::Controls::MenuFlyoutSeparator{});
+                break;
+            }
+            // A folder has a custom name and icon, and has a number of entries that require
+            // us to call this method recursively.
+            case NewTabMenuEntryType::Folder:
+            {
+                const auto folderEntry = entry.as<FolderEntry>();
+                const auto folderEntries = folderEntry.Entries();
+
+                // If the folder is empty, we should skip the entry if AllowEmpty is false, or
+                // when the folder should inline.
+                // The IsEmpty check includes semantics for nested (empty) folders
+                if (folderEntries.Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto))
+                {
+                    break;
+                }
+
+                // Recursively generate flyout items
+                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)
+                {
+                    for (auto const& folderEntryItem : folderEntryItems)
+                    {
+                        items.push_back(folderEntryItem);
+                    }
+
+                    break;
+                }
+
+                // Otherwise, create a flyout
+                auto folderItem = WUX::Controls::MenuFlyoutSubItem{};
+                folderItem.Text(folderEntry.Name());
+
+                auto icon = _CreateNewTabFlyoutIcon(folderEntry.Icon());
+                folderItem.Icon(icon);
+
+                for (const auto& folderEntryItem : folderEntryItems)
+                {
+                    folderItem.Items().Append(folderEntryItem);
+                }
+
+                // If the folder is empty, and by now we know we set AllowEmpty to true,
+                // create a placeholder item here
+                if (folderEntries.Size() == 0)
+                {
+                    auto placeholder = WUX::Controls::MenuFlyoutItem{};
+                    placeholder.Text(RS_(L"NewTabMenuFolderEmpty"));
+                    placeholder.IsEnabled(false);
+
+                    folderItem.Items().Append(placeholder);
+                }
+
+                items.push_back(folderItem);
+                break;
+            }
+            // Any "collection entry" will simply make us add each profile in the collection
+            // separately. This collection is stored as a map <int, Profile>, so the correct
+            // profile index is already known.
+            case NewTabMenuEntryType::RemainingProfiles:
+            case NewTabMenuEntryType::MatchProfiles:
+            {
+                const auto remainingProfilesEntry = entry.as<ProfileCollectionEntry>();
+                if (remainingProfilesEntry.Profiles() == nullptr)
+                {
+                    break;
+                }
+
+                for (auto&& [profileIndex, remainingProfile] : remainingProfilesEntry.Profiles())
+                {
+                    items.push_back(_CreateNewTabFlyoutProfile(remainingProfile, profileIndex));
+                }
+
+                break;
+            }
+            // A single profile, the profile index is also given in the entry
+            case NewTabMenuEntryType::Profile:
+            {
+                const auto profileEntry = entry.as<ProfileEntry>();
+                if (profileEntry.Profile() == nullptr)
+                {
+                    break;
+                }
+
+                auto profileItem = _CreateNewTabFlyoutProfile(profileEntry.Profile(), profileEntry.ProfileIndex());
+                items.push_back(profileItem);
+                break;
+            }
+            }
+        }
+
+        return items;
+    }
+
+    // Method Description:
+    // - This method creates a flyout menu item for a given profile with the given index.
+    //   It makes sure to set the correct icon, keybinding, and click-action.
+    WUX::Controls::MenuFlyoutItem TerminalPage::_CreateNewTabFlyoutProfile(const Profile profile, int profileIndex)
+    {
+        auto profileMenuItem = WUX::Controls::MenuFlyoutItem{};
+
+        // Add the keyboard shortcuts based on the number of profiles defined
+        // Look for a keychord that is bound to the equivalent
+        // NewTab(ProfileIndex=N) action
+        NewTerminalArgs newTerminalArgs{ profileIndex };
+        NewTabArgs newTabArgs{ newTerminalArgs };
+        auto profileKeyChord{ _settings.ActionMap().GetKeyBindingForAction(ShortcutAction::NewTab, newTabArgs) };
+
+        // make sure we find one to display
+        if (profileKeyChord)
+        {
+            _SetAcceleratorForMenuItem(profileMenuItem, profileKeyChord);
+        }
+
+        auto profileName = profile.Name();
+        profileMenuItem.Text(profileName);
+
+        // If there's an icon set for this profile, set it as the icon for
+        // this flyout item
+        if (!profile.Icon().empty())
+        {
+            const auto icon = _CreateNewTabFlyoutIcon(profile.Icon());
+            profileMenuItem.Icon(icon);
+        }
+
+        if (profile.Guid() == _settings.GlobalSettings().DefaultProfile())
+        {
+            // Contrast the default profile with others in font weight.
+            profileMenuItem.FontWeight(FontWeights::Bold());
+        }
+
+        auto newTabRun = WUX::Documents::Run();
+        newTabRun.Text(RS_(L"NewTabRun/Text"));
+        auto newPaneRun = WUX::Documents::Run();
+        newPaneRun.Text(RS_(L"NewPaneRun/Text"));
+        newPaneRun.FontStyle(FontStyle::Italic);
+        auto newWindowRun = WUX::Documents::Run();
+        newWindowRun.Text(RS_(L"NewWindowRun/Text"));
+        newWindowRun.FontStyle(FontStyle::Italic);
+        auto elevatedRun = WUX::Documents::Run();
+        elevatedRun.Text(RS_(L"ElevatedRun/Text"));
+        elevatedRun.FontStyle(FontStyle::Italic);
+
+        auto textBlock = WUX::Controls::TextBlock{};
+        textBlock.Inlines().Append(newTabRun);
+        textBlock.Inlines().Append(WUX::Documents::LineBreak{});
+        textBlock.Inlines().Append(newPaneRun);
+        textBlock.Inlines().Append(WUX::Documents::LineBreak{});
+        textBlock.Inlines().Append(newWindowRun);
+        textBlock.Inlines().Append(WUX::Documents::LineBreak{});
+        textBlock.Inlines().Append(elevatedRun);
+
+        auto toolTip = WUX::Controls::ToolTip{};
+        toolTip.Content(textBlock);
+        WUX::Controls::ToolTipService::SetToolTip(profileMenuItem, toolTip);
+
+        profileMenuItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) {
+            if (auto page{ weakThis.get() })
+            {
+                NewTerminalArgs newTerminalArgs{ profileIndex };
+                page->_OpenNewTerminalViaDropdown(newTerminalArgs);
+            }
+        });
+
+        return profileMenuItem;
+    }
+
+    // Method Description:
+    // - Helper method to create an IconElement that can be passed to MenuFlyoutItems and
+    //   MenuFlyoutSubItems
+    IconElement TerminalPage::_CreateNewTabFlyoutIcon(const winrt::hstring& iconSource)
+    {
+        if (iconSource.empty())
+        {
+            return nullptr;
+        }
+
+        auto icon = IconPathConverter::IconWUX(iconSource);
+        Automation::AutomationProperties::SetAccessibilityView(icon, Automation::Peers::AccessibilityView::Raw);
+
+        return icon;
+    }
+
     // Function Description:
     // Called when the openNewTabDropdown keybinding is used.
     // Shows the dropdown flyout.
diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h
index 519d8786eaf..5a0ad22f183 100644
--- a/src/cascadia/TerminalApp/TerminalPage.h
+++ b/src/cascadia/TerminalApp/TerminalPage.h
@@ -240,6 +240,10 @@ namespace winrt::TerminalApp::implementation
         winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowLargePasteWarningDialog();
 
         void _CreateNewTabFlyout();
+        std::vector<winrt::Windows::UI::Xaml::Controls::MenuFlyoutItemBase> _CreateNewTabFlyoutItems(winrt::Windows::Foundation::Collections::IVector<Microsoft::Terminal::Settings::Model::NewTabMenuEntry> entries);
+        winrt::Windows::UI::Xaml::Controls::IconElement _CreateNewTabFlyoutIcon(const winrt::hstring& icon);
+        winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _CreateNewTabFlyoutProfile(const Microsoft::Terminal::Settings::Model::Profile profile, int profileIndex);
+
         void _OpenNewTabDropdown();
         HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr);
         void _CreateNewTabFromPane(std::shared_ptr<Pane> pane);
diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h
index d8543e74b5a..ac9049b4b12 100644
--- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h
+++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h
@@ -153,6 +153,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         void _refreshDefaultTerminals();
 
         void _resolveDefaultProfile() const;
+        void _resolveNewTabMenuProfiles() const;
+        void _resolveNewTabMenuProfilesSet(const winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> entries, winrt::Windows::Foundation::Collections::IMap<int, Model::Profile>& remainingProfiles, Model::RemainingProfilesEntry& remainingProfilesEntry) const;
 
         void _validateSettings();
         void _validateAllSchemesExist();
diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
index da175280cb0..d179ac27fb2 100644
--- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
+++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp
@@ -28,6 +28,10 @@
 #include "DefaultTerminal.h"
 #include "FileUtils.h"
 
+#include "ProfileEntry.h"
+#include "FolderEntry.h"
+#include "MatchProfilesEntry.h"
+
 using namespace winrt::Windows::Foundation::Collections;
 using namespace winrt::Windows::ApplicationModel::AppExtensions;
 using namespace winrt::Microsoft::Terminal::Settings;
@@ -1098,6 +1102,7 @@ CascadiaSettings::CascadiaSettings(SettingsLoader&& loader) :
     _warnings = winrt::single_threaded_vector(std::move(warnings));
 
     _resolveDefaultProfile();
+    _resolveNewTabMenuProfiles();
     _validateSettings();
 }
 
@@ -1276,3 +1281,162 @@ void CascadiaSettings::_resolveDefaultProfile() const
     // Use the first profile as the new default.
     GlobalSettings().DefaultProfile(_allProfiles.GetAt(0).Guid());
 }
+
+// Method Description:
+// - Iterates through the "newTabMenu" entries and for ProfileEntries resolves the "profile"
+//   fields, which can be a profile name, to a GUID and stores it back.
+// - It finds any "source" entries and finds all profiles generated by that source
+// - Lastly, it finds any "remainingProfiles" entries and stores which profiles they
+//   represent (those that were not resolved before). It adds a warning when
+//   multiple of these entries are found.
+void CascadiaSettings::_resolveNewTabMenuProfiles() const
+{
+    Model::RemainingProfilesEntry remainingProfilesEntry = nullptr;
+
+    // The TerminalPage needs to know which profile has which profile ID. To prevent
+    // continuous lookups in the _activeProfiles vector, we create a map <int, Profile>
+    // to store these indices in-flight.
+    auto remainingProfilesMap = std::map<int, Model::Profile>{};
+    auto activeProfileCount = gsl::narrow_cast<int>(_activeProfiles.Size());
+    for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++)
+    {
+        remainingProfilesMap.emplace(profileIndex, _activeProfiles.GetAt(profileIndex));
+    }
+
+    // We keep track of the "remaining profiles" - those that have not yet been resolved
+    // in either a "profile" or "source" entry. They will possibly be assigned to a
+    // "remainingProfiles" entry
+    auto remainingProfiles = single_threaded_map(std::move(remainingProfilesMap));
+
+    // We call a recursive helper function to process the entries
+    auto entries = _globals->NewTabMenu();
+    _resolveNewTabMenuProfilesSet(entries, remainingProfiles, remainingProfilesEntry);
+
+    // If a "remainingProfiles" entry has been found, assign to it the remaining profiles
+    if (remainingProfilesEntry != nullptr)
+    {
+        remainingProfilesEntry.Profiles(remainingProfiles);
+    }
+
+    // If the configuration does not have a "newTabMenu" field, GlobalAppSettings
+    // will return a default value containing just a "remainingProfiles" entry. However,
+    // this value is regenerated on every "get" operation, so the effect of setting
+    // the remaining profiles above will be undone. So only in the case that no custom
+    // value is present in GlobalAppSettings, we will store the modified default value.
+    if (!_globals->HasNewTabMenu())
+    {
+        _globals->NewTabMenu(entries);
+    }
+}
+
+// Method Description:
+// - Helper function that processes a set of tab menu entries and resolves any profile names
+//   or source fields as necessary - see function above for a more detailed explanation.
+void CascadiaSettings::_resolveNewTabMenuProfilesSet(const IVector<Model::NewTabMenuEntry> entries, IMap<int, Model::Profile>& remainingProfilesMap, Model::RemainingProfilesEntry& remainingProfilesEntry) const
+{
+    if (entries == nullptr || entries.Size() == 0)
+    {
+        return;
+    }
+
+    for (const auto& entry : entries)
+    {
+        if (entry == nullptr)
+        {
+            continue;
+        }
+
+        switch (entry.Type())
+        {
+        // For a simple profile entry, the "profile" field can either be a name or a GUID. We
+        // use the GetProfileByName function to resolve this name to a profile instance, then
+        // find the index of that profile, and store this information in the entry.
+        case NewTabMenuEntryType::Profile:
+        {
+            // We need to access the unresolved profile name, a field that is not exposed
+            // in the projected class. So, we need to first obtain our implementation struct
+            // instance, to access this field.
+            const auto profileEntry{ winrt::get_self<implementation::ProfileEntry>(entry.as<Model::ProfileEntry>()) };
+
+            // Find the profile by name
+            const auto profile = GetProfileByName(profileEntry->ProfileName());
+
+            // If not found, or if the profile is hidden, skip it
+            if (profile == nullptr || profile.Hidden())
+            {
+                profileEntry->Profile(nullptr); // override "default" profile
+                break;
+            }
+
+            // Find the index of the resulting profile and store the result in the entry
+            uint32_t profileIndex;
+            _activeProfiles.IndexOf(profile, profileIndex);
+
+            profileEntry->Profile(profile);
+            profileEntry->ProfileIndex(profileIndex);
+
+            // Remove from remaining profiles list (map)
+            remainingProfilesMap.TryRemove(profileIndex);
+
+            break;
+        }
+
+        // For a remainingProfiles entry, we store it in the variable that is passed back to our caller,
+        // except when that one has already been set (so we found a second/third/...) instance, which will
+        // trigger a warning. We then ignore this entry.
+        case NewTabMenuEntryType::RemainingProfiles:
+        {
+            if (remainingProfilesEntry != nullptr)
+            {
+                _warnings.Append(SettingsLoadWarnings::DuplicateRemainingProfilesEntry);
+            }
+            else
+            {
+                remainingProfilesEntry = entry.as<Model::RemainingProfilesEntry>();
+            }
+            break;
+        }
+
+        // For a folder, we simply call this method recursively
+        case NewTabMenuEntryType::Folder:
+        {
+            // We need to access the unfiltered entry list, a field that is not exposed
+            // in the projected class. So, we need to first obtain our implementation struct
+            // instance, to access this field.
+            const auto folderEntry{ winrt::get_self<implementation::FolderEntry>(entry.as<Model::FolderEntry>()) };
+
+            auto folderEntries = folderEntry->RawEntries();
+            _resolveNewTabMenuProfilesSet(folderEntries, remainingProfilesMap, remainingProfilesEntry);
+            break;
+        }
+
+        // For a "matchProfiles" entry, we iterate through the list of all profiles and
+        // find all those matching: generated by the same source, having the same name, or
+        // having the same commandline. This can be expanded with regex support in the future.
+        // We make sure that none of the matches are included in the "remaining profiles" section.
+        case NewTabMenuEntryType::MatchProfiles:
+        {
+            // We need to access the matching function, which is not exposed in the projected class.
+            // So, we need to first obtain our implementation struct instance, to access this field.
+            const auto matchEntry{ winrt::get_self<implementation::MatchProfilesEntry>(entry.as<Model::MatchProfilesEntry>()) };
+
+            matchEntry->Profiles(single_threaded_map<int, Model::Profile>());
+
+            auto activeProfileCount = gsl::narrow_cast<int>(_activeProfiles.Size());
+            for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++)
+            {
+                const auto profile = _activeProfiles.GetAt(profileIndex);
+
+                // On a match, we store it in the entry and remove it from the remaining list
+                if (matchEntry->MatchesProfile(profile))
+                {
+                    matchEntry->Profiles().Insert(profileIndex, profile);
+                    remainingProfilesMap.TryRemove(profileIndex);
+                }
+            }
+
+            break;
+        }
+        }
+    }
+}
diff --git a/src/cascadia/TerminalSettingsModel/FolderEntry.cpp b/src/cascadia/TerminalSettingsModel/FolderEntry.cpp
new file mode 100644
index 00000000000..54af1d8c773
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/FolderEntry.cpp
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "FolderEntry.h"
+#include "JsonUtils.h"
+#include "TerminalSettingsSerializationHelpers.h"
+
+#include "FolderEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+using namespace winrt::Windows::Foundation::Collections;
+
+static constexpr std::string_view NameKey{ "name" };
+static constexpr std::string_view IconKey{ "icon" };
+static constexpr std::string_view EntriesKey{ "entries" };
+static constexpr std::string_view InliningKey{ "inline" };
+static constexpr std::string_view AllowEmptyKey{ "allowEmpty" };
+
+FolderEntry::FolderEntry() noexcept :
+    FolderEntry{ winrt::hstring{} }
+{
+}
+
+FolderEntry::FolderEntry(const winrt::hstring& name) noexcept :
+    FolderEntryT<FolderEntry, NewTabMenuEntry>(NewTabMenuEntryType::Folder),
+    _Name{ name }
+{
+}
+
+Json::Value FolderEntry::ToJson() const
+{
+    auto json = NewTabMenuEntry::ToJson();
+
+    JsonUtils::SetValueForKey(json, NameKey, _Name);
+    JsonUtils::SetValueForKey(json, IconKey, _Icon);
+    JsonUtils::SetValueForKey(json, EntriesKey, _Entries);
+    JsonUtils::SetValueForKey(json, InliningKey, _Inlining);
+    JsonUtils::SetValueForKey(json, AllowEmptyKey, _AllowEmpty);
+
+    return json;
+}
+
+winrt::com_ptr<NewTabMenuEntry> FolderEntry::FromJson(const Json::Value& json)
+{
+    auto entry = winrt::make_self<FolderEntry>();
+
+    JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
+    JsonUtils::GetValueForKey(json, IconKey, entry->_Icon);
+    JsonUtils::GetValueForKey(json, EntriesKey, entry->_Entries);
+    JsonUtils::GetValueForKey(json, InliningKey, entry->_Inlining);
+    JsonUtils::GetValueForKey(json, AllowEmptyKey, entry->_AllowEmpty);
+
+    return entry;
+}
+
+// A FolderEntry should only expose the entries to actually render to WinRT,
+// to keep the logic for collapsing/expanding more centralised.
+using NewTabMenuEntryModel = winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntry;
+IVector<NewTabMenuEntryModel> FolderEntry::Entries() const
+{
+    // We filter the full list of entries from JSON to just include the
+    // non-empty ones.
+    IVector<NewTabMenuEntryModel> result{ winrt::single_threaded_vector<NewTabMenuEntryModel>() };
+
+    for (const auto& entry : _Entries)
+    {
+        if (entry == nullptr)
+        {
+            continue;
+        }
+
+        switch (entry.Type())
+        {
+        case NewTabMenuEntryType::Invalid:
+            continue;
+
+        // A profile is filtered out if it is not valid, so if it was not resolved
+        case NewTabMenuEntryType::Profile:
+        {
+            const auto profileEntry = entry.as<ProfileEntry>();
+            if (profileEntry.Profile() == nullptr)
+            {
+                continue;
+            }
+            break;
+        }
+
+        // Any profile collection is filtered out if there are no results
+        case NewTabMenuEntryType::RemainingProfiles:
+        case NewTabMenuEntryType::MatchProfiles:
+        {
+            const auto profileCollectionEntry = entry.as<ProfileCollectionEntry>();
+            if (profileCollectionEntry.Profiles().Size() == 0)
+            {
+                continue;
+            }
+            break;
+        }
+
+        // A folder is filtered out if it has an effective size of 0 (calling
+        // this filtering method recursively), and if it is not allowed to be
+        // empty, or if it should auto-inline.
+        case NewTabMenuEntryType::Folder:
+        {
+            const auto folderEntry = entry.as<Model::FolderEntry>();
+            if (folderEntry.Entries().Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto))
+            {
+                continue;
+            }
+            break;
+        }
+        }
+
+        result.Append(entry);
+    }
+
+    return result;
+}
diff --git a/src/cascadia/TerminalSettingsModel/FolderEntry.h b/src/cascadia/TerminalSettingsModel/FolderEntry.h
new file mode 100644
index 00000000000..619d228fa7b
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/FolderEntry.h
@@ -0,0 +1,55 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- FolderEntry.h
+
+Abstract:
+- A folder entry in the "new tab" dropdown menu, 
+
+Author(s):
+- Floris Westerman - August 2022
+
+--*/
+#pragma once
+
+#include "pch.h"
+#include "NewTabMenuEntry.h"
+#include "FolderEntry.g.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+    struct FolderEntry : FolderEntryT<FolderEntry, NewTabMenuEntry>
+    {
+    public:
+        FolderEntry() noexcept;
+        explicit FolderEntry(const winrt::hstring& name) noexcept;
+
+        Json::Value ToJson() const override;
+        static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
+
+        // In JSON, we can set arbitrarily many profiles or nested profiles, that might not all
+        // be rendered; for example, when a profile entry is invalid, or when a folder is empty.
+        // Therefore, we will store the JSON entries list internally, and then expose only the
+        // entries to be rendered to WinRT.
+        winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> Entries() const;
+        winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> RawEntries() const
+        {
+            return _Entries;
+        };
+
+        WINRT_PROPERTY(winrt::hstring, Name);
+        WINRT_PROPERTY(winrt::hstring, Icon);
+        WINRT_PROPERTY(FolderEntryInlining, Inlining, FolderEntryInlining::Never);
+        WINRT_PROPERTY(bool, AllowEmpty, false);
+
+    private:
+        winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> _Entries{};
+    };
+}
+
+namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
+{
+    BASIC_FACTORY(FolderEntry);
+}
diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h
index e90253a28cd..4a594948eba 100644
--- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h
+++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h
@@ -23,6 +23,8 @@ Author(s):
 #include "Command.h"
 #include "ColorScheme.h"
 #include "Theme.h"
+#include "NewTabMenuEntry.h"
+#include "RemainingProfilesEntry.h"
 
 // fwdecl unittest classes
 namespace SettingsModelLocalTests
diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl
index a6ff9c188c0..06931641e41 100644
--- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl
+++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl
@@ -6,6 +6,7 @@
 import "Theme.idl";
 import "ColorScheme.idl";
 import "ActionMap.idl";
+import "NewTabMenuEntry.idl";
 
 namespace Microsoft.Terminal.Settings.Model
 {
@@ -94,6 +95,7 @@ namespace Microsoft.Terminal.Settings.Model
         INHERITABLE_SETTING(Boolean, AlwaysShowNotificationIcon);
         INHERITABLE_SETTING(IVector<String>, DisabledProfileSources);
         INHERITABLE_SETTING(Boolean, ShowAdminShield);
+        INHERITABLE_SETTING(IVector<NewTabMenuEntry>, NewTabMenu);
         INHERITABLE_SETTING(Boolean, EnableColorSelection);
 
         Windows.Foundation.Collections.IMapView<String, ColorScheme> ColorSchemes();
diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
index 91ff1b068a4..a3b957aa307 100644
--- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h
+++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
@@ -61,7 +61,8 @@ Author(s):
     X(winrt::Windows::Foundation::Collections::IVector<winrt::hstring>, DisabledProfileSources, "disabledProfileSources", nullptr)                         \
     X(bool, ShowAdminShield, "showAdminShield", true)                                                                                                      \
     X(bool, TrimPaste, "trimPaste", true)                                                                                                                  \
-    X(bool, EnableColorSelection, "experimental.enableColorSelection", false)
+    X(bool, EnableColorSelection, "experimental.enableColorSelection", false)                                                                              \
+    X(winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry>, NewTabMenu, "newTabMenu", winrt::single_threaded_vector<Model::NewTabMenuEntry>({ Model::RemainingProfilesEntry{} }))
 
 #define MTSM_PROFILE_SETTINGS(X)                                                                                                                               \
     X(int32_t, HistorySize, "historySize", DEFAULT_HISTORY_SIZE)                                                                                               \
diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp
new file mode 100644
index 00000000000..2873679018e
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.cpp
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "MatchProfilesEntry.h"
+#include "JsonUtils.h"
+
+#include "MatchProfilesEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+
+static constexpr std::string_view NameKey{ "name" };
+static constexpr std::string_view CommandlineKey{ "commandline" };
+static constexpr std::string_view SourceKey{ "source" };
+
+MatchProfilesEntry::MatchProfilesEntry() noexcept :
+    MatchProfilesEntryT<MatchProfilesEntry, ProfileCollectionEntry>(NewTabMenuEntryType::MatchProfiles)
+{
+}
+
+Json::Value MatchProfilesEntry::ToJson() const
+{
+    auto json = NewTabMenuEntry::ToJson();
+
+    JsonUtils::SetValueForKey(json, NameKey, _Name);
+    JsonUtils::SetValueForKey(json, CommandlineKey, _Commandline);
+    JsonUtils::SetValueForKey(json, SourceKey, _Source);
+
+    return json;
+}
+
+winrt::com_ptr<NewTabMenuEntry> MatchProfilesEntry::FromJson(const Json::Value& json)
+{
+    auto entry = winrt::make_self<MatchProfilesEntry>();
+
+    JsonUtils::GetValueForKey(json, NameKey, entry->_Name);
+    JsonUtils::GetValueForKey(json, CommandlineKey, entry->_Commandline);
+    JsonUtils::GetValueForKey(json, SourceKey, entry->_Source);
+
+    return entry;
+}
+
+bool MatchProfilesEntry::MatchesProfile(const Model::Profile& profile)
+{
+    // We use an optional here instead of a simple bool directly, since there is no
+    // sensible default value for the desired semantics: the first property we want
+    // to match on should always be applied (so one would set "true" as a default),
+    // but if none of the properties are set, the default return value should be false
+    // since this entry type is expected to behave like a positive match/whitelist.
+    //
+    // The easiest way to deal with this neatly is to use an optional, then for any
+    // property to match we consider a null value to be "true", and for the return
+    // value of the function we consider the null value to be "false".
+    auto isMatching = std::optional<bool>{};
+
+    if (!_Name.empty())
+    {
+        isMatching = { isMatching.value_or(true) && _Name == profile.Name() };
+    }
+
+    if (!_Source.empty())
+    {
+        isMatching = { isMatching.value_or(true) && _Source == profile.Source() };
+    }
+
+    if (!_Commandline.empty())
+    {
+        isMatching = { isMatching.value_or(true) && _Commandline == profile.Commandline() };
+    }
+
+    return isMatching.value_or(false);
+}
diff --git a/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h
new file mode 100644
index 00000000000..464a6780c12
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/MatchProfilesEntry.h
@@ -0,0 +1,42 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- MatchProfilesEntry.h
+
+Abstract:
+- An entry in the "new tab" dropdown menu that represents a collection
+    of profiles that match a specified name, source, or command line.
+
+Author(s):
+- Floris Westerman - November 2022
+
+--*/
+#pragma once
+
+#include "ProfileCollectionEntry.h"
+#include "MatchProfilesEntry.g.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+    struct MatchProfilesEntry : MatchProfilesEntryT<MatchProfilesEntry, ProfileCollectionEntry>
+    {
+    public:
+        MatchProfilesEntry() noexcept;
+
+        Json::Value ToJson() const override;
+        static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
+
+        bool MatchesProfile(const Model::Profile& profile);
+
+        WINRT_PROPERTY(winrt::hstring, Name);
+        WINRT_PROPERTY(winrt::hstring, Commandline);
+        WINRT_PROPERTY(winrt::hstring, Source);
+    };
+}
+
+namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
+{
+    BASIC_FACTORY(MatchProfilesEntry);
+}
diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
index a1aaf301ce8..07266a3eaf3 100644
--- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
+++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj
@@ -21,6 +21,27 @@
   <!-- ========================= Headers ======================== -->
   <ItemGroup>
     <ClInclude Include="ActionArgsMagic.h" />
+    <ClInclude Include="NewTabMenuEntry.h">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClInclude>
+    <ClInclude Include="SeparatorEntry.h">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClInclude>
+    <ClInclude Include="FolderEntry.h">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClInclude>
+    <ClInclude Include="ProfileEntry.h">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClInclude>
+    <ClInclude Include="RemainingProfilesEntry.h">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClInclude>
+    <ClInclude Include="ProfileCollectionEntry.h">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClInclude>
+    <ClInclude Include="MatchProfilesEntry.h">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClInclude>
     <ClInclude Include="VisualStudioGenerator.h" />
     <ClInclude Include="DefaultTerminal.h">
       <DependentUpon>DefaultTerminal.idl</DependentUpon>
@@ -162,6 +183,27 @@
     <ClCompile Include="EnumMappings.cpp">
       <DependentUpon>EnumMappings.idl</DependentUpon>
     </ClCompile>
+    <ClCompile Include="NewTabMenuEntry.cpp">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClCompile>
+    <ClCompile Include="SeparatorEntry.cpp">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClCompile>
+    <ClCompile Include="FolderEntry.cpp">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClCompile>
+    <ClCompile Include="ProfileEntry.cpp">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClCompile>
+    <ClCompile Include="ProfileCollectionEntry.cpp">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClCompile>
+    <ClCompile Include="RemainingProfilesEntry.cpp">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClCompile>
+    <ClCompile Include="MatchProfilesEntry.cpp">
+      <DependentUpon>NewTabMenuEntry.idl</DependentUpon>
+    </ClCompile>
     <ClCompile Include="VsDevCmdGenerator.cpp" />
     <ClCompile Include="VsDevShellGenerator.cpp" />
     <ClCompile Include="VsSetupConfiguration.cpp" />
@@ -180,6 +222,7 @@
     <Midl Include="ApplicationState.idl" />
     <Midl Include="CascadiaSettings.idl" />
     <Midl Include="ColorScheme.idl" />
+    <Midl Include="NewTabMenuEntry.idl" />
     <Midl Include="Theme.idl" />
     <Midl Include="Command.idl" />
     <Midl Include="DefaultTerminal.idl" />
diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters
index fb21fccdc31..ff79f87c270 100644
--- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters
+++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters
@@ -94,6 +94,7 @@
       <Filter>profileGeneration</Filter>
     </ClInclude>
     <ClInclude Include="ActionArgsMagic.h" />
+    <ClInclude Include="NewTabMenuEntry.h" />
   </ItemGroup>
   <ItemGroup>
     <Midl Include="ActionArgs.idl" />
@@ -113,6 +114,7 @@
     <Midl Include="FontConfig.idl" />
     <Midl Include="DefaultTerminal.idl" />
     <Midl Include="ApplicationState.idl" />
+    <Midl Include="NewTabMenuEntry.idl" />
   </ItemGroup>
   <ItemGroup>
     <None Include="packages.config" />
diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp
new file mode 100644
index 00000000000..e346d9711c1
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.cpp
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "NewTabMenuEntry.h"
+#include "JsonUtils.h"
+#include "TerminalSettingsSerializationHelpers.h"
+#include "SeparatorEntry.h"
+#include "FolderEntry.h"
+#include "ProfileEntry.h"
+#include "RemainingProfilesEntry.h"
+#include "MatchProfilesEntry.h"
+
+#include "NewTabMenuEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+using NewTabMenuEntryType = winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntryType;
+
+static constexpr std::string_view TypeKey{ "type" };
+
+NewTabMenuEntry::NewTabMenuEntry(const NewTabMenuEntryType type) noexcept :
+    _Type{ type }
+{
+}
+
+// This method will be overridden by the subclasses, which will then call this
+// parent implementation for a "base" json object.
+Json::Value NewTabMenuEntry::ToJson() const
+{
+    Json::Value json{ Json::ValueType::objectValue };
+
+    JsonUtils::SetValueForKey(json, TypeKey, _Type);
+
+    return json;
+}
+
+// Deserialize the JSON object based on the given type. We use the map from above for that.
+winrt::com_ptr<NewTabMenuEntry> NewTabMenuEntry::FromJson(const Json::Value& json)
+{
+    const auto type = JsonUtils::GetValueForKey<NewTabMenuEntryType>(json, TypeKey);
+
+    switch (type)
+    {
+    case NewTabMenuEntryType::Separator:
+        return SeparatorEntry::FromJson(json);
+    case NewTabMenuEntryType::Folder:
+        return FolderEntry::FromJson(json);
+    case NewTabMenuEntryType::Profile:
+        return ProfileEntry::FromJson(json);
+    case NewTabMenuEntryType::RemainingProfiles:
+        return RemainingProfilesEntry::FromJson(json);
+    case NewTabMenuEntryType::MatchProfiles:
+        return MatchProfilesEntry::FromJson(json);
+    default:
+        return nullptr;
+    }
+}
diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h
new file mode 100644
index 00000000000..57f6bc1a000
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.h
@@ -0,0 +1,72 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- NewTabMenuEntry.h
+
+Abstract:
+- An entry in the "new tab" dropdown menu. These entries exist in a few varieties,
+    such as separators, folders, or profile links.
+
+Author(s):
+- Floris Westerman - August 2022
+
+--*/
+#pragma once
+
+#include "NewTabMenuEntry.g.h"
+#include "JsonUtils.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+    struct NewTabMenuEntry : NewTabMenuEntryT<NewTabMenuEntry>
+    {
+    public:
+        static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
+        virtual Json::Value ToJson() const;
+
+        WINRT_PROPERTY(NewTabMenuEntryType, Type, NewTabMenuEntryType::Invalid);
+
+        // We have a protected/hidden constructor so consumers cannot instantiate
+        // this base class directly and need to go through either FromJson
+        // or one of the subclasses.
+    protected:
+        explicit NewTabMenuEntry(const NewTabMenuEntryType type) noexcept;
+    };
+}
+
+namespace Microsoft::Terminal::Settings::Model::JsonUtils
+{
+    using namespace winrt::Microsoft::Terminal::Settings::Model;
+
+    template<>
+    struct ConversionTrait<NewTabMenuEntry>
+    {
+        NewTabMenuEntry FromJson(const Json::Value& json)
+        {
+            const auto entry = implementation::NewTabMenuEntry::FromJson(json);
+            if (entry == nullptr)
+            {
+                return nullptr;
+            }
+
+            return *entry;
+        }
+
+        bool CanConvert(const Json::Value& json) const
+        {
+            return json.isObject();
+        }
+
+        Json::Value ToJson(const NewTabMenuEntry& val)
+        {
+            return winrt::get_self<implementation::NewTabMenuEntry>(val)->ToJson();
+        }
+
+        std::string TypeDescription() const
+        {
+            return "NewTabMenuEntry";
+        }
+    };
+}
diff --git a/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl
new file mode 100644
index 00000000000..64a3b1fdd94
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/NewTabMenuEntry.idl
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import "Profile.idl";
+
+namespace Microsoft.Terminal.Settings.Model
+{
+    enum NewTabMenuEntryType
+    {
+        Invalid = 0,
+        Profile,
+        Separator,
+        Folder,
+        RemainingProfiles,
+        MatchProfiles
+    };
+
+    [default_interface] unsealed runtimeclass NewTabMenuEntry
+    {
+        NewTabMenuEntryType Type;
+    }
+
+    [default_interface] runtimeclass SeparatorEntry : NewTabMenuEntry
+    {
+        SeparatorEntry();
+    }
+
+    [default_interface] runtimeclass ProfileEntry : NewTabMenuEntry
+    {
+        ProfileEntry();
+        ProfileEntry(String profile);
+
+        Profile Profile;
+        Int32 ProfileIndex;
+    }
+
+    enum FolderEntryInlining
+    {
+        Never = 0,
+        Auto
+    };
+
+    [default_interface] runtimeclass FolderEntry : NewTabMenuEntry 
+    {
+        FolderEntry();
+        FolderEntry(String name);
+
+        String Name;
+        String Icon;
+        FolderEntryInlining Inlining;
+        Boolean AllowEmpty;
+
+        IVector<NewTabMenuEntry> Entries();
+    }
+
+    [default_interface] unsealed runtimeclass ProfileCollectionEntry : NewTabMenuEntry
+    {
+        IMap<Int32, Profile> Profiles;
+    }
+
+    [default_interface] runtimeclass RemainingProfilesEntry : ProfileCollectionEntry
+    {
+        RemainingProfilesEntry();
+    }
+
+    [default_interface] runtimeclass MatchProfilesEntry : ProfileCollectionEntry
+    {
+        MatchProfilesEntry();
+
+        String Name;
+        String Commandline;
+        String Source;
+    }
+}
diff --git a/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.cpp b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.cpp
new file mode 100644
index 00000000000..430f4153015
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.cpp
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "ProfileCollectionEntry.h"
+#include "JsonUtils.h"
+
+#include "ProfileCollectionEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+
+ProfileCollectionEntry::ProfileCollectionEntry(const NewTabMenuEntryType type) noexcept :
+    ProfileCollectionEntryT<ProfileCollectionEntry, NewTabMenuEntry>(type)
+{
+}
diff --git a/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.h b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.h
new file mode 100644
index 00000000000..86cfa723445
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/ProfileCollectionEntry.h
@@ -0,0 +1,39 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- SeparatorEntry.h
+
+Abstract:
+- An entry in the "new tab" dropdown menu that represents some collection
+    of profiles. This is an abstract class that has concretizations like
+    "all profiles from a source" or "all remaining profiles"
+
+Author(s):
+- Floris Westerman - August 2022
+
+--*/
+#pragma once
+
+#include "NewTabMenuEntry.h"
+#include "ProfileCollectionEntry.g.h"
+#include "Profile.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+    struct ProfileCollectionEntry : ProfileCollectionEntryT<ProfileCollectionEntry, NewTabMenuEntry>
+    {
+    public:
+        // Since a comma does not work very nicely in a macro and we need one
+        // for our map definition, we use a macro te define a comma.
+#define COMMA ,
+        WINRT_PROPERTY(winrt::Windows::Foundation::Collections::IMap<int COMMA Model::Profile>, Profiles);
+#undef COMMA
+
+        // We have a protected/hidden constructor so consumers cannot instantiate
+        // this class directly and need to go through one of the subclasses.
+    protected:
+        explicit ProfileCollectionEntry(const NewTabMenuEntryType type) noexcept;
+    };
+}
diff --git a/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp b/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp
new file mode 100644
index 00000000000..c857eea8678
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/ProfileEntry.cpp
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "ProfileEntry.h"
+#include "JsonUtils.h"
+
+#include "ProfileEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+
+static constexpr std::string_view ProfileKey{ "profile" };
+
+ProfileEntry::ProfileEntry() noexcept :
+    ProfileEntry{ winrt::hstring{} }
+{
+}
+
+ProfileEntry::ProfileEntry(const winrt::hstring& profile) noexcept :
+    ProfileEntryT<ProfileEntry, NewTabMenuEntry>(NewTabMenuEntryType::Profile),
+    _ProfileName{ profile }
+{
+}
+
+Json::Value ProfileEntry::ToJson() const
+{
+    auto json = NewTabMenuEntry::ToJson();
+
+    // We will now return a profile reference to the JSON representation. Logic is
+    // as follows:
+    // - When Profile is null, this is typically because an existing profile menu entry
+    //   in the JSON config is invalid (nonexistent or hidden profile). Then, we store
+    //   the original profile string value as read from JSON, to improve portability
+    //   of the settings file and limit modifications to the JSON.
+    // - Otherwise, we always store the GUID of the profile. This will effectively convert
+    //   all name-matched profiles from the settings file to GUIDs. This might be unexpected
+    //   to some users, but is less error-prone and will continue to work when profile
+    //   names are changed.
+    if (_Profile == nullptr)
+    {
+        JsonUtils::SetValueForKey(json, ProfileKey, _ProfileName);
+    }
+    else
+    {
+        JsonUtils::SetValueForKey(json, ProfileKey, _Profile.Guid());
+    }
+
+    return json;
+}
+
+winrt::com_ptr<NewTabMenuEntry> ProfileEntry::FromJson(const Json::Value& json)
+{
+    auto entry = winrt::make_self<ProfileEntry>();
+
+    JsonUtils::GetValueForKey(json, ProfileKey, entry->_ProfileName);
+
+    return entry;
+}
diff --git a/src/cascadia/TerminalSettingsModel/ProfileEntry.h b/src/cascadia/TerminalSettingsModel/ProfileEntry.h
new file mode 100644
index 00000000000..fe0f4573d29
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/ProfileEntry.h
@@ -0,0 +1,53 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- FolderEntry.h
+
+Abstract:
+- A profile entry in the "new tab" dropdown menu, referring to
+    a single profile.
+
+Author(s):
+- Floris Westerman - August 2022
+
+--*/
+#pragma once
+
+#include "NewTabMenuEntry.h"
+#include "ProfileEntry.g.h"
+
+#include "Profile.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+    struct ProfileEntry : ProfileEntryT<ProfileEntry, NewTabMenuEntry>
+    {
+    public:
+        ProfileEntry() noexcept;
+        explicit ProfileEntry(const winrt::hstring& profile) noexcept;
+
+        Json::Value ToJson() const override;
+        static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
+
+        // In JSON, only a profile name (guid or string) can be set;
+        // but the consumers of this class would like to have direct access
+        // to the appropriate Model::Profile. Therefore, we have a read-only
+        // property ProfileName that corresponds to the JSON value, and
+        // then CascadiaSettings::_resolveNewTabMenuProfiles() will populate
+        // the Profile and ProfileIndex properties appropriately
+        winrt::hstring ProfileName() const noexcept { return _ProfileName; };
+
+        WINRT_PROPERTY(Model::Profile, Profile);
+        WINRT_PROPERTY(int, ProfileIndex);
+
+    private:
+        winrt::hstring _ProfileName;
+    };
+}
+
+namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
+{
+    BASIC_FACTORY(ProfileEntry);
+}
diff --git a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp
new file mode 100644
index 00000000000..1d2539da2d8
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.cpp
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "RemainingProfilesEntry.h"
+#include "NewTabMenuEntry.h"
+#include "JsonUtils.h"
+
+#include "RemainingProfilesEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+
+RemainingProfilesEntry::RemainingProfilesEntry() noexcept :
+    RemainingProfilesEntryT<RemainingProfilesEntry, ProfileCollectionEntry>(NewTabMenuEntryType::RemainingProfiles)
+{
+}
+
+winrt::com_ptr<NewTabMenuEntry> RemainingProfilesEntry::FromJson(const Json::Value&)
+{
+    return winrt::make_self<RemainingProfilesEntry>();
+}
diff --git a/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h
new file mode 100644
index 00000000000..153669e4f7b
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/RemainingProfilesEntry.h
@@ -0,0 +1,35 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- SeparatorEntry.h
+
+Abstract:
+- An entry in the "new tab" dropdown menu that represents all profiles
+    that were not included explicitly elsewhere
+
+Author(s):
+- Floris Westerman - August 2022
+
+--*/
+#pragma once
+
+#include "ProfileCollectionEntry.h"
+#include "RemainingProfilesEntry.g.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+    struct RemainingProfilesEntry : RemainingProfilesEntryT<RemainingProfilesEntry, ProfileCollectionEntry>
+    {
+    public:
+        RemainingProfilesEntry() noexcept;
+
+        static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
+    };
+}
+
+namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
+{
+    BASIC_FACTORY(RemainingProfilesEntry);
+}
diff --git a/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp b/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp
new file mode 100644
index 00000000000..d6cee21090c
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/SeparatorEntry.cpp
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "SeparatorEntry.h"
+#include "JsonUtils.h"
+
+#include "SeparatorEntry.g.cpp"
+
+using namespace Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
+
+SeparatorEntry::SeparatorEntry() noexcept :
+    SeparatorEntryT<SeparatorEntry, NewTabMenuEntry>(NewTabMenuEntryType::Separator)
+{
+}
+
+winrt::com_ptr<NewTabMenuEntry> SeparatorEntry::FromJson(const Json::Value&)
+{
+    return winrt::make_self<SeparatorEntry>();
+}
diff --git a/src/cascadia/TerminalSettingsModel/SeparatorEntry.h b/src/cascadia/TerminalSettingsModel/SeparatorEntry.h
new file mode 100644
index 00000000000..e074ce251c7
--- /dev/null
+++ b/src/cascadia/TerminalSettingsModel/SeparatorEntry.h
@@ -0,0 +1,34 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- SeparatorEntry.h
+
+Abstract:
+- A separator entry in the "new tab" dropdown menu
+
+Author(s):
+- Floris Westerman - August 2022
+
+--*/
+#pragma once
+
+#include "NewTabMenuEntry.h"
+#include "SeparatorEntry.g.h"
+
+namespace winrt::Microsoft::Terminal::Settings::Model::implementation
+{
+    struct SeparatorEntry : SeparatorEntryT<SeparatorEntry, NewTabMenuEntry>
+    {
+    public:
+        SeparatorEntry() noexcept;
+
+        static com_ptr<NewTabMenuEntry> FromJson(const Json::Value& json);
+    };
+}
+
+namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation
+{
+    BASIC_FACTORY(SeparatorEntry);
+}
diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
index 95e5a03cf86..fd5738da4a0 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
+++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h
@@ -651,6 +651,27 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::ScrollToMarkDirection)
     };
 };
 
+// Possible NewTabMenuEntryType values
+JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::NewTabMenuEntryType)
+{
+    JSON_MAPPINGS(5) = {
+        pair_type{ "profile", ValueType::Profile },
+        pair_type{ "separator", ValueType::Separator },
+        pair_type{ "folder", ValueType::Folder },
+        pair_type{ "remainingProfiles", ValueType::RemainingProfiles },
+        pair_type{ "matchProfiles", ValueType::MatchProfiles },
+    };
+};
+
+// Possible FolderEntryInlining values
+JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::FolderEntryInlining)
+{
+    JSON_MAPPINGS(2) = {
+        pair_type{ "never", ValueType::Never },
+        pair_type{ "auto", ValueType::Auto },
+    };
+};
+
 template<>
 struct ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait<::winrt::Microsoft::Terminal::Control::SelectionColor>
 {
diff --git a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl
index 43dee4e5a61..5d3f676609a 100644
--- a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl
+++ b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl
@@ -22,6 +22,7 @@ namespace Microsoft.Terminal.Settings.Model
         FailedToParseStartupActions,
         FailedToParseSubCommands,
         UnknownTheme,
+        DuplicateRemainingProfilesEntry,
         WARNINGS_SIZE // IMPORTANT: This MUST be the last value in this enum. It's an unused placeholder.
     };
 
diff --git a/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj b/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj
index 88132dc9402..14a3532fdf0 100644
--- a/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj
+++ b/src/cascadia/TerminalSettingsModel/dll/Microsoft.Terminal.Settings.Model.vcxproj
@@ -38,6 +38,25 @@
     <ClInclude Include="../Profile.h" />
     <ClInclude Include="../TerminalWarnings.h" />
     <ClInclude Include="../IconPathConverter.h" />
+    <ClInclude Include="../NewTabMenuEntry.h" />
+    <ClInclude Include="../SeparatorEntry.h">
+      <DependentUpon>../NewTabMenuEntry.h</DependentUpon>
+    </ClInclude>
+    <ClInclude Include="../FolderEntry.h">
+      <DependentUpon>../NewTabMenuEntry.h</DependentUpon>
+    </ClInclude>
+    <ClInclude Include="../ProfileEntry.h">
+      <DependentUpon>../NewTabMenuEntry.h</DependentUpon>
+    </ClInclude>
+    <ClInclude Include="../RemainingProfilesEntry.h">
+      <DependentUpon>../NewTabMenuEntry.h</DependentUpon>
+    </ClInclude>
+    <ClInclude Include="../ProfileCollectionEntry.h">
+      <DependentUpon>../NewTabMenuEntry.h</DependentUpon>
+    </ClInclude>
+    <ClInclude Include="../MatchProfilesEntry.h">
+      <DependentUpon>../NewTabMenuEntry.h</DependentUpon>
+    </ClInclude>
   </ItemGroup>
   <!-- ========================= Cpp Files ======================== -->
   <!-- Don't put source files in here - put them in the lib's vcxproj instead! -->


Test patch: