Repository: microsoft/terminal (language: cpp)

Issue:
A pane doesn't necessarily need to host a terminal.
A pane doesn't necessarily need to host a terminal. It could potentially host another `UIElement`. One could imagine enabling a user to quickly open up a Browser pane to search for a particular string without needing to leave the terminal.



_notes_:
* [`dev/migrie/f/non-terminal-panes`](https://github.com/microsoft/terminal/tree/dev/migrie/f/non-terminal-panes) was the original prototype
  This prototype built a media pane for the Terminal. An example screenshot: 
  ![image](https://user-images.githubusercontent.com/18356694/199717120-dddf84e8-6166-43dc-8d43-8b5fdd574deb.png)
  I can't find the original gif, cause that was from 2019. The demo deck is in a recording linked to a mail from jeffbog "RE: DEP Hackathon Share-Out", 8/7/2019
* [`dev/migrie/fhl/adaptive-card-extension`](https://github.com/microsoft/terminal/compare/dev/migrie/fhl/adaptive-card-extension) had a draft of this in Oct 2021
* [`dev/migrie/fhl/non-terminal-panes-2023`](https://github.com/microsoft/terminal/compare/dev/migrie/fhl/non-terminal-panes-2023) an attempted start circa May 2023. Didn't have more than a few hours for it though. 
  * [`dev/migrie/fhl/scratchpad-pane`](https://github.com/microsoft/terminal/compare/dev/migrie/fhl/scratchpad-pane) for the branch with a test pane in it. 
    * [`dev/migrie/f/sui-panes`](https://github.com/microsoft/terminal/compare/dev/migrie/f/sui-panes)
      * `dev/migrie/fhl-fall-2023/oceans`
        * A comment from leonard: you can just render an HWND to a Composition surface, and then insert a comp surface into the xaml tree. So look at that next hackathon
      * Tasks pane: `dev/migrie/fhl/tasks-pane` https://github.com/microsoft/terminal/compare/dev/migrie/f/sui-panes...dev/migrie/fhl/tasks-pane,  somewhere in the neighborhood of #13445 
        * Markdown pane: `dev/migrie/fhl/md-pane` https://github.com/microsoft/terminal/compare/dev/migrie/f/tasks-pane...dev/migrie/fhl/md-pane
      * +til events ==> `dev/migrie/fhl/2024-spring-merge-base`
        * Inline notebooks: https://github.com/microsoft/terminal/compare/dev/migrie/fhl/2024-spring-merge-base...dev/migrie/fhl/2024-inline-notebook
  
    


Discussion / hints:
This issue is related I believe #644 
From @sharpninja in #4719,

> # Description of the new feature/enhancement
> 
> I know this may seem way out there, but hear me out.
> 
> Windows Terminal is about _tools_.  A single window open that has our command line tools in one place.  There are many things that go along with that that are better when provided as a GUI, such as Settings for tools.  In addition, other things, such as package managers, greatly benefit from having a GUI to observe listings of packages and their details.  These GUIs are _related_ to our open command prompts and it would be great of they could be shown in the same Window.
> 
> # Proposed technical implementation details (optional)
> 
> A Visual Studio template would be created to build a Windows Terminal Plugin.  It would have metadata that would specify commands that could be typed in the terminal that would be processed by Windows Terminal upon hitting Enter on the keyboard (and only when invoked from the keyboard) that if found would pass the entire command line to the Plugin.  Example:
> 
> !packages nuget list Microsoft.Extensions.*
> 
> This would invoke a plugin named "packages" which would in turn parse the command line and determine it needed to get a list of packages from nuget that match Microsoft.Extensions.* and display the results in the GUI.  It would only interact with the Profile it was invoked from.  
> 
> Other examples are accelerators for Dotnet core such that a Plugin could be open with a set of buttons for common actions/macros that the user may need.  This would be great for system admins who might administer 50 machines and run the same powershell commands on each of them.  Currently she would have to add aliases to profiles on potentially each machine.  No bueno!  This way she sets up some macros in a GUI, enables the plugin for that Profile and then can invoke her predetermined commands.
> 
> Another really useful plugin would be a Text Editor that could be used for profiles and scripts.  The data would be piped to/from the Console for reading and saving.  Another example is a file manager that could be used to move data between profiles over encrypted pipes.
> 
> Performing actions against a profile would always be done through the Plugin system.  Communication would be STDIN/STDOUT to the console.  A GUI could take the whole window or split the window vertically or horizontally and be resizable.  Plugins would be exposed through a Store and could be imported via nuget packages.

c66a56656 might be a commit that enables this, accidentally while working on something else.
\<showerthought>

what if we just had the Terminal create an HWND in the space of that pane, and let a OOP extension just get that HWND and put whatever it wants into it? Terminal would be responsible for resizing the HWND, hiding it, whatever. It'd be a child HWND of the Terminal, so it'd necessarily be tied to our window. But then a 3p app could just put a Xaml Island in it's _own_ child HWND, in its own process? 

```
Terminal 
└── Our Xaml Island
    └── HwndPane HWND
        └── Extension HWND
            └── Extension Xaml Island
```

Fix patch:
diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp
index 4a09828c680..d21497a51d9 100644
--- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp
+++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp
@@ -1326,7 +1326,7 @@ namespace TerminalAppLocalTests
             const auto& controlSettings = activeControl.Settings();
             VERIFY_IS_NOT_NULL(controlSettings);
 
-            VERIFY_ARE_EQUAL(til::color{ 0xff0c0c0c }, controlSettings.DefaultBackground());
+            VERIFY_ARE_EQUAL(til::color{ 0xff0c0c0c }, til::color{ controlSettings.DefaultBackground() });
         });
 
         TestOnUIThread([&page]() {
@@ -1344,7 +1344,7 @@ namespace TerminalAppLocalTests
             VERIFY_IS_NOT_NULL(controlSettings);
 
             Log::Comment(L"Color should be changed to the preview");
-            VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground());
+            VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, til::color{ controlSettings.DefaultBackground() });
 
             // And we should have stored a function to revert the change.
             VERIFY_ARE_EQUAL(1u, page->_restorePreviewFuncs.size());
@@ -1366,7 +1366,7 @@ namespace TerminalAppLocalTests
             VERIFY_IS_NOT_NULL(controlSettings);
 
             Log::Comment(L"Color should be changed");
-            VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground());
+            VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, til::color{ controlSettings.DefaultBackground() });
 
             // After preview there should be no more restore functions to execute.
             VERIFY_ARE_EQUAL(0u, page->_restorePreviewFuncs.size());
@@ -1394,7 +1394,7 @@ namespace TerminalAppLocalTests
             const auto& controlSettings = activeControl.Settings();
             VERIFY_IS_NOT_NULL(controlSettings);
 
-            VERIFY_ARE_EQUAL(til::color{ 0xff0c0c0c }, controlSettings.DefaultBackground());
+            VERIFY_ARE_EQUAL(til::color{ 0xff0c0c0c }, til::color{ controlSettings.DefaultBackground() });
         });
 
         TestOnUIThread([&page]() {
@@ -1412,7 +1412,7 @@ namespace TerminalAppLocalTests
             VERIFY_IS_NOT_NULL(controlSettings);
 
             Log::Comment(L"Color should be changed to the preview");
-            VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground());
+            VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, til::color{ controlSettings.DefaultBackground() });
         });
 
         TestOnUIThread([&page]() {
@@ -1428,7 +1428,7 @@ namespace TerminalAppLocalTests
             VERIFY_IS_NOT_NULL(controlSettings);
 
             Log::Comment(L"Color should be the same as it originally was");
-            VERIFY_ARE_EQUAL(til::color{ 0xff0c0c0c }, controlSettings.DefaultBackground());
+            VERIFY_ARE_EQUAL(til::color{ 0xff0c0c0c }, til::color{ controlSettings.DefaultBackground() });
         });
         Log::Comment(L"Sleep to let events propagate");
         Sleep(250);
@@ -1450,7 +1450,7 @@ namespace TerminalAppLocalTests
             const auto& controlSettings = activeControl.Settings();
             VERIFY_IS_NOT_NULL(controlSettings);
 
-            VERIFY_ARE_EQUAL(til::color{ 0xff0c0c0c }, controlSettings.DefaultBackground());
+            VERIFY_ARE_EQUAL(til::color{ 0xff0c0c0c }, til::color{ controlSettings.DefaultBackground() });
         });
 
         TestOnUIThread([&page]() {
@@ -1467,7 +1467,7 @@ namespace TerminalAppLocalTests
             VERIFY_IS_NOT_NULL(controlSettings);
 
             Log::Comment(L"Color should be changed to the preview");
-            VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, controlSettings.DefaultBackground());
+            VERIFY_ARE_EQUAL(til::color{ 0xff000000 }, til::color{ controlSettings.DefaultBackground() });
         });
 
         TestOnUIThread([&page]() {
@@ -1484,7 +1484,7 @@ namespace TerminalAppLocalTests
             VERIFY_IS_NOT_NULL(controlSettings);
 
             Log::Comment(L"Color should be changed to the preview");
-            VERIFY_ARE_EQUAL(til::color{ 0xffFAFAFA }, controlSettings.DefaultBackground());
+            VERIFY_ARE_EQUAL(til::color{ 0xffFAFAFA }, til::color{ controlSettings.DefaultBackground() });
         });
 
         TestOnUIThread([&page]() {
@@ -1503,7 +1503,7 @@ namespace TerminalAppLocalTests
             VERIFY_IS_NOT_NULL(controlSettings);
 
             Log::Comment(L"Color should be changed");
-            VERIFY_ARE_EQUAL(til::color{ 0xffFAFAFA }, controlSettings.DefaultBackground());
+            VERIFY_ARE_EQUAL(til::color{ 0xffFAFAFA }, til::color{ controlSettings.DefaultBackground() });
         });
         Log::Comment(L"Sleep to let events propagate");
         Sleep(250);
diff --git a/src/cascadia/LocalTests_TerminalApp/pch.h b/src/cascadia/LocalTests_TerminalApp/pch.h
index 25df5f47e9e..24162c0f936 100644
--- a/src/cascadia/LocalTests_TerminalApp/pch.h
+++ b/src/cascadia/LocalTests_TerminalApp/pch.h
@@ -56,6 +56,8 @@ Author(s):
 #include <winrt/windows.applicationmodel.core.h>
 
 #include <winrt/Microsoft.Terminal.TerminalConnection.h>
+#include <winrt/Microsoft.Terminal.Core.h>
+#include <winrt/Microsoft.Terminal.Control.h>
 #include <winrt/Microsoft.Terminal.Settings.Model.h>
 
 #include <winrt/Microsoft.UI.Xaml.Controls.h>
diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp
index 4e4f63d3676..df357864c3d 100644
--- a/src/cascadia/TerminalApp/AppActionHandlers.cpp
+++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp
@@ -1401,7 +1401,7 @@ namespace winrt::TerminalApp::implementation
         {
             if (const auto activePane{ activeTab->GetActivePane() })
             {
-                _restartPaneConnection(activePane);
+                _restartPaneConnection(activePane->GetContent().try_as<TerminalApp::TerminalPaneContent>(), nullptr);
             }
         }
         args.Handled(true);
diff --git a/src/cascadia/TerminalApp/IPaneContent.idl b/src/cascadia/TerminalApp/IPaneContent.idl
new file mode 100644
index 00000000000..ebe0914ea9c
--- /dev/null
+++ b/src/cascadia/TerminalApp/IPaneContent.idl
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+namespace TerminalApp
+{
+
+    runtimeclass BellEventArgs
+    {
+        Boolean FlashTaskbar { get; };
+    };
+
+    interface IPaneContent
+    {
+        Windows.UI.Xaml.FrameworkElement GetRoot();
+
+        Windows.Foundation.Size MinimumSize { get; };
+
+        String Title { get; };
+        UInt64 TaskbarState { get; };
+        UInt64 TaskbarProgress { get; };
+        Boolean ReadOnly { get; };
+
+        Microsoft.Terminal.Settings.Model.NewTerminalArgs GetNewTerminalArgs(Boolean asContent);
+
+        void Focus(Windows.UI.Xaml.FocusState reason);
+
+        void Close();
+
+        event Windows.Foundation.TypedEventHandler<IPaneContent, Object> CloseRequested;
+
+        event Windows.Foundation.TypedEventHandler<Object, Object> ConnectionStateChanged;
+        event Windows.Foundation.TypedEventHandler<IPaneContent, BellEventArgs> BellRequested;
+        event Windows.Foundation.TypedEventHandler<IPaneContent, Object> TitleChanged;
+        event Windows.Foundation.TypedEventHandler<IPaneContent, Object> TabColorChanged;
+        event Windows.Foundation.TypedEventHandler<IPaneContent, Object> TaskbarProgressChanged;
+        event Windows.Foundation.TypedEventHandler<IPaneContent, Object> ReadOnlyChanged;
+        event Windows.Foundation.TypedEventHandler<IPaneContent, Object> FocusRequested;
+    };
+
+
+    enum PaneSnapDirection
+    {
+        Width,
+        Height
+    };
+
+    interface ISnappable
+    {
+        Single SnapDownToGrid(PaneSnapDirection direction, Single sizeToSnap);
+        Windows.Foundation.Size GridUnitSize { get; };
+    };
+}
diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp
index 59988d928fc..9ed53d49204 100644
--- a/src/cascadia/TerminalApp/Pane.cpp
+++ b/src/cascadia/TerminalApp/Pane.cpp
@@ -3,6 +3,7 @@
 
 #include "pch.h"
 #include "Pane.h"
+
 #include "AppLogic.h"
 
 #include "Utils.h"
@@ -33,19 +34,21 @@ static const int CombinedPaneBorderSize = 2 * PaneBorderSize;
 static const int AnimationDurationInMilliseconds = 200;
 static const Duration AnimationDuration = DurationHelper::FromTimeSpan(winrt::Windows::Foundation::TimeSpan(std::chrono::milliseconds(AnimationDurationInMilliseconds)));
 
-Pane::Pane(const Profile& profile, const TermControl& control, const bool lastFocused) :
-    _control{ control },
-    _lastActive{ lastFocused },
-    _profile{ profile }
+Pane::Pane(const IPaneContent& content, const bool lastFocused) :
+    _content{ content },
+    _lastActive{ lastFocused }
 {
     _root.Children().Append(_borderFirst);
-    _borderFirst.Child(_control);
 
-    _setupControlEvents();
+    const auto& control{ _content.GetRoot() };
+    _borderFirst.Child(control);
 
     // Register an event with the control to have it inform us when it gains focus.
-    _gotFocusRevoker = _control.GotFocus(winrt::auto_revoke, { this, &Pane::_ControlGotFocusHandler });
-    _lostFocusRevoker = _control.LostFocus(winrt::auto_revoke, { this, &Pane::_ControlLostFocusHandler });
+    if (control)
+    {
+        _gotFocusRevoker = control.GotFocus(winrt::auto_revoke, { this, &Pane::_ContentGotFocusHandler });
+        _lostFocusRevoker = control.LostFocus(winrt::auto_revoke, { this, &Pane::_ContentLostFocusHandler });
+    }
 
     // When our border is tapped, make sure to transfer focus to our control.
     // LOAD-BEARING: This will NOT work if the border's BorderBrush is set to
@@ -102,19 +105,6 @@ Pane::Pane(std::shared_ptr<Pane> first,
     });
 }
 
-void Pane::_setupControlEvents()
-{
-    _controlEvents._ConnectionStateChanged = _control.ConnectionStateChanged(winrt::auto_revoke, { this, &Pane::_ControlConnectionStateChangedHandler });
-    _controlEvents._WarningBell = _control.WarningBell(winrt::auto_revoke, { this, &Pane::_ControlWarningBellHandler });
-    _controlEvents._CloseTerminalRequested = _control.CloseTerminalRequested(winrt::auto_revoke, { this, &Pane::_CloseTerminalRequestedHandler });
-    _controlEvents._RestartTerminalRequested = _control.RestartTerminalRequested(winrt::auto_revoke, { this, &Pane::_RestartTerminalRequestedHandler });
-    _controlEvents._ReadOnlyChanged = _control.ReadOnlyChanged(winrt::auto_revoke, { this, &Pane::_ControlReadOnlyChangedHandler });
-}
-void Pane::_removeControlEvents()
-{
-    _controlEvents = {};
-}
-
 // Method Description:
 // - Extract the terminal settings from the current (leaf) pane's control
 //   to be used to create an equivalent control
@@ -129,55 +119,7 @@ NewTerminalArgs Pane::GetTerminalArgsForPane(const bool asContent) const
     // Leaves are the only things that have controls
     assert(_IsLeaf());
 
-    NewTerminalArgs args{};
-    auto controlSettings = _control.Settings();
-
-    args.Profile(controlSettings.ProfileName());
-    // If we know the user's working directory use it instead of the profile.
-    if (const auto dir = _control.WorkingDirectory(); !dir.empty())
-    {
-        args.StartingDirectory(dir);
-    }
-    else
-    {
-        args.StartingDirectory(controlSettings.StartingDirectory());
-    }
-    args.TabTitle(controlSettings.StartingTitle());
-    args.Commandline(controlSettings.Commandline());
-    args.SuppressApplicationTitle(controlSettings.SuppressApplicationTitle());
-    if (controlSettings.TabColor() || controlSettings.StartingTabColor())
-    {
-        til::color c;
-        // StartingTabColor is prioritized over other colors
-        if (const auto color = controlSettings.StartingTabColor())
-        {
-            c = til::color(color.Value());
-        }
-        else
-        {
-            c = til::color(controlSettings.TabColor().Value());
-        }
-
-        args.TabColor(winrt::Windows::Foundation::IReference<winrt::Windows::UI::Color>{ static_cast<winrt::Windows::UI::Color>(c) });
-    }
-
-    // TODO:GH#9800 - we used to be able to persist the color scheme that a
-    // TermControl was initialized with, by name. With the change to having the
-    // control own its own copy of its settings, this isn't possible anymore.
-    //
-    // We may be able to get around this by storing the Name in the Core::Scheme
-    // object. That would work for schemes set by the Terminal, but not ones set
-    // by VT, but that seems good enough.
-
-    // Only fill in the ContentId if absolutely needed. If you fill in a number
-    // here (even 0), we'll serialize that number, AND treat that action as an
-    // "attach existing" rather than a "create"
-    if (asContent)
-    {
-        args.ContentId(_control.ContentId());
-    }
-
-    return args;
+    return _content.GetNewTerminalArgs(asContent);
 }
 
 // Method Description:
@@ -1022,181 +964,18 @@ Pane::PaneNeighborSearch Pane::_FindPaneAndNeighbor(const std::shared_ptr<Pane>
 }
 
 // Method Description:
-// - Called when our attached control is closed. Triggers listeners to our close
-//   event, if we're a leaf pane.
-// - If this was called, and we became a parent pane (due to work on another
-//   thread), this function will do nothing (allowing the control's new parent
-//   to handle the event instead).
+// - Returns true if the connection state of this pane is closed.
 // Arguments:
 // - <none>
 // Return Value:
-// - <none>
-winrt::fire_and_forget Pane::_ControlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender,
-                                                                   const winrt::Windows::Foundation::IInspectable& /*args*/)
-{
-    auto newConnectionState = ConnectionState::Closed;
-    if (const auto coreState = sender.try_as<ICoreState>())
-    {
-        newConnectionState = coreState.ConnectionState();
-    }
-
-    const auto previousConnectionState = std::exchange(_connectionState, newConnectionState);
-    if (newConnectionState < ConnectionState::Closed)
-    {
-        // Pane doesn't care if the connection isn't entering a terminal state.
-        co_return;
-    }
-
-    const auto weakThis = weak_from_this();
-    co_await wil::resume_foreground(_root.Dispatcher());
-    const auto strongThis = weakThis.lock();
-    if (!strongThis)
-    {
-        co_return;
-    }
-
-    // It's possible that this event handler started being executed, scheduled
-    // on the UI thread, another child got created. So our control is
-    // actually no longer _our_ control, and instead could be a descendant.
-    //
-    // When the control's new Pane takes ownership of the control, the new
-    // parent will register its own event handler. That event handler will get
-    // fired after this handler returns, and will properly cleanup state.
-    if (!_IsLeaf())
-    {
-        co_return;
-    }
-
-    if (previousConnectionState < ConnectionState::Connected && newConnectionState >= ConnectionState::Failed)
-    {
-        // A failure to complete the connection (before it has _connected_) is not covered by "closeOnExit".
-        // This is to prevent a misconfiguration (closeOnExit: always, startingDirectory: garbage) resulting
-        // in Terminal flashing open and immediately closed.
-        co_return;
-    }
-
-    if (_profile)
-    {
-        if (_isDefTermSession && _profile.CloseOnExit() == CloseOnExitMode::Automatic)
-        {
-            // For 'automatic', we only care about the connection state if we were launched by Terminal
-            // Since we were launched via defterm, ignore the connection state (i.e. we treat the
-            // close on exit mode as 'always', see GH #13325 for discussion)
-            Close();
-        }
-
-        const auto mode = _profile.CloseOnExit();
-        if ((mode == CloseOnExitMode::Always) ||
-            ((mode == CloseOnExitMode::Graceful || mode == CloseOnExitMode::Automatic) && newConnectionState == ConnectionState::Closed))
-        {
-            Close();
-        }
-    }
-}
-
-void Pane::_CloseTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
-                                          const winrt::Windows::Foundation::IInspectable& /*args*/)
-{
-    // It's possible that this event handler started being executed, then before
-    // we got the lock, another thread created another child. So our control is
-    // actually no longer _our_ control, and instead could be a descendant.
-    //
-    // When the control's new Pane takes ownership of the control, the new
-    // parent will register its own event handler. That event handler will get
-    // fired after this handler returns, and will properly cleanup state.
-    if (!_IsLeaf())
-    {
-        return;
-    }
-
-    Close();
-}
-
-void Pane::_RestartTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
-                                            const winrt::Windows::Foundation::IInspectable& /*args*/)
-{
-    if (!_IsLeaf())
-    {
-        return;
-    }
-    RestartTerminalRequested.raise(shared_from_this());
-}
-
-winrt::fire_and_forget Pane::_playBellSound(winrt::Windows::Foundation::Uri uri)
-{
-    auto weakThis{ weak_from_this() };
-
-    co_await wil::resume_foreground(_root.Dispatcher());
-    if (auto pane{ weakThis.lock() })
-    {
-        if (!_bellPlayerCreated)
-        {
-            // The MediaPlayer might not exist on Windows N SKU.
-            try
-            {
-                _bellPlayerCreated = true;
-                _bellPlayer = winrt::Windows::Media::Playback::MediaPlayer();
-                // GH#12258: The media keys (like play/pause) should have no effect on our bell sound.
-                _bellPlayer.CommandManager().IsEnabled(false);
-            }
-            CATCH_LOG();
-        }
-        if (_bellPlayer)
-        {
-            const auto source{ winrt::Windows::Media::Core::MediaSource::CreateFromUri(uri) };
-            const auto item{ winrt::Windows::Media::Playback::MediaPlaybackItem(source) };
-            _bellPlayer.Source(item);
-            _bellPlayer.Play();
-        }
-    }
-}
-
-// Method Description:
-// - Plays a warning note when triggered by the BEL control character,
-//   using the sound configured for the "Critical Stop" system event.`
-//   This matches the behavior of the Windows Console host.
-// - Will also flash the taskbar if the bellStyle setting for this profile
-//   has the 'visual' flag set
-// Arguments:
-// - <unused>
-void Pane::_ControlWarningBellHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
-                                      const winrt::Windows::Foundation::IInspectable& /*eventArgs*/)
+// - true if the connection state of this Pane is closed.
+bool Pane::IsConnectionClosed() const
 {
-    if (!_IsLeaf())
-    {
-        return;
-    }
-    if (_profile)
+    if (const auto& control{ GetTerminalControl() })
     {
-        // We don't want to do anything if nothing is set, so check for that first
-        if (static_cast<int>(_profile.BellStyle()) != 0)
-        {
-            if (WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Audible))
-            {
-                // Audible is set, play the sound
-                auto sounds{ _profile.BellSound() };
-                if (sounds && sounds.Size() > 0)
-                {
-                    winrt::hstring soundPath{ wil::ExpandEnvironmentStringsW<std::wstring>(sounds.GetAt(rand() % sounds.Size()).c_str()) };
-                    winrt::Windows::Foundation::Uri uri{ soundPath };
-                    _playBellSound(uri);
-                }
-                else
-                {
-                    const auto soundAlias = reinterpret_cast<LPCTSTR>(SND_ALIAS_SYSTEMHAND);
-                    PlaySound(soundAlias, NULL, SND_ALIAS_ID | SND_ASYNC | SND_SENTRY);
-                }
-            }
-
-            if (WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Window))
-            {
-                _control.BellLightOn();
-            }
-
-            // raise the event with the bool value corresponding to the taskbar flag
-            PaneRaiseBell.raise(nullptr, WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Taskbar));
-        }
+        return control.ConnectionState() >= ConnectionState::Closed;
     }
+    return false;
 }
 
 // Event Description:
@@ -1207,7 +986,7 @@ void Pane::_ControlWarningBellHandler(const winrt::Windows::Foundation::IInspect
 // - <unused>
 // Return Value:
 // - <none>
-void Pane::_ControlGotFocusHandler(const winrt::Windows::Foundation::IInspectable& sender,
+void Pane::_ContentGotFocusHandler(const winrt::Windows::Foundation::IInspectable& sender,
                                    const RoutedEventArgs& /* args */)
 {
     auto f = FocusState::Programmatic;
@@ -1222,7 +1001,7 @@ void Pane::_ControlGotFocusHandler(const winrt::Windows::Foundation::IInspectabl
 // - Called when our control loses focus. We'll use this to trigger our LostFocus
 //   callback. The tab that's hosting us should have registered a callback which
 //   can be used to update its own internal focus state
-void Pane::_ControlLostFocusHandler(const winrt::Windows::Foundation::IInspectable& /* sender */,
+void Pane::_ContentLostFocusHandler(const winrt::Windows::Foundation::IInspectable& /* sender */,
                                     const RoutedEventArgs& /* args */)
 {
     LostFocus.raise(shared_from_this());
@@ -1245,21 +1024,9 @@ void Pane::Close()
 //   and connections beneath it.
 void Pane::Shutdown()
 {
-    // Clear out our media player callbacks, and stop any playing media. This
-    // will prevent the callback from being triggered after we've closed, and
-    // also make sure that our sound stops when we're closed.
-    if (_bellPlayer)
-    {
-        _bellPlayer.Pause();
-        _bellPlayer.Source(nullptr);
-        _bellPlayer.Close();
-        _bellPlayer = nullptr;
-        _bellPlayerCreated = false;
-    }
-
     if (_IsLeaf())
     {
-        _control.Close();
+        _content.Close();
     }
     else
     {
@@ -1314,7 +1081,7 @@ TermControl Pane::GetLastFocusedTerminalControl()
             {
                 if (p->_IsLeaf())
                 {
-                    return p->_control;
+                    return p->GetTerminalControl();
                 }
                 pane = p;
             }
@@ -1322,7 +1089,8 @@ TermControl Pane::GetLastFocusedTerminalControl()
         }
         return _firstChild->GetLastFocusedTerminalControl();
     }
-    return _control;
+    // we _are_ a leaf.
+    return GetTerminalControl();
 }
 
 // Method Description:
@@ -1332,9 +1100,16 @@ TermControl Pane::GetLastFocusedTerminalControl()
 // - <none>
 // Return Value:
 // - nullptr if this Pane is a parent, otherwise the TermControl of this Pane.
-TermControl Pane::GetTerminalControl()
+TermControl Pane::GetTerminalControl() const
 {
-    return _IsLeaf() ? _control : nullptr;
+    if (const auto& terminalPane{ _getTerminalContent() })
+    {
+        return terminalPane.GetTermControl();
+    }
+    else
+    {
+        return nullptr;
+    }
 }
 
 // Method Description:
@@ -1381,19 +1156,11 @@ void Pane::SetActive()
 Profile Pane::GetFocusedProfile()
 {
     auto lastFocused = GetActivePane();
-    return lastFocused ? lastFocused->_profile : nullptr;
-}
-
-// Method Description:
-// - Returns true if the connection state of this pane is closed. If this Pane is not a leaf this will
-//   return false.
-// Arguments:
-// - <none>
-// Return Value:
-// - true if the connection state of this Pane is closed.
-bool Pane::IsConnectionClosed() const
-{
-    return _control && _control.ConnectionState() >= ConnectionState::Closed;
+    if (const auto& terminalPane{ lastFocused->_getTerminalContent() })
+    {
+        return terminalPane.GetProfile();
+    }
+    return nullptr;
 }
 
 // Method Description:
@@ -1466,10 +1233,7 @@ void Pane::UpdateVisuals()
 void Pane::_Focus()
 {
     GotFocus.raise(shared_from_this(), FocusState::Programmatic);
-    if (const auto& control = GetLastFocusedTerminalControl())
-    {
-        control.Focus(FocusState::Programmatic);
-    }
+    _content.Focus(FocusState::Programmatic);
 }
 
 // Method Description:
@@ -1519,9 +1283,10 @@ void Pane::UpdateSettings(const TerminalSettingsCreateResult& settings, const Pr
 {
     assert(_IsLeaf());
 
-    _profile = profile;
-
-    _control.UpdateControlSettings(settings.DefaultSettings(), settings.UnfocusedSettings());
+    if (const auto& terminalPane{ _getTerminalContent() })
+    {
+        return terminalPane.UpdateSettings(settings, profile);
+    }
 }
 
 // Method Description:
@@ -1573,7 +1338,7 @@ std::shared_ptr<Pane> Pane::DetachPane(std::shared_ptr<Pane> pane)
         auto detached = isFirstChild ? _firstChild : _secondChild;
         // Remove the child from the tree, replace the current node with the
         // other child.
-        _CloseChild(isFirstChild, true);
+        _CloseChild(isFirstChild);
 
         // Update the borders on this pane and any children to match if we have
         // no parent.
@@ -1602,12 +1367,9 @@ std::shared_ptr<Pane> Pane::DetachPane(std::shared_ptr<Pane> pane)
 // Arguments:
 // - closeFirst: if true, the first child should be closed, and the second
 //   should be preserved, and vice-versa for false.
-// - isDetaching: if true, then the pane event handlers for the closed child
-//   should be kept, this way they don't have to be recreated when it is later
-//   reattached to a tree somewhere as the control moves with the pane.
 // Return Value:
 // - <none>
-void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
+void Pane::_CloseChild(const bool closeFirst)
 {
     // If we're a leaf, then chances are both our children closed in close
     // succession. We waited on the lock while the other child was closed, so
@@ -1643,35 +1405,16 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
         _borders = _GetCommonBorders();
 
         // take the control, profile, id and isDefTermSession of the pane that _wasn't_ closed.
-        _control = remainingChild->_control;
-        _connectionState = remainingChild->_connectionState;
-        _profile = remainingChild->_profile;
+        _content = remainingChild->_content;
         _id = remainingChild->Id();
-        _isDefTermSession = remainingChild->_isDefTermSession;
-
-        // Add our new event handler before revoking the old one.
-        _setupControlEvents();
 
         // Revoke the old event handlers. Remove both the handlers for the panes
         // themselves closing, and remove their handlers for their controls
         // closing. At this point, if the remaining child's control is closed,
         // they'll trigger only our event handler for the control's close.
 
-        // However, if we are detaching the pane we want to keep its control
-        // handlers since it is just getting moved.
-        if (!isDetaching)
-        {
-            closedChild->WalkTree([](auto p) {
-                if (p->_IsLeaf())
-                {
-                    p->_removeControlEvents();
-                }
-            });
-        }
-
         closedChild->Closed(closedChildClosedToken);
         remainingChild->Closed(remainingChildClosedToken);
-        remainingChild->_removeControlEvents();
 
         // If we or either of our children was focused, we want to take that
         // focus from them.
@@ -1691,7 +1434,8 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
 
         // Reattach the TermControl to our grid.
         _root.Children().Append(_borderFirst);
-        _borderFirst.Child(_control);
+        const auto& control{ _content.GetRoot() };
+        _borderFirst.Child(control);
 
         // Make sure to set our _splitState before focusing the control. If you
         // fail to do this, when the tab handles the GotFocus event and asks us
@@ -1700,14 +1444,17 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
         _splitState = SplitState::None;
 
         // re-attach our handler for the control's GotFocus event.
-        _gotFocusRevoker = _control.GotFocus(winrt::auto_revoke, { this, &Pane::_ControlGotFocusHandler });
-        _lostFocusRevoker = _control.LostFocus(winrt::auto_revoke, { this, &Pane::_ControlLostFocusHandler });
+        if (control)
+        {
+            _gotFocusRevoker = control.GotFocus(winrt::auto_revoke, { this, &Pane::_ContentGotFocusHandler });
+            _lostFocusRevoker = control.LostFocus(winrt::auto_revoke, { this, &Pane::_ContentLostFocusHandler });
+        }
 
         // If we're inheriting the "last active" state from one of our children,
         // focus our control now. This should trigger our own GotFocus event.
         if (usedToFocusClosedChildsTerminal || _lastActive)
         {
-            _control.Focus(FocusState::Programmatic);
+            _content.Focus(FocusState::Programmatic);
 
             // See GH#7252
             // Manually fire off the GotFocus event. Typically, this is done
@@ -1746,15 +1493,6 @@ void Pane::_CloseChild(const bool closeFirst, const bool isDetaching)
         // Remove the event handlers on the old children
         remainingChild->Closed(remainingChildClosedToken);
         closedChild->Closed(closedChildClosedToken);
-        if (!isDetaching)
-        {
-            closedChild->WalkTree([](auto p) {
-                if (p->_IsLeaf())
-                {
-                    p->_removeControlEvents();
-                }
-            });
-        }
 
         // Reset our UI:
         _root.Children().Clear();
@@ -1847,7 +1585,7 @@ void Pane::_CloseChildRoutine(const bool closeFirst)
     // this one doesn't seem to.
     if (!animationsEnabledInOS || !animationsEnabledInApp || eitherChildZoomed)
     {
-        _CloseChild(closeFirst, false);
+        _CloseChild(closeFirst);
         return;
     }
 
@@ -1950,7 +1688,7 @@ void Pane::_CloseChildRoutine(const bool closeFirst)
         {
             // We don't need to manually undo any of the above trickiness.
             // We're going to re-parent the child's content into us anyways
-            pane->_CloseChild(closeFirst, false);
+            pane->_CloseChild(closeFirst);
         }
     });
 }
@@ -2173,7 +1911,7 @@ void Pane::_SetupEntranceAnimation()
         auto child = isFirstChild ? _firstChild : _secondChild;
         auto childGrid = child->_root;
         // If we are splitting a parent pane this may be null
-        auto control = child->_control;
+        auto control = child->_content.GetRoot();
         // Build up our animation:
         // * it'll take as long as our duration (200ms)
         // * it'll change the value of our property from 0 to secondSize
@@ -2494,9 +2232,6 @@ std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::_Split(SplitDirect
 
     if (_IsLeaf())
     {
-        // revoke our handler - the child will take care of the control now.
-        _removeControlEvents();
-
         // Remove our old GotFocus handler from the control. We don't want the
         // control telling us that it's now focused, we want it telling its new
         // parent.
@@ -2525,11 +2260,8 @@ std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::_Split(SplitDirect
     else
     {
         //   Move our control, guid, isDefTermSession into the first one.
-        _firstChild = std::make_shared<Pane>(_profile, _control);
-        _firstChild->_connectionState = std::exchange(_connectionState, ConnectionState::NotConnected);
-        _profile = nullptr;
-        _control = { nullptr };
-        _firstChild->_isDefTermSession = _isDefTermSession;
+        _firstChild = std::make_shared<Pane>(_content);
+        _content = nullptr;
         _firstChild->_broadcastEnabled = _broadcastEnabled;
     }
 
@@ -2852,8 +2584,16 @@ float Pane::CalcSnappedDimension(const bool widthOrHeight, const float dimension
 //   If requested size is already snapped, then both returned values equal this value.
 Pane::SnapSizeResult Pane::_CalcSnappedDimension(const bool widthOrHeight, const float dimension) const
 {
+    const auto direction{ widthOrHeight ? PaneSnapDirection::Width : PaneSnapDirection::Height };
+
     if (_IsLeaf())
     {
+        const auto& snappable{ _content.try_as<ISnappable>() };
+        if (!snappable)
+        {
+            return { dimension, dimension };
+        }
+
         // If we're a leaf pane, align to the grid of controlling terminal
 
         const auto minSize = _GetMinSize();
@@ -2864,8 +2604,10 @@ Pane::SnapSizeResult Pane::_CalcSnappedDimension(const bool widthOrHeight, const
             return { minDimension, minDimension };
         }
 
-        auto lower = _control.SnapDimensionToGrid(widthOrHeight, dimension);
-        if (widthOrHeight)
+        auto lower = snappable.SnapDownToGrid(widthOrHeight ? PaneSnapDirection::Width : PaneSnapDirection::Height,
+                                              dimension);
+
+        if (direction == PaneSnapDirection::Width)
         {
             lower += WI_IsFlagSet(_borders, Borders::Left) ? PaneBorderSize : 0;
             lower += WI_IsFlagSet(_borders, Borders::Right) ? PaneBorderSize : 0;
@@ -2884,8 +2626,10 @@ Pane::SnapSizeResult Pane::_CalcSnappedDimension(const bool widthOrHeight, const
         }
         else
         {
-            const auto cellSize = _control.CharacterDimensions();
-            const auto higher = lower + (widthOrHeight ? cellSize.Width : cellSize.Height);
+            const auto cellSize = snappable.GridUnitSize();
+            const auto higher = lower + (direction == PaneSnapDirection::Width ?
+                                             cellSize.Width :
+                                             cellSize.Height);
             return { lower, higher };
         }
     }
@@ -2931,21 +2675,34 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si
 {
     if (_IsLeaf())
     {
-        // We're a leaf pane, so just add one more row or column (unless isMinimumSize
-        // is true, see below).
-
-        if (sizeNode.isMinimumSize)
+        const auto& snappable{ _content.try_as<ISnappable>() };
+        if (snappable)
         {
-            // If the node is of its minimum size, this size might not be snapped (it might
-            // be, say, half a character, or fixed 10 pixels), so snap it upward. It might
-            // however be already snapped, so add 1 to make sure it really increases
-            // (not strictly necessary but to avoid surprises).
-            sizeNode.size = _CalcSnappedDimension(widthOrHeight, sizeNode.size + 1).higher;
+            // We're a leaf pane, so just add one more row or column (unless isMinimumSize
+            // is true, see below).
+
+            if (sizeNode.isMinimumSize)
+            {
+                // If the node is of its minimum size, this size might not be snapped (it might
+                // be, say, half a character, or fixed 10 pixels), so snap it upward. It might
+                // however be already snapped, so add 1 to make sure it really increases
+                // (not strictly necessary but to avoid surprises).
+                sizeNode.size = _CalcSnappedDimension(widthOrHeight, sizeNode.size + 1).higher;
+            }
+            else
+            {
+                const auto cellSize = snappable.GridUnitSize();
+                sizeNode.size += widthOrHeight ? cellSize.Width : cellSize.Height;
+            }
         }
         else
         {
-            const auto cellSize = _control.CharacterDimensions();
-            sizeNode.size += widthOrHeight ? cellSize.Width : cellSize.Height;
+            // If we're a leaf that didn't have a TermControl, then just increment
+            // by one. We have to increment by _some_ value, because this is used in
+            // a while() loop to find the next bigger size we can snap to. But since
+            // a non-terminal control doesn't really care what size it's snapped to,
+            // we can just say "one pixel larger is the next snap point"
+            sizeNode.size += 1;
         }
     }
     else
@@ -3050,7 +2807,7 @@ Size Pane::_GetMinSize() const
 {
     if (_IsLeaf())
     {
-        auto controlSize = _control.MinimumSize();
+        auto controlSize = _content.MinimumSize();
         auto newWidth = controlSize.Width;
         auto newHeight = controlSize.Height;
 
@@ -3148,14 +2905,17 @@ int Pane::GetLeafPaneCount() const noexcept
 //   created via default handoff
 void Pane::FinalizeConfigurationGivenDefault()
 {
-    _isDefTermSession = true;
+    if (const auto& terminalPane{ _content.try_as<TerminalPaneContent>() })
+    {
+        terminalPane.MarkAsDefterm();
+    }
 }
 
 // Method Description:
 // - Returns true if the pane or one of its descendants is read-only
 bool Pane::ContainsReadOnly() const
 {
-    return _IsLeaf() ? _control.ReadOnly() : (_firstChild->ContainsReadOnly() || _secondChild->ContainsReadOnly());
+    return _IsLeaf() ? _content.ReadOnly() : (_firstChild->ContainsReadOnly() || _secondChild->ContainsReadOnly());
 }
 
 // Method Description:
@@ -3170,8 +2930,8 @@ void Pane::CollectTaskbarStates(std::vector<winrt::TerminalApp::TaskbarState>& s
 {
     if (_IsLeaf())
     {
-        auto tbState{ winrt::make<winrt::TerminalApp::implementation::TaskbarState>(_control.TaskbarState(),
-                                                                                    _control.TaskbarProgress()) };
+        auto tbState{ winrt::make<winrt::TerminalApp::implementation::TaskbarState>(_content.TaskbarState(),
+                                                                                    _content.TaskbarProgress()) };
         states.push_back(tbState);
     }
     else
@@ -3186,9 +2946,12 @@ void Pane::EnableBroadcast(bool enabled)
     if (_IsLeaf())
     {
         _broadcastEnabled = enabled;
-        _control.CursorVisibility(enabled ?
-                                      CursorDisplayState::Shown :
-                                      CursorDisplayState::Default);
+        if (const auto& termControl{ GetTerminalControl() })
+        {
+            termControl.CursorVisibility(enabled ?
+                                             CursorDisplayState::Shown :
+                                             CursorDisplayState::Default);
+        }
         UpdateVisuals();
     }
     else
@@ -3205,9 +2968,12 @@ void Pane::BroadcastKey(const winrt::Microsoft::Terminal::Control::TermControl&
                         const bool keyDown)
 {
     WalkTree([&](const auto& pane) {
-        if (pane->_IsLeaf() && pane->_control != sourceControl && !pane->_control.ReadOnly())
+        if (const auto& termControl{ pane->GetTerminalControl() })
         {
-            pane->_control.RawWriteKeyEvent(vkey, scanCode, modifiers, keyDown);
+            if (termControl != sourceControl && !termControl.ReadOnly())
+            {
+                termControl.RawWriteKeyEvent(vkey, scanCode, modifiers, keyDown);
+            }
         }
     });
 }
@@ -3218,9 +2984,12 @@ void Pane::BroadcastChar(const winrt::Microsoft::Terminal::Control::TermControl&
                          const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers)
 {
     WalkTree([&](const auto& pane) {
-        if (pane->_IsLeaf() && pane->_control != sourceControl && !pane->_control.ReadOnly())
+        if (const auto& termControl{ pane->GetTerminalControl() })
         {
-            pane->_control.RawWriteChar(character, scanCode, modifiers);
+            if (termControl != sourceControl && !termControl.ReadOnly())
+            {
+                termControl.RawWriteChar(character, scanCode, modifiers);
+            }
         }
     });
 }
@@ -3229,19 +2998,16 @@ void Pane::BroadcastString(const winrt::Microsoft::Terminal::Control::TermContro
                            const winrt::hstring& text)
 {
     WalkTree([&](const auto& pane) {
-        if (pane->_IsLeaf() && pane->_control != sourceControl && !pane->_control.ReadOnly())
+        if (const auto& termControl{ pane->GetTerminalControl() })
         {
-            pane->_control.RawWriteString(text);
+            if (termControl != sourceControl && !termControl.ReadOnly())
+            {
+                termControl.RawWriteString(text);
+            }
         }
     });
 }
 
-void Pane::_ControlReadOnlyChangedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
-                                          const winrt::Windows::Foundation::IInspectable& /*e*/)
-{
-    UpdateVisuals();
-}
-
 winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::_ComputeBorderColor()
 {
     if (_lastActive)
@@ -3249,7 +3015,7 @@ winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::_ComputeBorderColor()
         return _themeResources.focusedBorderBrush;
     }
 
-    if (_broadcastEnabled && (_IsLeaf() && !_control.ReadOnly()))
+    if (_broadcastEnabled && (_IsLeaf() && !_content.ReadOnly()))
     {
         return _themeResources.broadcastBorderBrush;
     }
diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h
index e7660d4b70c..1f237f83190 100644
--- a/src/cascadia/TerminalApp/Pane.h
+++ b/src/cascadia/TerminalApp/Pane.h
@@ -21,6 +21,7 @@
 #pragma once
 
 #include "TaskbarState.h"
+#include "TerminalPaneContent.h"
 
 // fwdecl unittest classes
 namespace TerminalAppLocalTests
@@ -61,8 +62,7 @@ struct PaneResources
 class Pane : public std::enable_shared_from_this<Pane>
 {
 public:
-    Pane(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile,
-         const winrt::Microsoft::Terminal::Control::TermControl& control,
+    Pane(const winrt::TerminalApp::IPaneContent& content,
          const bool lastFocused = false);
 
     Pane(std::shared_ptr<Pane> first,
@@ -73,7 +73,7 @@ class Pane : public std::enable_shared_from_this<Pane>
 
     std::shared_ptr<Pane> GetActivePane();
     winrt::Microsoft::Terminal::Control::TermControl GetLastFocusedTerminalControl();
-    winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl();
+    winrt::Microsoft::Terminal::Control::TermControl GetTerminalControl() const;
     winrt::Microsoft::Terminal::Settings::Model::Profile GetFocusedProfile();
     bool IsConnectionClosed() const;
 
@@ -82,10 +82,15 @@ class Pane : public std::enable_shared_from_this<Pane>
     // - If this is a branch/root pane, return nullptr.
     winrt::Microsoft::Terminal::Settings::Model::Profile GetProfile() const
     {
-        return _profile;
+        if (const auto& c{ _content.try_as<winrt::TerminalApp::TerminalPaneContent>() })
+        {
+            return c.GetProfile();
+        }
+        return nullptr;
     }
 
     winrt::Windows::UI::Xaml::Controls::Grid GetRootElement();
+    winrt::TerminalApp::IPaneContent GetContent() const noexcept { return _IsLeaf() ? _content : nullptr; }
 
     bool WasLastFocused() const noexcept;
     void UpdateVisuals();
@@ -217,9 +222,7 @@ class Pane : public std::enable_shared_from_this<Pane>
 
     til::event<gotFocusArgs> GotFocus;
     til::event<winrt::delegate<std::shared_ptr<Pane>>> LostFocus;
-    til::event<winrt::Windows::Foundation::EventHandler<bool>> PaneRaiseBell;
     til::event<winrt::delegate<std::shared_ptr<Pane>>> Detached;
-    til::event<winrt::delegate<std::shared_ptr<Pane>>> RestartTerminalRequested;
 
 private:
     struct PanePoint;
@@ -239,10 +242,8 @@ class Pane : public std::enable_shared_from_this<Pane>
     std::shared_ptr<Pane> _secondChild{ nullptr };
     SplitState _splitState{ SplitState::None };
     float _desiredSplitPosition;
-    winrt::Microsoft::Terminal::Control::TermControl _control{ nullptr };
-    winrt::Microsoft::Terminal::TerminalConnection::ConnectionState _connectionState{ winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::NotConnected };
-    winrt::Microsoft::Terminal::Settings::Model::Profile _profile{ nullptr };
-    bool _isDefTermSession{ false };
+
+    winrt::TerminalApp::IPaneContent _content{ nullptr };
 #pragma endregion
 
     std::optional<uint32_t> _id;
@@ -252,17 +253,6 @@ class Pane : public std::enable_shared_from_this<Pane>
     winrt::event_token _firstClosedToken{ 0 };
     winrt::event_token _secondClosedToken{ 0 };
 
-    struct ControlEventTokens
-    {
-        winrt::Microsoft::Terminal::Control::TermControl::ConnectionStateChanged_revoker _ConnectionStateChanged;
-        winrt::Microsoft::Terminal::Control::TermControl::WarningBell_revoker _WarningBell;
-        winrt::Microsoft::Terminal::Control::TermControl::CloseTerminalRequested_revoker _CloseTerminalRequested;
-        winrt::Microsoft::Terminal::Control::TermControl::RestartTerminalRequested_revoker _RestartTerminalRequested;
-        winrt::Microsoft::Terminal::Control::TermControl::ReadOnlyChanged_revoker _ReadOnlyChanged;
-    } _controlEvents;
-    void _setupControlEvents();
-    void _removeControlEvents();
-
     winrt::Windows::UI::Xaml::UIElement::GotFocus_revoker _gotFocusRevoker;
     winrt::Windows::UI::Xaml::UIElement::LostFocus_revoker _lostFocusRevoker;
 
@@ -271,13 +261,14 @@ class Pane : public std::enable_shared_from_this<Pane>
     bool _zoomed{ false };
     bool _broadcastEnabled{ false };
 
-    winrt::Windows::Media::Playback::MediaPlayer _bellPlayer{ nullptr };
-    bool _bellPlayerCreated{ false };
-
     bool _IsLeaf() const noexcept;
     bool _HasFocusedChild() const noexcept;
     void _SetupChildCloseHandlers();
     bool _HasChild(const std::shared_ptr<Pane> child);
+    winrt::TerminalApp::TerminalPaneContent _getTerminalContent() const
+    {
+        return _IsLeaf() ? _content.try_as<winrt::TerminalApp::TerminalPaneContent>() : nullptr;
+    }
 
     std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> _Split(winrt::Microsoft::Terminal::Settings::Model::SplitDirection splitType,
                                                                    const float splitSize,
@@ -303,24 +294,16 @@ class Pane : public std::enable_shared_from_this<Pane>
                                             const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction,
                                             const PanePoint offset);
 
-    void _CloseChild(const bool closeFirst, const bool isDetaching);
+    void _CloseChild(const bool closeFirst);
     void _CloseChildRoutine(const bool closeFirst);
 
     void _Focus();
     void _FocusFirstChild();
-    winrt::fire_and_forget _ControlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/);
-    void _ControlWarningBellHandler(const winrt::Windows::Foundation::IInspectable& sender,
-                                    const winrt::Windows::Foundation::IInspectable& e);
-    void _ControlGotFocusHandler(const winrt::Windows::Foundation::IInspectable& sender,
+    void _ContentGotFocusHandler(const winrt::Windows::Foundation::IInspectable& sender,
                                  const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
-    void _ControlLostFocusHandler(const winrt::Windows::Foundation::IInspectable& sender,
+    void _ContentLostFocusHandler(const winrt::Windows::Foundation::IInspectable& sender,
                                   const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
 
-    void _ControlReadOnlyChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& e);
-
-    void _CloseTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/);
-    void _RestartTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/);
-
     std::pair<float, float> _CalcChildrenSizes(const float fullSize) const;
     SnapChildrenSizeResult _CalcSnappedChildrenSizes(const bool widthOrHeight, const float fullSize) const;
     SnapSizeResult _CalcSnappedDimension(const bool widthOrHeight, const float dimension) const;
@@ -331,8 +314,6 @@ class Pane : public std::enable_shared_from_this<Pane>
 
     SplitState _convertAutomaticOrDirectionalSplitState(const winrt::Microsoft::Terminal::Settings::Model::SplitDirection& splitType) const;
 
-    winrt::fire_and_forget _playBellSound(winrt::Windows::Foundation::Uri uri);
-
     // Function Description:
     // - Returns true if the given direction can be used with the given split
     //   type.
diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp
index 2228f8234e9..b407c6107c1 100644
--- a/src/cascadia/TerminalApp/TabManagement.cpp
+++ b/src/cascadia/TerminalApp/TabManagement.cpp
@@ -63,7 +63,7 @@ namespace winrt::TerminalApp::implementation
     // - existingConnection: An optional connection that is already established to a PTY
     //   for this tab to host instead of creating one.
     //   If not defined, the tab will create the connection.
-    HRESULT TerminalPage::_OpenNewTab(const NewTerminalArgs& newTerminalArgs, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection)
+    HRESULT TerminalPage::_OpenNewTab(const NewTerminalArgs& newTerminalArgs)
     try
     {
         const auto profile{ _settings.GetProfileForArgs(newTerminalArgs) };
@@ -86,7 +86,7 @@ namespace winrt::TerminalApp::implementation
         //
         // This call to _MakePane won't return nullptr, we already checked that
         // case above with the _maybeElevate call.
-        _CreateNewTabFromPane(_MakePane(newTerminalArgs, nullptr, existingConnection));
+        _CreateNewTabFromPane(_MakePane(newTerminalArgs, nullptr));
         return S_OK;
     }
     CATCH_RETURN();
diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj
index af091aae03e..d6abba2db90 100644
--- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj
+++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj
@@ -158,6 +158,9 @@
     <ClInclude Include="SettingsLoadEventArgs.h">
       <DependentUpon>TerminalWindow.idl</DependentUpon>
     </ClInclude>
+    <ClInclude Include="TerminalPaneContent.h">
+      <DependentUpon>TerminalPaneContent.idl</DependentUpon>
+    </ClInclude>
     <ClInclude Include="Toast.h" />
     <ClInclude Include="SuggestionsControl.h">
       <DependentUpon>SuggestionsControl.xaml</DependentUpon>
@@ -262,6 +265,9 @@
     <ClCompile Include="TerminalWindow.cpp">
       <DependentUpon>TerminalWindow.idl</DependentUpon>
     </ClCompile>
+    <ClCompile Include="TerminalPaneContent.cpp">
+      <DependentUpon>TerminalPaneContent.idl</DependentUpon>
+    </ClCompile>
     <ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
     <ClCompile Include="Toast.cpp" />
     <ClCompile Include="SuggestionsControl.cpp">
@@ -334,6 +340,8 @@
       <SubType>Code</SubType>
     </Midl>
     <Midl Include="FilteredCommand.idl" />
+    <Midl Include="IPaneContent.idl" />
+    <Midl Include="TerminalPaneContent.idl" />
   </ItemGroup>
   <!-- ========================= Misc Files ======================== -->
   <ItemGroup>
diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp
index a46544ce60c..605110a0ad7 100644
--- a/src/cascadia/TerminalApp/TerminalPage.cpp
+++ b/src/cascadia/TerminalApp/TerminalPage.cpp
@@ -1297,15 +1297,20 @@ namespace winrt::TerminalApp::implementation
         return connection;
     }
 
-    TerminalConnection::ITerminalConnection TerminalPage::_duplicateConnectionForRestart(std::shared_ptr<Pane> pane)
+    TerminalConnection::ITerminalConnection TerminalPage::_duplicateConnectionForRestart(const TerminalApp::TerminalPaneContent& paneContent)
     {
-        const auto& control{ pane->GetTerminalControl() };
+        if (paneContent == nullptr)
+        {
+            return nullptr;
+        }
+
+        const auto& control{ paneContent.GetTermControl() };
         if (control == nullptr)
         {
             return nullptr;
         }
         const auto& connection = control.Connection();
-        auto profile{ pane->GetProfile() };
+        auto profile{ paneContent.GetProfile() };
 
         TerminalSettingsCreateResult controlSettings{ nullptr };
 
@@ -1724,11 +1729,8 @@ namespace winrt::TerminalApp::implementation
         // Add an event handler for when the terminal or tab wants to set a
         // progress indicator on the taskbar
         hostingTab.TaskbarProgressChanged({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler });
-    }
 
-    void TerminalPage::_RegisterPaneEvents(std::shared_ptr<Pane>& pane)
-    {
-        pane->RestartTerminalRequested({ get_weak(), &TerminalPage::_restartPaneConnection });
+        hostingTab.RestartTerminalRequested({ get_weak(), &TerminalPage::_restartPaneConnection });
     }
 
     // Method Description:
@@ -2377,13 +2379,6 @@ namespace winrt::TerminalApp::implementation
         _UnZoomIfNeeded();
         auto [original, _] = activeTab->SplitPane(*realSplitType, splitSize, newPane);
 
-        // When we split the pane, the Pane itself will create a _new_ Pane
-        // instance for the original content. We need to make sure we also
-        // re-add our event handler to that newly created pane.
-        //
-        // _MakePane will already call this for the newly created pane.
-        _RegisterPaneEvents(original);
-
         // After GH#6586, the control will no longer focus itself
         // automatically when it's finished being laid out. Manually focus
         // the control here instead.
@@ -3114,10 +3109,9 @@ namespace winrt::TerminalApp::implementation
             // Don't need to worry about duplicating or anything - we'll
             // serialize the actual profile's GUID along with the content guid.
             const auto& profile = _settings.GetProfileForArgs(newTerminalArgs);
-
             const auto control = _AttachControlToContent(newTerminalArgs.ContentId());
-
-            return std::make_shared<Pane>(profile, control);
+            auto paneContent{ winrt::make<TerminalPaneContent>(profile, control) };
+            return std::make_shared<Pane>(paneContent);
         }
 
         TerminalSettingsCreateResult controlSettings{ nullptr };
@@ -3173,13 +3167,15 @@ namespace winrt::TerminalApp::implementation
 
         const auto control = _CreateNewControlAndContent(controlSettings, connection);
 
-        auto resultPane = std::make_shared<Pane>(profile, control);
+        auto paneContent{ winrt::make<TerminalPaneContent>(profile, control) };
+        auto resultPane = std::make_shared<Pane>(paneContent);
 
         if (debugConnection) // this will only be set if global debugging is on and tap is active
         {
             auto newControl = _CreateNewControlAndContent(controlSettings, debugConnection);
             // Split (auto) with the debug tap.
-            auto debugPane = std::make_shared<Pane>(profile, newControl);
+            auto debugContent{ winrt::make<TerminalPaneContent>(profile, newControl) };
+            auto debugPane = std::make_shared<Pane>(debugContent);
 
             // Since we're doing this split directly on the pane (instead of going through TerminalTab,
             // we need to handle the panes 'active' states
@@ -3193,16 +3189,20 @@ namespace winrt::TerminalApp::implementation
             original->SetActive();
         }
 
-        _RegisterPaneEvents(resultPane);
-
         return resultPane;
     }
 
-    void TerminalPage::_restartPaneConnection(const std::shared_ptr<Pane>& pane)
+    void TerminalPage::_restartPaneConnection(
+        const TerminalApp::TerminalPaneContent& paneContent,
+        const winrt::Windows::Foundation::IInspectable&)
     {
-        if (const auto& connection{ _duplicateConnectionForRestart(pane) })
+        // Note: callers are likely passing in `nullptr` as the args here, as
+        // the TermControl.RestartTerminalRequested event doesn't actually pass
+        // any args upwards itself. If we ever change this, make sure you check
+        // for nulls
+        if (const auto& connection{ _duplicateConnectionForRestart(paneContent) })
         {
-            pane->GetTerminalControl().Connection(connection);
+            paneContent.GetTermControl().Connection(connection);
             connection.Start();
         }
     }
diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h
index 5e22760fc67..639165d1730 100644
--- a/src/cascadia/TerminalApp/TerminalPage.h
+++ b/src/cascadia/TerminalApp/TerminalPage.h
@@ -301,14 +301,14 @@ namespace winrt::TerminalApp::implementation
         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);
+        HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs);
         void _CreateNewTabFromPane(std::shared_ptr<Pane> pane, uint32_t insertPosition = -1);
 
         std::wstring _evaluatePathForCwd(std::wstring_view path);
 
         winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(Microsoft::Terminal::Settings::Model::Profile profile, Microsoft::Terminal::Settings::Model::TerminalSettings settings, const bool inheritCursor);
-        winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _duplicateConnectionForRestart(std::shared_ptr<Pane> pane);
-        void _restartPaneConnection(const std::shared_ptr<Pane>& pane);
+        winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _duplicateConnectionForRestart(const TerminalApp::TerminalPaneContent& paneContent);
+        void _restartPaneConnection(const TerminalApp::TerminalPaneContent&, const winrt::Windows::Foundation::IInspectable&);
 
         winrt::fire_and_forget _OpenNewWindow(const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs);
 
@@ -344,7 +344,6 @@ namespace winrt::TerminalApp::implementation
         void _InitializeTab(winrt::com_ptr<TerminalTab> newTabImpl, uint32_t insertPosition = -1);
         void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term);
         void _RegisterTabEvents(TerminalTab& hostingTab);
-        void _RegisterPaneEvents(std::shared_ptr<Pane>& pane);
 
         void _DismissTabContextMenus();
         void _FocusCurrentTab(const bool focusAlways);
diff --git a/src/cascadia/TerminalApp/TerminalPaneContent.cpp b/src/cascadia/TerminalApp/TerminalPaneContent.cpp
new file mode 100644
index 00000000000..597631d8fdc
--- /dev/null
+++ b/src/cascadia/TerminalApp/TerminalPaneContent.cpp
@@ -0,0 +1,332 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "TerminalPaneContent.h"
+#include "TerminalPaneContent.g.cpp"
+
+#include "BellEventArgs.g.cpp"
+
+#include <Mmsystem.h>
+using namespace winrt::Windows::Foundation;
+using namespace winrt::Windows::UI::Xaml;
+using namespace winrt::Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Control;
+using namespace winrt::Microsoft::Terminal::TerminalConnection;
+
+namespace winrt::TerminalApp::implementation
+{
+    TerminalPaneContent::TerminalPaneContent(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile,
+                                             const winrt::Microsoft::Terminal::Control::TermControl& control) :
+        _control{ control },
+        _profile{ profile }
+    {
+        _setupControlEvents();
+    }
+
+    void TerminalPaneContent::_setupControlEvents()
+    {
+        _controlEvents._ConnectionStateChanged = _control.ConnectionStateChanged(winrt::auto_revoke, { this, &TerminalPaneContent::_controlConnectionStateChangedHandler });
+        _controlEvents._WarningBell = _control.WarningBell(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlWarningBellHandler });
+        _controlEvents._CloseTerminalRequested = _control.CloseTerminalRequested(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_closeTerminalRequestedHandler });
+        _controlEvents._RestartTerminalRequested = _control.RestartTerminalRequested(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_restartTerminalRequestedHandler });
+
+        _controlEvents._TitleChanged = _control.TitleChanged(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlTitleChanged });
+        _controlEvents._TabColorChanged = _control.TabColorChanged(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlTabColorChanged });
+        _controlEvents._SetTaskbarProgress = _control.SetTaskbarProgress(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlSetTaskbarProgress });
+        _controlEvents._ReadOnlyChanged = _control.ReadOnlyChanged(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlReadOnlyChanged });
+        _controlEvents._FocusFollowMouseRequested = _control.FocusFollowMouseRequested(winrt::auto_revoke, { get_weak(), &TerminalPaneContent::_controlFocusFollowMouseRequested });
+    }
+    void TerminalPaneContent::_removeControlEvents()
+    {
+        _controlEvents = {};
+    }
+
+    winrt::Windows::UI::Xaml::FrameworkElement TerminalPaneContent::GetRoot()
+    {
+        return _control;
+    }
+    winrt::Microsoft::Terminal::Control::TermControl TerminalPaneContent::GetTermControl()
+    {
+        return _control;
+    }
+    winrt::Windows::Foundation::Size TerminalPaneContent::MinimumSize()
+    {
+        return _control.MinimumSize();
+    }
+    void TerminalPaneContent::Focus(winrt::Windows::UI::Xaml::FocusState reason)
+    {
+        _control.Focus(reason);
+    }
+    void TerminalPaneContent::Close()
+    {
+        _removeControlEvents();
+
+        // Clear out our media player callbacks, and stop any playing media. This
+        // will prevent the callback from being triggered after we've closed, and
+        // also make sure that our sound stops when we're closed.
+        if (_bellPlayer)
+        {
+            _bellPlayer.Pause();
+            _bellPlayer.Source(nullptr);
+            _bellPlayer.Close();
+            _bellPlayer = nullptr;
+            _bellPlayerCreated = false;
+        }
+
+        CloseRequested.raise(*this, nullptr);
+    }
+
+    NewTerminalArgs TerminalPaneContent::GetNewTerminalArgs(const bool asContent) const
+    {
+        NewTerminalArgs args{};
+        const auto& controlSettings = _control.Settings();
+
+        args.Profile(controlSettings.ProfileName());
+        // If we know the user's working directory use it instead of the profile.
+        if (const auto dir = _control.WorkingDirectory(); !dir.empty())
+        {
+            args.StartingDirectory(dir);
+        }
+        else
+        {
+            args.StartingDirectory(controlSettings.StartingDirectory());
+        }
+        args.TabTitle(controlSettings.StartingTitle());
+        args.Commandline(controlSettings.Commandline());
+        args.SuppressApplicationTitle(controlSettings.SuppressApplicationTitle());
+        if (controlSettings.TabColor() || controlSettings.StartingTabColor())
+        {
+            til::color c;
+            // StartingTabColor is prioritized over other colors
+            if (const auto color = controlSettings.StartingTabColor())
+            {
+                c = til::color(color.Value());
+            }
+            else
+            {
+                c = til::color(controlSettings.TabColor().Value());
+            }
+
+            args.TabColor(winrt::Windows::Foundation::IReference<winrt::Windows::UI::Color>{ static_cast<winrt::Windows::UI::Color>(c) });
+        }
+
+        // TODO:GH#9800 - we used to be able to persist the color scheme that a
+        // TermControl was initialized with, by name. With the change to having the
+        // control own its own copy of its settings, this isn't possible anymore.
+        //
+        // We may be able to get around this by storing the Name in the Core::Scheme
+        // object. That would work for schemes set by the Terminal, but not ones set
+        // by VT, but that seems good enough.
+
+        // Only fill in the ContentId if absolutely needed. If you fill in a number
+        // here (even 0), we'll serialize that number, AND treat that action as an
+        // "attach existing" rather than a "create"
+        if (asContent)
+        {
+            args.ContentId(_control.ContentId());
+        }
+
+        return args;
+    }
+
+    void TerminalPaneContent::_controlTitleChanged(const IInspectable&, const IInspectable&)
+    {
+        TitleChanged.raise(*this, nullptr);
+    }
+    void TerminalPaneContent::_controlTabColorChanged(const IInspectable&, const IInspectable&)
+    {
+        TabColorChanged.raise(*this, nullptr);
+    }
+    void TerminalPaneContent::_controlSetTaskbarProgress(const IInspectable&, const IInspectable&)
+    {
+        TaskbarProgressChanged.raise(*this, nullptr);
+    }
+    void TerminalPaneContent::_controlReadOnlyChanged(const IInspectable&, const IInspectable&)
+    {
+        ReadOnlyChanged.raise(*this, nullptr);
+    }
+    void TerminalPaneContent::_controlFocusFollowMouseRequested(const IInspectable&, const IInspectable&)
+    {
+        FocusRequested.raise(*this, nullptr);
+    }
+
+    // Method Description:
+    // - Called when our attached control is closed. Triggers listeners to our close
+    //   event, if we're a leaf pane.
+    // - If this was called, and we became a parent pane (due to work on another
+    //   thread), this function will do nothing (allowing the control's new parent
+    //   to handle the event instead).
+    // Arguments:
+    // - <none>
+    // Return Value:
+    // - <none>
+    winrt::fire_and_forget TerminalPaneContent::_controlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender,
+                                                                                      const winrt::Windows::Foundation::IInspectable& args)
+    {
+        ConnectionStateChanged.raise(sender, args);
+        auto newConnectionState = ConnectionState::Closed;
+        if (const auto coreState = sender.try_as<ICoreState>())
+        {
+            newConnectionState = coreState.ConnectionState();
+        }
+
+        const auto previousConnectionState = std::exchange(_connectionState, newConnectionState);
+        if (newConnectionState < ConnectionState::Closed)
+        {
+            // Pane doesn't care if the connection isn't entering a terminal state.
+            co_return;
+        }
+
+        const auto weakThis = get_weak();
+        co_await wil::resume_foreground(_control.Dispatcher());
+        const auto strongThis = weakThis.get();
+        if (!strongThis)
+        {
+            co_return;
+        }
+
+        // It's possible that this event handler started being executed, scheduled
+        // on the UI thread, another child got created. So our control is
+        // actually no longer _our_ control, and instead could be a descendant.
+        //
+        // When the control's new Pane takes ownership of the control, the new
+        // parent will register its own event handler. That event handler will get
+        // fired after this handler returns, and will properly cleanup state.
+
+        if (previousConnectionState < ConnectionState::Connected && newConnectionState >= ConnectionState::Failed)
+        {
+            // A failure to complete the connection (before it has _connected_) is not covered by "closeOnExit".
+            // This is to prevent a misconfiguration (closeOnExit: always, startingDirectory: garbage) resulting
+            // in Terminal flashing open and immediately closed.
+            co_return;
+        }
+
+        if (_profile)
+        {
+            if (_isDefTermSession && _profile.CloseOnExit() == CloseOnExitMode::Automatic)
+            {
+                // For 'automatic', we only care about the connection state if we were launched by Terminal
+                // Since we were launched via defterm, ignore the connection state (i.e. we treat the
+                // close on exit mode as 'always', see GH #13325 for discussion)
+                Close();
+            }
+
+            const auto mode = _profile.CloseOnExit();
+            if ((mode == CloseOnExitMode::Always) ||
+                ((mode == CloseOnExitMode::Graceful || mode == CloseOnExitMode::Automatic) && newConnectionState == ConnectionState::Closed))
+            {
+                Close();
+            }
+        }
+    }
+
+    // Method Description:
+    // - Plays a warning note when triggered by the BEL control character,
+    //   using the sound configured for the "Critical Stop" system event.`
+    //   This matches the behavior of the Windows Console host.
+    // - Will also flash the taskbar if the bellStyle setting for this profile
+    //   has the 'visual' flag set
+    // Arguments:
+    // - <unused>
+    void TerminalPaneContent::_controlWarningBellHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
+                                                         const winrt::Windows::Foundation::IInspectable& /*eventArgs*/)
+    {
+        if (_profile)
+        {
+            // We don't want to do anything if nothing is set, so check for that first
+            if (static_cast<int>(_profile.BellStyle()) != 0)
+            {
+                if (WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Audible))
+                {
+                    // Audible is set, play the sound
+                    auto sounds{ _profile.BellSound() };
+                    if (sounds && sounds.Size() > 0)
+                    {
+                        winrt::hstring soundPath{ wil::ExpandEnvironmentStringsW<std::wstring>(sounds.GetAt(rand() % sounds.Size()).c_str()) };
+                        winrt::Windows::Foundation::Uri uri{ soundPath };
+                        _playBellSound(uri);
+                    }
+                    else
+                    {
+                        const auto soundAlias = reinterpret_cast<LPCTSTR>(SND_ALIAS_SYSTEMHAND);
+                        PlaySound(soundAlias, NULL, SND_ALIAS_ID | SND_ASYNC | SND_SENTRY);
+                    }
+                }
+
+                if (WI_IsFlagSet(_profile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Window))
+                {
+                    _control.BellLightOn();
+                }
+
+                // raise the event with the bool value corresponding to the taskbar flag
+                BellRequested.raise(*this,
+                                    *winrt::make_self<TerminalApp::implementation::BellEventArgs>(WI_IsFlagSet(_profile.BellStyle(), BellStyle::Taskbar)));
+            }
+        }
+    }
+
+    winrt::fire_and_forget TerminalPaneContent::_playBellSound(winrt::Windows::Foundation::Uri uri)
+    {
+        auto weakThis{ get_weak() };
+        co_await wil::resume_foreground(_control.Dispatcher());
+        if (auto pane{ weakThis.get() })
+        {
+            if (!_bellPlayerCreated)
+            {
+                // The MediaPlayer might not exist on Windows N SKU.
+                try
+                {
+                    _bellPlayerCreated = true;
+                    _bellPlayer = winrt::Windows::Media::Playback::MediaPlayer();
+                    // GH#12258: The media keys (like play/pause) should have no effect on our bell sound.
+                    _bellPlayer.CommandManager().IsEnabled(false);
+                }
+                CATCH_LOG();
+            }
+            if (_bellPlayer)
+            {
+                const auto source{ winrt::Windows::Media::Core::MediaSource::CreateFromUri(uri) };
+                const auto item{ winrt::Windows::Media::Playback::MediaPlaybackItem(source) };
+                _bellPlayer.Source(item);
+                _bellPlayer.Play();
+            }
+        }
+    }
+    void TerminalPaneContent::_closeTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
+                                                             const winrt::Windows::Foundation::IInspectable& /*args*/)
+    {
+        Close();
+    }
+
+    void TerminalPaneContent::_restartTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/,
+                                                               const winrt::Windows::Foundation::IInspectable& /*args*/)
+    {
+        RestartTerminalRequested.raise(*this, nullptr);
+    }
+
+    void TerminalPaneContent::UpdateSettings(const TerminalSettingsCreateResult& settings,
+                                             const Profile& profile)
+    {
+        _profile = profile;
+        _control.UpdateControlSettings(settings.DefaultSettings(), settings.UnfocusedSettings());
+    }
+
+    // Method Description:
+    // - Should be called when this pane is created via a default terminal handoff
+    // - Finalizes our configuration given the information that we have been
+    //   created via default handoff
+    void TerminalPaneContent::MarkAsDefterm()
+    {
+        _isDefTermSession = true;
+    }
+
+    float TerminalPaneContent::SnapDownToGrid(const TerminalApp::PaneSnapDirection direction, const float sizeToSnap)
+    {
+        return _control.SnapDimensionToGrid(direction == PaneSnapDirection::Width, sizeToSnap);
+    }
+    Windows::Foundation::Size TerminalPaneContent::GridUnitSize()
+    {
+        return _control.CharacterDimensions();
+    }
+}
diff --git a/src/cascadia/TerminalApp/TerminalPaneContent.h b/src/cascadia/TerminalApp/TerminalPaneContent.h
new file mode 100644
index 00000000000..2c2deee0124
--- /dev/null
+++ b/src/cascadia/TerminalApp/TerminalPaneContent.h
@@ -0,0 +1,102 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+#include "TerminalPaneContent.g.h"
+#include "BellEventArgs.g.h"
+
+namespace winrt::TerminalApp::implementation
+{
+    struct BellEventArgs : public BellEventArgsT<BellEventArgs>
+    {
+    public:
+        BellEventArgs(bool flashTaskbar) :
+            FlashTaskbar(flashTaskbar) {}
+
+        til::property<bool> FlashTaskbar;
+    };
+
+    struct TerminalPaneContent : TerminalPaneContentT<TerminalPaneContent>
+    {
+        TerminalPaneContent(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile,
+                            const winrt::Microsoft::Terminal::Control::TermControl& control);
+
+        winrt::Windows::UI::Xaml::FrameworkElement GetRoot();
+        winrt::Microsoft::Terminal::Control::TermControl GetTermControl();
+        winrt::Windows::Foundation::Size MinimumSize();
+        void Focus(winrt::Windows::UI::Xaml::FocusState reason = winrt::Windows::UI::Xaml::FocusState::Programmatic);
+        void Close();
+
+        winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs GetNewTerminalArgs(const bool asContent) const;
+
+        void UpdateSettings(const winrt::Microsoft::Terminal::Settings::Model::TerminalSettingsCreateResult& settings,
+                            const winrt::Microsoft::Terminal::Settings::Model::Profile& profile);
+
+        void MarkAsDefterm();
+
+        winrt::Microsoft::Terminal::Settings::Model::Profile GetProfile() const
+        {
+            return _profile;
+        }
+
+        winrt::hstring Title() { return _control.Title(); }
+        uint64_t TaskbarState() { return _control.TaskbarState(); }
+        uint64_t TaskbarProgress() { return _control.TaskbarProgress(); }
+        bool ReadOnly() { return _control.ReadOnly(); }
+
+        float SnapDownToGrid(const TerminalApp::PaneSnapDirection direction, const float sizeToSnap);
+        Windows::Foundation::Size GridUnitSize();
+
+        til::typed_event<TerminalApp::TerminalPaneContent, winrt::Windows::Foundation::IInspectable> RestartTerminalRequested;
+        til::typed_event<> ConnectionStateChanged;
+        til::typed_event<IPaneContent> CloseRequested;
+        til::typed_event<IPaneContent, winrt::TerminalApp::BellEventArgs> BellRequested;
+        til::typed_event<IPaneContent> TitleChanged;
+        til::typed_event<IPaneContent> TabColorChanged;
+        til::typed_event<IPaneContent> TaskbarProgressChanged;
+        til::typed_event<IPaneContent> ReadOnlyChanged;
+        til::typed_event<IPaneContent> FocusRequested;
+
+    private:
+        winrt::Microsoft::Terminal::Control::TermControl _control{ nullptr };
+        winrt::Microsoft::Terminal::TerminalConnection::ConnectionState _connectionState{ winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::NotConnected };
+        winrt::Microsoft::Terminal::Settings::Model::Profile _profile{ nullptr };
+        bool _isDefTermSession{ false };
+
+        winrt::Windows::Media::Playback::MediaPlayer _bellPlayer{ nullptr };
+        bool _bellPlayerCreated{ false };
+
+        struct ControlEventTokens
+        {
+            winrt::Microsoft::Terminal::Control::TermControl::ConnectionStateChanged_revoker _ConnectionStateChanged;
+            winrt::Microsoft::Terminal::Control::TermControl::WarningBell_revoker _WarningBell;
+            winrt::Microsoft::Terminal::Control::TermControl::CloseTerminalRequested_revoker _CloseTerminalRequested;
+            winrt::Microsoft::Terminal::Control::TermControl::RestartTerminalRequested_revoker _RestartTerminalRequested;
+
+            winrt::Microsoft::Terminal::Control::TermControl::TitleChanged_revoker _TitleChanged;
+            winrt::Microsoft::Terminal::Control::TermControl::TabColorChanged_revoker _TabColorChanged;
+            winrt::Microsoft::Terminal::Control::TermControl::SetTaskbarProgress_revoker _SetTaskbarProgress;
+            winrt::Microsoft::Terminal::Control::TermControl::ReadOnlyChanged_revoker _ReadOnlyChanged;
+            winrt::Microsoft::Terminal::Control::TermControl::FocusFollowMouseRequested_revoker _FocusFollowMouseRequested;
+
+        } _controlEvents;
+        void _setupControlEvents();
+        void _removeControlEvents();
+
+        winrt::fire_and_forget _playBellSound(winrt::Windows::Foundation::Uri uri);
+
+        winrt::fire_and_forget _controlConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/);
+        void _controlWarningBellHandler(const winrt::Windows::Foundation::IInspectable& sender,
+                                        const winrt::Windows::Foundation::IInspectable& e);
+        void _controlReadOnlyChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& e);
+
+        void _controlTitleChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args);
+        void _controlTabColorChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args);
+        void _controlSetTaskbarProgress(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args);
+        void _controlReadOnlyChanged(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args);
+        void _controlFocusFollowMouseRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args);
+
+        void _closeTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/);
+        void _restartTerminalRequestedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& /*args*/);
+    };
+}
diff --git a/src/cascadia/TerminalApp/TerminalPaneContent.idl b/src/cascadia/TerminalApp/TerminalPaneContent.idl
new file mode 100644
index 00000000000..7e04c8b836c
--- /dev/null
+++ b/src/cascadia/TerminalApp/TerminalPaneContent.idl
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import "IPaneContent.idl";
+
+namespace TerminalApp
+{
+    [default_interface] runtimeclass TerminalPaneContent : IPaneContent, ISnappable
+    {
+        Microsoft.Terminal.Control.TermControl GetTermControl();
+
+        void UpdateSettings(const Microsoft.Terminal.Settings.Model.TerminalSettingsCreateResult settings,
+                            const Microsoft.Terminal.Settings.Model.Profile profile);
+
+        void MarkAsDefterm();
+
+        Microsoft.Terminal.Settings.Model.Profile GetProfile();
+
+        event Windows.Foundation.TypedEventHandler<TerminalPaneContent, Object> RestartTerminalRequested;
+    }
+}
diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp
index b3d37d88a17..c5cb7f5b5f2 100644
--- a/src/cascadia/TerminalApp/TerminalTab.cpp
+++ b/src/cascadia/TerminalApp/TerminalTab.cpp
@@ -186,6 +186,11 @@ namespace winrt::TerminalApp::implementation
         return nullptr;
     }
 
+    IPaneContent TerminalTab::GetActiveContent() const
+    {
+        return _activePane ? _activePane->GetContent() : nullptr;
+    }
+
     // Method Description:
     // - Called after construction of a Tab object to bind event handlers to its
     //   associated Pane and TermControl objects
@@ -200,9 +205,9 @@ namespace winrt::TerminalApp::implementation
         _rootPane->WalkTree([&](std::shared_ptr<Pane> pane) {
             // Attach event handlers to each new pane
             _AttachEventHandlersToPane(pane);
-            if (auto control = pane->GetTerminalControl())
+            if (auto content = pane->GetContent())
             {
-                _AttachEventHandlersToControl(pane->Id().value(), control);
+                _AttachEventHandlersToContent(pane->Id().value(), content);
             }
         });
     }
@@ -377,8 +382,8 @@ namespace winrt::TerminalApp::implementation
         {
             return RS_(L"MultiplePanes");
         }
-        const auto lastFocusedControl = GetActiveTerminalControl();
-        return lastFocusedControl ? lastFocusedControl.Title() : L"";
+        const auto activeContent = GetActiveContent();
+        return activeContent ? activeContent.Title() : L"";
     }
 
     // Method Description:
@@ -513,7 +518,10 @@ namespace winrt::TerminalApp::implementation
             if (p->_IsLeaf())
             {
                 p->Id(_nextPaneId);
-                _AttachEventHandlersToControl(p->Id().value(), p->_control);
+                if (const auto& content{ p->GetContent() })
+                {
+                    _AttachEventHandlersToContent(p->Id().value(), content);
+                }
                 _nextPaneId++;
             }
             return false;
@@ -626,7 +634,11 @@ namespace winrt::TerminalApp::implementation
             if (p->_IsLeaf())
             {
                 p->Id(_nextPaneId);
-                _AttachEventHandlersToControl(p->Id().value(), p->_control);
+
+                if (const auto& content{ p->GetContent() })
+                {
+                    _AttachEventHandlersToContent(p->Id().value(), content);
+                }
                 _nextPaneId++;
             }
         });
@@ -883,29 +895,17 @@ namespace winrt::TerminalApp::implementation
     //   the control itself doesn't have a particular ID and its pointer is
     //   unstable since it is moved when panes split.
     // Arguments:
-    // - paneId: The ID of the pane that contains the given control.
-    // - control: the control to remove events from.
+    // - paneId: The ID of the pane that contains the given content.
     // Return Value:
     // - <none>
-    void TerminalTab::_DetachEventHandlersFromControl(const uint32_t paneId, const TermControl& control)
+    void TerminalTab::_DetachEventHandlersFromContent(const uint32_t paneId)
     {
-        auto it = _controlEvents.find(paneId);
-        if (it != _controlEvents.end())
+        auto it = _contentEvents.find(paneId);
+        if (it != _contentEvents.end())
         {
-            auto& events = it->second;
-
-            control.TitleChanged(events.titleToken);
-            control.TabColorChanged(events.colorToken);
-            control.SetTaskbarProgress(events.taskbarToken);
-            control.ConnectionStateChanged(events.stateToken);
-            control.ReadOnlyChanged(events.readOnlyToken);
-            control.FocusFollowMouseRequested(events.focusToken);
-
-            events.KeySent.revoke();
-            events.CharSent.revoke();
-            events.StringSent.revoke();
-
-            _controlEvents.erase(paneId);
+            // revoke the event handlers by resetting the event struct
+            // and remove it from the map
+            _contentEvents.erase(paneId);
         }
     }
 
@@ -920,87 +920,159 @@ namespace winrt::TerminalApp::implementation
     // - control: the TermControl to add events to.
     // Return Value:
     // - <none>
-    void TerminalTab::_AttachEventHandlersToControl(const uint32_t paneId, const TermControl& control)
+    void TerminalTab::_AttachEventHandlersToContent(const uint32_t paneId, const TerminalApp::IPaneContent& content)
     {
         auto weakThis{ get_weak() };
         auto dispatcher = TabViewItem().Dispatcher();
-        ControlEventTokens events{};
-
-        events.titleToken = control.TitleChanged([dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget {
-            // The lambda lives in the `std::function`-style container owned by `control`. That is, when the
-            // `control` gets destroyed the lambda struct also gets destroyed. In other words, we need to
-            // copy `weakThis` onto the stack, because that's the only thing that gets captured in coroutines.
-            // See: https://devblogs.microsoft.com/oldnewthing/20211103-00/?p=105870
-            const auto weakThisCopy = weakThis;
-            co_await wil::resume_foreground(dispatcher);
-            if (auto tab{ weakThisCopy.get() })
-            {
-                // The title of the control changed, but not necessarily the title of the tab.
-                // Set the tab's text to the active panes' text.
-                tab->UpdateTitle();
-            }
-        });
+        ContentEventTokens events{};
+
+        events.CloseRequested = content.CloseRequested(
+            winrt::auto_revoke,
+            [dispatcher, weakThis](auto sender, auto&&) -> winrt::fire_and_forget {
+                // Don't forget! this ^^^^^^^^ sender can't be a reference, this is a async callback.
+
+                // The lambda lives in the `std::function`-style container owned by `control`. That is, when the
+                // `control` gets destroyed the lambda struct also gets destroyed. In other words, we need to
+                // copy `weakThis` onto the stack, because that's the only thing that gets captured in coroutines.
+                // See: https://devblogs.microsoft.com/oldnewthing/20211103-00/?p=105870
+                const auto weakThisCopy = weakThis;
+                co_await wil::resume_foreground(dispatcher);
+                // Check if Tab's lifetime has expired
+                if (auto tab{ weakThisCopy.get() })
+                {
+                    if (const auto content{ sender.try_as<TerminalApp::IPaneContent>() })
+                    {
+                        tab->_rootPane->WalkTree([content](std::shared_ptr<Pane> pane) {
+                            if (pane->GetContent() == content)
+                            {
+                                pane->Close();
+                            }
+                        });
+                    }
+                }
+            });
 
-        events.colorToken = control.TabColorChanged([dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget {
-            const auto weakThisCopy = weakThis;
-            co_await wil::resume_foreground(dispatcher);
-            if (auto tab{ weakThisCopy.get() })
-            {
-                // The control's tabColor changed, but it is not necessarily the
-                // active control in this tab. We'll just recalculate the
-                // current color anyways.
-                tab->_RecalculateAndApplyTabColor();
-            }
-        });
+        events.TitleChanged = content.TitleChanged(
+            winrt::auto_revoke,
+            [dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget {
+                // The lambda lives in the `std::function`-style container owned by `control`. That is, when the
+                // `control` gets destroyed the lambda struct also gets destroyed. In other words, we need to
+                // copy `weakThis` onto the stack, because that's the only thing that gets captured in coroutines.
+                // See: https://devblogs.microsoft.com/oldnewthing/20211103-00/?p=105870
+                const auto weakThisCopy = weakThis;
+                co_await wil::resume_foreground(dispatcher);
+                // Check if Tab's lifetime has expired
+                if (auto tab{ weakThisCopy.get() })
+                {
+                    // The title of the control changed, but not necessarily the title of the tab.
+                    // Set the tab's text to the active panes' text.
+                    tab->UpdateTitle();
+                }
+            });
 
-        events.taskbarToken = control.SetTaskbarProgress([dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget {
-            const auto weakThisCopy = weakThis;
-            co_await wil::resume_foreground(dispatcher);
-            if (auto tab{ weakThisCopy.get() })
-            {
-                tab->_UpdateProgressState();
-            }
-        });
+        events.TabColorChanged = content.TabColorChanged(
+            winrt::auto_revoke,
+            [dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget {
+                const auto weakThisCopy = weakThis;
+                co_await wil::resume_foreground(dispatcher);
+                if (auto tab{ weakThisCopy.get() })
+                {
+                    // The control's tabColor changed, but it is not necessarily the
+                    // active control in this tab. We'll just recalculate the
+                    // current color anyways.
+                    tab->_RecalculateAndApplyTabColor();
+                }
+            });
 
-        events.stateToken = control.ConnectionStateChanged([dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget {
-            const auto weakThisCopy = weakThis;
-            co_await wil::resume_foreground(dispatcher);
-            if (auto tab{ weakThisCopy.get() })
-            {
-                tab->_UpdateConnectionClosedState();
-            }
-        });
+        events.TaskbarProgressChanged = content.TaskbarProgressChanged(
+            winrt::auto_revoke,
+            [dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget {
+                const auto weakThisCopy = weakThis;
+                co_await wil::resume_foreground(dispatcher);
+                // Check if Tab's lifetime has expired
+                if (auto tab{ weakThisCopy.get() })
+                {
+                    tab->_UpdateProgressState();
+                }
+            });
 
-        events.readOnlyToken = control.ReadOnlyChanged([dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget {
-            const auto weakThisCopy = weakThis;
-            co_await wil::resume_foreground(dispatcher);
-            if (auto tab{ weakThisCopy.get() })
-            {
-                tab->_RecalculateAndApplyReadOnly();
-            }
-        });
+        events.ConnectionStateChanged = content.ConnectionStateChanged(
+            winrt::auto_revoke,
+            [dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget {
+                const auto weakThisCopy = weakThis;
+                co_await wil::resume_foreground(dispatcher);
+                if (auto tab{ weakThisCopy.get() })
+                {
+                    tab->_UpdateConnectionClosedState();
+                }
+            });
 
-        events.focusToken = control.FocusFollowMouseRequested([dispatcher, weakThis](auto sender, auto) -> winrt::fire_and_forget {
-            const auto weakThisCopy = weakThis;
-            co_await wil::resume_foreground(dispatcher);
-            if (const auto tab{ weakThisCopy.get() })
-            {
-                if (tab->_focused())
+        events.ReadOnlyChanged = content.ReadOnlyChanged(
+            winrt::auto_revoke,
+            [dispatcher, weakThis](auto&&, auto&&) -> winrt::fire_and_forget {
+                const auto weakThisCopy = weakThis;
+                co_await wil::resume_foreground(dispatcher);
+                if (auto tab{ weakThis.get() })
                 {
-                    if (const auto termControl{ sender.try_as<winrt::Microsoft::Terminal::Control::TermControl>() })
+                    tab->_RecalculateAndApplyReadOnly();
+                }
+            });
+
+        events.FocusRequested = content.FocusRequested(
+            winrt::auto_revoke,
+            [dispatcher, weakThis](TerminalApp::IPaneContent sender, auto) -> winrt::fire_and_forget {
+                const auto weakThisCopy = weakThis;
+                co_await wil::resume_foreground(dispatcher);
+                if (const auto tab{ weakThisCopy.get() })
+                {
+                    if (tab->_focused())
                     {
-                        termControl.Focus(FocusState::Pointer);
+                        sender.Focus(FocusState::Pointer);
                     }
                 }
-            }
-        });
+            });
+
+        events.BellRequested = content.BellRequested(
+            winrt::auto_revoke,
+            [dispatcher, weakThis](TerminalApp::IPaneContent sender, auto bellArgs) -> winrt::fire_and_forget {
+                const auto weakThisCopy = weakThis;
+                co_await wil::resume_foreground(dispatcher);
+                if (const auto tab{ weakThisCopy.get() })
+                {
+                    if (bellArgs.FlashTaskbar())
+                    {
+                        // If visual is set, we need to bubble this event all the way to app host to flash the taskbar
+                        // In this part of the chain we bubble it from the hosting tab to the page
+                        tab->TabRaiseVisualBell.raise();
+                    }
+
+                    // Show the bell indicator in the tab header
+                    tab->ShowBellIndicator(true);
+
+                    // If this tab is focused, activate the bell indicator timer, which will
+                    // remove the bell indicator once it fires
+                    // (otherwise, the indicator is removed when the tab gets focus)
+                    if (tab->_focusState != WUX::FocusState::Unfocused)
+                    {
+                        tab->ActivateBellIndicatorTimer();
+                    }
+                }
+            });
+
+        if (const auto& terminal{ content.try_as<TerminalApp::TerminalPaneContent>() })
+        {
+            events.RestartTerminalRequested = terminal.RestartTerminalRequested(winrt::auto_revoke, { get_weak(), &TerminalTab::_bubbleRestartTerminalRequested });
+        }
 
         if (_tabStatus.IsInputBroadcastActive())
         {
-            _addBroadcastHandlers(control, events);
+            if (const auto& termContent{ content.try_as<TerminalApp::TerminalPaneContent>() })
+            {
+                _addBroadcastHandlers(termContent.GetTermControl(), events);
+            }
         }
 
-        _controlEvents[paneId] = std::move(events);
+        _contentEvents[paneId] = std::move(events);
     }
 
     // Method Description:
@@ -1250,36 +1322,12 @@ namespace winrt::TerminalApp::implementation
             }
         });
 
-        // Add a PaneRaiseBell event handler to the Pane
-        auto bellToken = pane->PaneRaiseBell([weakThis](auto&& /*s*/, auto&& visual) {
-            if (auto tab{ weakThis.get() })
-            {
-                if (visual)
-                {
-                    // If visual is set, we need to bubble this event all the way to app host to flash the taskbar
-                    // In this part of the chain we bubble it from the hosting tab to the page
-                    tab->TabRaiseVisualBell.raise();
-                }
-
-                // Show the bell indicator in the tab header
-                tab->ShowBellIndicator(true);
-
-                // If this tab is focused, activate the bell indicator timer, which will
-                // remove the bell indicator once it fires
-                // (otherwise, the indicator is removed when the tab gets focus)
-                if (tab->_focusState != WUX::FocusState::Unfocused)
-                {
-                    tab->ActivateBellIndicatorTimer();
-                }
-            }
-        });
-
         // box the event token so that we can give a reference to it in the
         // event handler.
         auto detachedToken = std::make_shared<winrt::event_token>();
         // Add a Detached event handler to the Pane to clean up tab state
         // and other event handlers when a pane is removed from this tab.
-        *detachedToken = pane->Detached([weakThis, weakPane, gotFocusToken, lostFocusToken, closedToken, bellToken, detachedToken](std::shared_ptr<Pane> /*sender*/) {
+        *detachedToken = pane->Detached([weakThis, weakPane, gotFocusToken, lostFocusToken, closedToken, detachedToken](std::shared_ptr<Pane> /*sender*/) {
             // Make sure we do this at most once
             if (auto pane{ weakPane.lock() })
             {
@@ -1287,14 +1335,10 @@ namespace winrt::TerminalApp::implementation
                 pane->GotFocus(gotFocusToken);
                 pane->LostFocus(lostFocusToken);
                 pane->Closed(closedToken);
-                pane->PaneRaiseBell(bellToken);
 
                 if (auto tab{ weakThis.get() })
                 {
-                    if (auto control = pane->GetTerminalControl())
-                    {
-                        tab->_DetachEventHandlersFromControl(pane->Id().value(), control);
-                    }
+                    tab->_DetachEventHandlersFromContent(pane->Id().value());
 
                     for (auto i = tab->_mruPanes.begin(); i != tab->_mruPanes.end(); ++i)
                     {
@@ -1495,7 +1539,12 @@ namespace winrt::TerminalApp::implementation
                 // GH#10112 - if we're opening the tab renamer, don't
                 // immediately toss focus to the control. We don't want to steal
                 // focus from the tab renamer.
-                if (!tab->_headerControl.InRename() && !tab->GetActiveTerminalControl().SearchBoxEditInFocus())
+                const auto& terminalControl{ tab->GetActiveTerminalControl() }; // maybe null
+                // If we're
+                // * NOT in a rename
+                // * AND (the content isn't a TermControl, OR the term control doesn't have focus in the search box)
+                if (!tab->_headerControl.InRename() &&
+                    (terminalControl == nullptr || !terminalControl.SearchBoxEditInFocus()))
                 {
                     tab->RequestFocusActiveControl.raise();
                 }
@@ -1695,6 +1744,18 @@ namespace winrt::TerminalApp::implementation
         return _zoomedPane != nullptr;
     }
 
+    TermControl _termControlFromPane(const auto& pane)
+    {
+        if (const auto content{ pane->GetContent() })
+        {
+            if (const auto termContent{ content.try_as<winrt::TerminalApp::TerminalPaneContent>() })
+            {
+                return termContent.GetTermControl();
+            }
+        }
+        return nullptr;
+    }
+
     // Method Description:
     // - Toggle read-only mode on the active pane
     // - If a parent pane is selected, this will ensure that all children have
@@ -1706,14 +1767,14 @@ namespace winrt::TerminalApp::implementation
         auto hasReadOnly = false;
         auto allReadOnly = true;
         _activePane->WalkTree([&](const auto& p) {
-            if (const auto& control{ p->GetTerminalControl() })
+            if (const auto& control{ _termControlFromPane(p) })
             {
                 hasReadOnly |= control.ReadOnly();
                 allReadOnly &= control.ReadOnly();
             }
         });
         _activePane->WalkTree([&](const auto& p) {
-            if (const auto& control{ p->GetTerminalControl() })
+            if (const auto& control{ _termControlFromPane(p) })
             {
                 // If all controls have the same read only state then just toggle
                 if (allReadOnly || !hasReadOnly)
@@ -1738,14 +1799,14 @@ namespace winrt::TerminalApp::implementation
         auto hasReadOnly = false;
         auto allReadOnly = true;
         _activePane->WalkTree([&](const auto& p) {
-            if (const auto& control{ p->GetTerminalControl() })
+            if (const auto& control{ _termControlFromPane(p) })
             {
                 hasReadOnly |= control.ReadOnly();
                 allReadOnly &= control.ReadOnly();
             }
         });
         _activePane->WalkTree([&](const auto& p) {
-            if (const auto& control{ p->GetTerminalControl() })
+            if (const auto& control{ _termControlFromPane(p) })
             {
                 // If all controls have the same read only state then just disable
                 if (allReadOnly || !hasReadOnly)
@@ -1776,6 +1837,10 @@ namespace winrt::TerminalApp::implementation
 
         ReadOnly(_rootPane->ContainsReadOnly());
         _updateIsClosable();
+
+        // Update all the visuals on all our panes, so they can update their
+        // border colors accordingly.
+        _rootPane->WalkTree([](const auto& p) { p->UpdateVisuals(); });
     }
 
     std::shared_ptr<Pane> TerminalTab::GetActivePane() const
@@ -1826,10 +1891,10 @@ namespace winrt::TerminalApp::implementation
             {
                 return;
             }
-            if (const auto& control{ p->GetTerminalControl() })
+            if (const auto& control{ _termControlFromPane(p) })
             {
-                auto it = _controlEvents.find(*paneId);
-                if (it != _controlEvents.end())
+                auto it = _contentEvents.find(*paneId);
+                if (it != _contentEvents.end())
                 {
                     auto& events = it->second;
 
@@ -1847,7 +1912,7 @@ namespace winrt::TerminalApp::implementation
         });
     }
 
-    void TerminalTab::_addBroadcastHandlers(const TermControl& termControl, ControlEventTokens& events)
+    void TerminalTab::_addBroadcastHandlers(const TermControl& termControl, ContentEventTokens& events)
     {
         auto weakThis{ get_weak() };
         // ADD EVENT HANDLERS HERE
@@ -1937,4 +2002,9 @@ namespace winrt::TerminalApp::implementation
         ActionAndArgs actionAndArgs{ ShortcutAction::Find, nullptr };
         _dispatch.DoAction(*this, actionAndArgs);
     }
+    void TerminalTab::_bubbleRestartTerminalRequested(TerminalApp::TerminalPaneContent sender,
+                                                      const winrt::Windows::Foundation::IInspectable& args)
+    {
+        RestartTerminalRequested.raise(sender, args);
+    }
 }
diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h
index ca5901af648..df9e4cae825 100644
--- a/src/cascadia/TerminalApp/TerminalTab.h
+++ b/src/cascadia/TerminalApp/TerminalTab.h
@@ -25,6 +25,7 @@ namespace winrt::TerminalApp::implementation
 
         winrt::Microsoft::Terminal::Control::TermControl GetActiveTerminalControl() const;
         winrt::Microsoft::Terminal::Settings::Model::Profile GetFocusedProfile() const noexcept;
+        winrt::TerminalApp::IPaneContent GetActiveContent() const;
 
         void Focus(winrt::Windows::UI::Xaml::FocusState focusState) override;
 
@@ -96,6 +97,8 @@ namespace winrt::TerminalApp::implementation
             return _tabStatus;
         }
 
+        til::typed_event<TerminalApp::TerminalPaneContent> RestartTerminalRequested;
+
         til::event<winrt::delegate<>> ActivePaneChanged;
         til::event<winrt::delegate<>> TabRaiseVisualBell;
         til::typed_event<IInspectable, IInspectable> TaskbarProgressChanged;
@@ -122,20 +125,25 @@ namespace winrt::TerminalApp::implementation
         winrt::event_token _colorClearedToken;
         winrt::event_token _pickerClosedToken;
 
-        struct ControlEventTokens
+        struct ContentEventTokens
         {
-            winrt::event_token titleToken;
-            winrt::event_token colorToken;
-            winrt::event_token taskbarToken;
-            winrt::event_token stateToken;
-            winrt::event_token readOnlyToken;
-            winrt::event_token focusToken;
-
+            winrt::TerminalApp::IPaneContent::BellRequested_revoker BellRequested;
+            winrt::TerminalApp::IPaneContent::TitleChanged_revoker TitleChanged;
+            winrt::TerminalApp::IPaneContent::TabColorChanged_revoker TabColorChanged;
+            winrt::TerminalApp::IPaneContent::TaskbarProgressChanged_revoker TaskbarProgressChanged;
+            winrt::TerminalApp::IPaneContent::ConnectionStateChanged_revoker ConnectionStateChanged;
+            winrt::TerminalApp::IPaneContent::ReadOnlyChanged_revoker ReadOnlyChanged;
+            winrt::TerminalApp::IPaneContent::FocusRequested_revoker FocusRequested;
+            winrt::TerminalApp::IPaneContent::CloseRequested_revoker CloseRequested;
+
+            // These events literally only apply if the content is a TermControl.
             winrt::Microsoft::Terminal::Control::TermControl::KeySent_revoker KeySent;
             winrt::Microsoft::Terminal::Control::TermControl::CharSent_revoker CharSent;
             winrt::Microsoft::Terminal::Control::TermControl::StringSent_revoker StringSent;
+
+            winrt::TerminalApp::TerminalPaneContent::RestartTerminalRequested_revoker RestartTerminalRequested;
         };
-        std::unordered_map<uint32_t, ControlEventTokens> _controlEvents;
+        std::unordered_map<uint32_t, ContentEventTokens> _contentEvents;
 
         winrt::event_token _rootClosedToken{};
 
@@ -162,8 +170,8 @@ namespace winrt::TerminalApp::implementation
         void _CreateContextMenu() override;
         virtual winrt::hstring _CreateToolTipTitle() override;
 
-        void _DetachEventHandlersFromControl(const uint32_t paneId, const winrt::Microsoft::Terminal::Control::TermControl& control);
-        void _AttachEventHandlersToControl(const uint32_t paneId, const winrt::Microsoft::Terminal::Control::TermControl& control);
+        void _DetachEventHandlersFromContent(const uint32_t paneId);
+        void _AttachEventHandlersToContent(const uint32_t paneId, const winrt::TerminalApp::IPaneContent& content);
         void _AttachEventHandlersToPane(std::shared_ptr<Pane> pane);
 
         void _UpdateActivePane(std::shared_ptr<Pane> pane);
@@ -181,7 +189,7 @@ namespace winrt::TerminalApp::implementation
 
         virtual winrt::Windows::UI::Xaml::Media::Brush _BackgroundBrush() override;
 
-        void _addBroadcastHandlers(const winrt::Microsoft::Terminal::Control::TermControl& control, ControlEventTokens& events);
+        void _addBroadcastHandlers(const winrt::Microsoft::Terminal::Control::TermControl& control, ContentEventTokens& events);
 
         void _chooseColorClicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
         void _renameTabClicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
@@ -192,6 +200,8 @@ namespace winrt::TerminalApp::implementation
         void _moveTabToNewWindowClicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
         void _findClicked(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::RoutedEventArgs& e);
 
+        void _bubbleRestartTerminalRequested(TerminalApp::TerminalPaneContent sender, const winrt::Windows::Foundation::IInspectable& args);
+
         friend class ::TerminalAppLocalTests::TabTests;
     };
 }
diff --git a/src/cascadia/TerminalApp/pch.h b/src/cascadia/TerminalApp/pch.h
index 6abc67781f2..e82056e57f9 100644
--- a/src/cascadia/TerminalApp/pch.h
+++ b/src/cascadia/TerminalApp/pch.h
@@ -84,6 +84,7 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalAppProvider);
 
 // Manually include til after we include Windows.Foundation to give it winrt superpowers
 #include "til.h"
+#include <til/winrt.h>
 
 #include <SafeDispatcherTimer.h>
 


Test patch: