Repository: microsoft/terminal (language: cpp)

Issue:
Add support for tab tearoff and tab merge
> I'm repurposing this for tab reordering and demoting it to a Task. We'll track the (much larger) workitem to do tab tearoff as a deliverable for vBacklog.
> Notes/reasoning:
> * Tab reordering is easy.
> * Tab tearoff requires us to answer the following questions:
>   * WT.exe is a multi-instance app. How do we drag tabs between _different processes?_
>     * Should WT be multi-instance? We may want to single-instance it.
>       * If we want to single-instance it we need to design "new window" (part of #1051)
>   * We still need to design "new window" for tearoff-without-merge because we need a drop target for the old tab.

_Originally posted by @DHowett-MSFT in https://github.com/microsoft/terminal/issues/443#issuecomment-501928756_


Discussion / hints:
also to have a sort of way to manage many open tabs... If I have 3 WSL tabs 1 cmd and two powershells tabbed it get's out of the UI at the right side of the tab bar (so I can't close the last tabs unless I close the first ones).
This would be so useful. I constantly open new tabs on some focused work, start splitting panes within the tab, and then at some point decide to move the whole tab and panes (right now I just recreate the panes and their directories from scratch) to a new terminal window in a new virtual desktop.
Hi, 

Any progress on this?  Just wanted to say I find this to be very useful.  

Thanks for all your hard work on this product!
Thanks for asking! Unfortunately, there hasn’t been any progress. When there is, this issue will be the first to be updated about it. 
Thanks for the info!  Okay I'll be following this issue :)
I just tried to tear a tab off a window in the Windows Terminal Preview version and was unable to. That led me to searching for this issue. 

Here's my use case: I am trying to demonstrate connecting two separate tmux clients to the same session, and running at different window sizes. In order to effectively do this, I need to resize one tmux window to separate dimensions from the other one. Since both my Ubuntu WSL tabs were running in the same Windows Terminal window, I was unable to do this. Hence, I tried to "tear" one of the existing tabs off into its own Windows 10 window.

This is where I got stuck, and had to open a brand new Terminal window instead, and then open up an Ubuntu tab in that window.

The work-around isn't hard, but it's just convenient to be able to tear off existing tabs, just like you can in Google Chrome.
Moving tabs to a different window is important.

I never use Edge, I am a Firefox fan boy (privacy, sharp fonts, amazing dev tools and so on ;-)). But I just tested to move a tab to a different Edge window. It works! Well guys, I am sure you can handle this for Windows Terminal too! =)
I need this so much.
Tab tearoff works with Konsole and web browsers which is why I expected this to work.

Interestingly, when I tried it out with other multi-tabbed applications(e.g. Kate) it didn't do it as I expected.

Regardless, it would be useful to drag out tabs to different windows to help organise processes.
Yes, this is an important feature. Please add support. Thanks! 😄 
I'm curious how many people are subscribed to this thread. Browsers have trained us all how tabs work, and it's really unexpected when a tool like Windows Terminal has tabs but doesn't behave like browsers do. Actually, given this is almost 2 years old, there's also a layer of sadness I and I'm guessing many others feel to see a table stakes feature like this be deprioritized this long.
Listen here pal. This hasn't been deprioritized, this has literally been my main priority for the last basically 12 months now. [I've been researching](https://github.com/microsoft/terminal/issues/5000). [I've been prototyping](https://github.com/microsoft/terminal/branches/all?query=oop). [I've been writing specs](https://github.com/microsoft/terminal/tree/main/doc/specs/%235000%20-%20Process%20Model%202.0). There has been a lot of planning that's gone into this, to make sure it works, and works for all the different scenarios we have in mind in the Terminal. Now that we've got a lot of the other foundations out of the way, I'm finally starting to [get close to landing this](https://github.com/microsoft/terminal/tree/dev/migrie/oop/the-whole-thing). So I'd appreciate your continued patience while we work on this. 

As always, we'll post here when there's an update to share. 
"Listen here pal." wow, how condescending. I noticed I had an unknown prejudice against this guy, so looked around for more comments to better inform myself and hopefully change my mind and this is the first one I saw. No need to look further. Holy shit, dude. Get a grip! I may be toxic myself at times (ok, most of the time), but I'm a user. You are a project member and, apparent, MS employee. Just, wow! Take a course on community management or get out of the public eye.
I appreciate that the original comment was worded more strongly and that it may have been a bad day/week for both/either Mike or Erik. I'm also locking this because I want to keep it on-topic -- not to suppress discussion, but because these discussions are never fruitful.
@zadjii-msft Would you be able to provide a brief update of the progress towards these features?  I can see there's definitely been activity and commits where this issue has been referenced but a nice couple sentence paragraph of your progress would be very uplifting.  Thanks for your time!
More specific details, including my todo list are in #5000. I'm hoping to do the control settings rewrite this release (`dev/migrie/oop/ragnarok`), and hopefully some of the other foundation work before the end of the year (`dev/migrie/oop/infinity-war`). Hopefully we can get "`move-pane -w <id>` to move a pane/tab to another Terminal window" early next year, and followed shortly by tearing actual tabs out. This is still my main priority, just one that keeps getting sidelined by other things that can ship in a single release cycle. We've got the end of the year coming up and things always slow way down around then, and there's some time dedicated in the near future to improving our tooling (which should help accelerate overall code velocity)
I just tried to drag a tab from one WT to another, fully expecting it to work. And I ended up here! :-) Another big thumbs up from me for this feature, looking forward to it, it will be really useful.
期待这个功能的进展  
I hope that this feature will not be limited to Windows 11 and will be backwards compatible with Windows 10. A lot of businesses and users are still on Windows 10 and likely will be for the time being.
@MattBDev Don't worry, no part of how I plan on implementing tearoff is restricted to Windows 11. Something quite dramatic would have to come up in the next couple months to change that.
Still waiting for this feature in August 2022...

My thoughts exactly...

This is literally being actively worked on. See https://github.com/microsoft/terminal/issues/5000#issuecomment-1185447944 for the latest status update.
You literally just marked my honest opinion as spam. WTF

This issue was opened 3 years ago. I think community upset over it is
justified, and marking that organic discourse as spam is at best flat our
wrong, and at worst indicative of a seriously dysfunctional corporate
culture at microsoft.

I collapsed the comments because they did not constructively contribute to the conversation at hand. If you'd like to +1 a comment or issue, the best way to do that is to just press the 👍 button. I'm pretty liberal throughout the repo of collapsing any comments that don't constructively contribute to the discussion, especially on a thread with 300 upvotes. Commentary on how we run this repo is probably best directed at a [separate discussion](https://github.com/microsoft/terminal/discussions) rather than in the midst of another thread.

Progress has been made consistently	on tab tear out over the last few years (as linked in the broader thread). It's involved multiple huge refactors of the codebase, so it's taken quite some effort. There are also other priorities that have come up, and with our limited engineering resources, we choose to provide consistent feature enhancements to the product rather than lock everything down for a few months to finish this one feature. 

The work I've got in the last few branches is very promising, but it needs a lot of polish before we can ship it. If anyone's got any advice or wants to help contribute - the comments at the bottom of #5000 have links to the work in progress, and what still needs to be done. Thanks!
I am just adding another comment to the fire instead of an issue request.. Make the windows able to be torn off. This is a BASIC function of any tabbed interface. It is the core of a tabbed interface. 

The entire predicate of a tabbed interface comes from Microdot's prior unwillingness to provide native support for group windows.  Now in this implementation of a Microsoft product, the opposite is true. 
Why you would utilized a common interface feature and then not implement the actual reason why that feature exists is completely bizarre. 

Perhaps the most bizarre about this is that you mention in the original feature request _from 3 years ago_ that this is a multi-instance application. It still is, and it still should be. This aids troubleshooting efforts and minimizes the effort to implement an core function and usability in line with **standard**  UX design recommendations.. 
The application core functions exactly the same as if you have two open windows or one with two tabs. You nest and run the subordinate shells within the app-x parent thread no matter what.... Every tabbed UX application does this.


Honestly take a step back and look at it from a 1000' view. You are currently working on the function and UX of the most critical application for systems administrators for the bulk of your business. Have you ever tried to identify the root cause of a mail issue with a hybrid deployment of azure and on-prem exchange? It,s not unusual to have 4 screens open at once actively gathering or displaying information. 

This issue should be prioritized and there shouldn't be a complaint about it. This team made the decision to revolutionize the method thhat people utilize the Windows shell. For some reason you only read half the book on a UX design. Imagine if Microsoft Launched te chromium version of Edge and didn't implement tear off tabs.  How would the general reception of that product go?
Thanks for your input.

This issue is well understood and additional community commentary on it has not provided an equivalent amount of additional clarity.

I'm going to lock it for now. If there is additional insight you can provide, I'd love to receive it in a thread that will _not_ notify upwards of twenty people and an unknown number of additional subscribers.

Fix patch:
diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp
index ee0e1b1e3bd..a62493ecb4a 100644
--- a/src/cascadia/Remoting/Monarch.cpp
+++ b/src/cascadia/Remoting/Monarch.cpp
@@ -1109,4 +1109,39 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
             // drag/drop)
         }
     }
+
+    // Very similar to the above. Someone came and told us that they were the target of a drag/drop, and they know who started it.
+    // We will go tell the person who started it that they should send that target the content which was dragged.
+    void Monarch::RequestSendContent(const Remoting::RequestReceiveContentArgs& args)
+    {
+        TraceLoggingWrite(g_hRemotingProvider,
+                          "Monarch_SendContent_Requested",
+                          TraceLoggingUInt64(args.SourceWindow(), "source", "The window which started the drag"),
+                          TraceLoggingUInt64(args.TargetWindow(), "target", "The window which was the target of the drop"),
+                          TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
+                          TraceLoggingKeyword(TIL_KEYWORD_TRACE));
+
+        if (auto senderPeasant{ _getPeasant(args.SourceWindow()) })
+        {
+            senderPeasant.SendContent(args);
+
+            TraceLoggingWrite(g_hRemotingProvider,
+                              "Monarch_SendContent_Completed",
+                              TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
+                              TraceLoggingKeyword(TIL_KEYWORD_TRACE));
+        }
+        else
+        {
+            TraceLoggingWrite(g_hRemotingProvider,
+                              "Monarch_SendContent_NoWindow",
+                              TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
+                              TraceLoggingKeyword(TIL_KEYWORD_TRACE));
+
+            // TODO GH#5000
+            //
+            // In the case where window couldn't be found, then create a window
+            // for that name / ID. Do this as a part of tear-out (different than
+            // drag/drop)
+        }
+    }
 }
diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h
index 33a7d6711f7..54c31e98e27 100644
--- a/src/cascadia/Remoting/Monarch.h
+++ b/src/cascadia/Remoting/Monarch.h
@@ -82,6 +82,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
         Windows::Foundation::Collections::IVector<winrt::hstring> GetAllWindowLayouts();
 
         void RequestMoveContent(winrt::hstring window, winrt::hstring content, uint32_t tabIndex);
+        void RequestSendContent(const Remoting::RequestReceiveContentArgs& args);
 
         TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs);
         TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl
index 0bbc337ca70..44ebf7b0f1c 100644
--- a/src/cascadia/Remoting/Monarch.idl
+++ b/src/cascadia/Remoting/Monarch.idl
@@ -41,8 +41,7 @@ namespace Microsoft.Terminal.Remoting
         Windows.Foundation.IReference<UInt64> WindowID;
     }
 
-    [default_interface] runtimeclass QuitAllRequestedArgs 
-    {
+    [default_interface] runtimeclass QuitAllRequestedArgs {
         QuitAllRequestedArgs();
         Windows.Foundation.IAsyncAction BeforeQuitAllAction;
     }
@@ -71,6 +70,7 @@ namespace Microsoft.Terminal.Remoting
         Windows.Foundation.Collections.IVector<String> GetAllWindowLayouts();
 
         void RequestMoveContent(String window, String content, UInt32 tabIndex);
+        void RequestSendContent(RequestReceiveContentArgs args);
 
         event Windows.Foundation.TypedEventHandler<Object, FindTargetWindowArgs> FindTargetWindowRequested;
         event Windows.Foundation.TypedEventHandler<Object, Object> ShowNotificationIconRequested;
diff --git a/src/cascadia/Remoting/Peasant.cpp b/src/cascadia/Remoting/Peasant.cpp
index 659de9b72fc..94f8406c00c 100644
--- a/src/cascadia/Remoting/Peasant.cpp
+++ b/src/cascadia/Remoting/Peasant.cpp
@@ -9,6 +9,7 @@
 #include "Peasant.g.cpp"
 #include "../../types/inc/utils.hpp"
 #include "AttachRequest.g.cpp"
+#include "RequestReceiveContentArgs.g.cpp"
 
 using namespace winrt;
 using namespace winrt::Microsoft::Terminal;
@@ -327,4 +328,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
         }
         return args->WindowLayoutJson();
     }
+
+    void Peasant::SendContent(const Remoting::RequestReceiveContentArgs& args)
+    {
+        _SendContentRequestedHandlers(*this, args);
+    }
 }
diff --git a/src/cascadia/Remoting/Peasant.h b/src/cascadia/Remoting/Peasant.h
index a0fb55dce73..26a82a17fef 100644
--- a/src/cascadia/Remoting/Peasant.h
+++ b/src/cascadia/Remoting/Peasant.h
@@ -6,6 +6,7 @@
 #include "Peasant.g.h"
 #include "RenameRequestArgs.h"
 #include "AttachRequest.g.h"
+#include "RequestReceiveContentArgs.g.h"
 
 namespace RemotingUnitTests
 {
@@ -25,6 +26,19 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
             _TabIndex{ tabIndex } {};
     };
 
+    struct RequestReceiveContentArgs : RequestReceiveContentArgsT<RequestReceiveContentArgs>
+    {
+        WINRT_PROPERTY(uint64_t, SourceWindow);
+        WINRT_PROPERTY(uint64_t, TargetWindow);
+        WINRT_PROPERTY(uint32_t, TabIndex);
+
+    public:
+        RequestReceiveContentArgs(const uint64_t src, const uint64_t tgt, const uint32_t tabIndex) :
+            _SourceWindow{ src },
+            _TargetWindow{ tgt },
+            _TabIndex{ tabIndex } {};
+    };
+
     struct Peasant : public PeasantT<Peasant>
     {
         Peasant();
@@ -52,6 +66,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
         winrt::Microsoft::Terminal::Remoting::CommandlineArgs InitialArgs();
 
         winrt::hstring GetWindowLayout();
+        void SendContent(const winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs& args);
 
         WINRT_PROPERTY(winrt::hstring, WindowName);
         WINRT_PROPERTY(winrt::hstring, ActiveTabTitle);
@@ -70,6 +85,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
         TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::GetWindowLayoutArgs);
 
         TYPED_EVENT(AttachRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::AttachRequest);
+        TYPED_EVENT(SendContentRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs);
 
     private:
         Peasant(const uint64_t testPID);
@@ -87,4 +103,5 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
 namespace winrt::Microsoft::Terminal::Remoting::factory_implementation
 {
     BASIC_FACTORY(Peasant);
+    BASIC_FACTORY(RequestReceiveContentArgs);
 }
diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl
index a26604e4bf6..2b8780cae8d 100644
--- a/src/cascadia/Remoting/Peasant.idl
+++ b/src/cascadia/Remoting/Peasant.idl
@@ -55,6 +55,13 @@ namespace Microsoft.Terminal.Remoting
         String Content { get; };
         UInt32 TabIndex { get; };
     }
+    [default_interface] runtimeclass RequestReceiveContentArgs {
+        RequestReceiveContentArgs(UInt64 src, UInt64 tgt, UInt32 tabIndex);
+
+        UInt64 SourceWindow { get; };
+        UInt64 TargetWindow { get; };
+        UInt32 TabIndex { get; };
+    };
 
     interface IPeasant
     {
@@ -82,7 +89,7 @@ namespace Microsoft.Terminal.Remoting
         String GetWindowLayout();
 
         void AttachContentToWindow(AttachRequest request);
-
+        void SendContent(RequestReceiveContentArgs args);
 
         event Windows.Foundation.TypedEventHandler<Object, WindowActivatedArgs> WindowActivated;
         event Windows.Foundation.TypedEventHandler<Object, CommandlineArgs> ExecuteCommandlineRequested;
@@ -98,6 +105,8 @@ namespace Microsoft.Terminal.Remoting
         event Windows.Foundation.TypedEventHandler<Object, Object> QuitRequested;
 
         event Windows.Foundation.TypedEventHandler<Object, AttachRequest> AttachRequested;
+
+        event Windows.Foundation.TypedEventHandler<Object, RequestReceiveContentArgs> SendContentRequested;
     };
 
     [default_interface] runtimeclass Peasant : IPeasant
diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp
index 5fdb1830c25..afa8403790e 100644
--- a/src/cascadia/Remoting/WindowManager.cpp
+++ b/src/cascadia/Remoting/WindowManager.cpp
@@ -422,4 +422,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
         _monarch.RequestMoveContent(window, content, tabIndex);
     }
 
+    winrt::fire_and_forget WindowManager::RequestSendContent(Remoting::RequestReceiveContentArgs args)
+    {
+        co_await winrt::resume_background();
+        _monarch.RequestSendContent(args);
+    }
 }
diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h
index 89dcf0962cb..e42d6b4ba27 100644
--- a/src/cascadia/Remoting/WindowManager.h
+++ b/src/cascadia/Remoting/WindowManager.h
@@ -45,6 +45,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation
         bool DoesQuakeWindowExist();
 
         winrt::fire_and_forget RequestMoveContent(winrt::hstring window, winrt::hstring content, uint32_t tabIndex);
+        winrt::fire_and_forget RequestSendContent(Remoting::RequestReceiveContentArgs args);
 
         TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs);
 
diff --git a/src/cascadia/Remoting/WindowManager.idl b/src/cascadia/Remoting/WindowManager.idl
index d01670f8bd0..ca9d78a5d1a 100644
--- a/src/cascadia/Remoting/WindowManager.idl
+++ b/src/cascadia/Remoting/WindowManager.idl
@@ -27,6 +27,7 @@ namespace Microsoft.Terminal.Remoting
         Boolean DoesQuakeWindowExist();
 
         void RequestMoveContent(String window, String content, UInt32 tabIndex);
+        void RequestSendContent(RequestReceiveContentArgs args);
 
         event Windows.Foundation.TypedEventHandler<Object, FindTargetWindowArgs> FindTargetWindowRequested;
 
diff --git a/src/cascadia/TerminalApp/TabBase.cpp b/src/cascadia/TerminalApp/TabBase.cpp
index 52734c9f31e..54b25c48b08 100644
--- a/src/cascadia/TerminalApp/TabBase.cpp
+++ b/src/cascadia/TerminalApp/TabBase.cpp
@@ -264,6 +264,19 @@ namespace winrt::TerminalApp::implementation
                 tab->_RequestFocusActiveControlHandlers();
             }
         });
+
+        // BODGY: When the tab is drag/dropped, the TabView gets a
+        // TabDragStarting. However, the way it is implemented[^1], the
+        // TabViewItem needs either an Item or a Content for the event to
+        // include the correct TabViewItem. Otherwise, it will just return the
+        // first TabViewItem in the TabView with the same Content as the dragged
+        // tab (which, if the Content is null, will be the _first_ tab).
+        //
+        // So here, we'll stick an empty border in, just so that every tab has a
+        // Content which is not equal to the others.
+        //
+        // [^1]: microsoft-ui-xaml/blob/92fbfcd55f05c92ac65569f5d284c5b36492091e/dev/TabView/TabView.cpp#L751-L758
+        TabViewItem().Content(winrt::WUX::Controls::Border{});
     }
 
     std::optional<winrt::Windows::UI::Color> TabBase::GetTabColor()
diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp
index 511c4925fe9..560a531650b 100644
--- a/src/cascadia/TerminalApp/TabManagement.cpp
+++ b/src/cascadia/TerminalApp/TabManagement.cpp
@@ -512,6 +512,11 @@ namespace winrt::TerminalApp::implementation
             _mruTabs.RemoveAt(mruIndex);
         }
 
+        if (_stashedDraggedTab && *_stashedDraggedTab == tab)
+        {
+            _stashedDraggedTab = nullptr;
+        }
+
         _tabs.RemoveAt(tabIndex);
         _tabView.TabItems().RemoveAt(tabIndex);
         _UpdateTabIndices();
@@ -703,7 +708,8 @@ namespace winrt::TerminalApp::implementation
     winrt::TerminalApp::TabBase TerminalPage::_GetTabByTabViewItem(const Microsoft::UI::Xaml::Controls::TabViewItem& tabViewItem) const noexcept
     {
         uint32_t tabIndexFromControl{};
-        if (_tabView.TabItems().IndexOf(tabViewItem, tabIndexFromControl))
+        const auto items{ _tabView.TabItems() };
+        if (items.IndexOf(tabViewItem, tabIndexFromControl))
         {
             // If IndexOf returns true, we've actually got an index
             return _tabs.GetAt(tabIndexFromControl);
diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp
index 013e766cec1..efe73161ec0 100644
--- a/src/cascadia/TerminalApp/TerminalPage.cpp
+++ b/src/cascadia/TerminalApp/TerminalPage.cpp
@@ -8,6 +8,7 @@
 #include "LastTabClosedEventArgs.g.cpp"
 #include "RenameWindowRequestedArgs.g.cpp"
 #include "RequestMoveContentArgs.g.cpp"
+#include "RequestReceiveContentArgs.g.cpp"
 
 #include <filesystem>
 
@@ -246,6 +247,10 @@ namespace winrt::TerminalApp::implementation
         _tabView.TabCloseRequested({ this, &TerminalPage::_OnTabCloseRequested });
         _tabView.TabItemsChanged({ this, &TerminalPage::_OnTabItemsChanged });
 
+        _tabView.TabDragStarting({ this, &TerminalPage::_onTabDragStarting });
+        _tabView.TabStripDragOver({ this, &TerminalPage::_onTabStripDragOver });
+        _tabView.TabStripDrop({ this, &TerminalPage::_onTabStripDrop });
+
         _CreateNewTabFlyout();
 
         _UpdateTabWidthMode();
@@ -1959,12 +1964,15 @@ namespace winrt::TerminalApp::implementation
         });
     }
 
-    void TerminalPage::_DetachTabFromWindow(const winrt::com_ptr<TerminalTab>& terminalTab)
+    void TerminalPage::_DetachTabFromWindow(const winrt::com_ptr<TabBase>& tab)
     {
-        // Detach the root pane, which will act like the whole tab got detached.
-        if (const auto rootPane = terminalTab->GetRootPane())
+        if (const auto terminalTab = tab.try_as<TerminalTab>())
         {
-            _DetachPaneFromWindow(rootPane);
+            // Detach the root pane, which will act like the whole tab got detached.
+            if (const auto rootPane = terminalTab->GetRootPane())
+            {
+                _DetachPaneFromWindow(rootPane);
+            }
         }
     }
 
@@ -2073,6 +2081,25 @@ namespace winrt::TerminalApp::implementation
         {
             _actionDispatch->DoAction(action);
         }
+
+        // After handling all the actions, then re-check the tabIndex. We might
+        // have been called as a part of a tab drag/drop. In that case, the
+        // tabIndex is actually relevant, and we need to move the tab we just
+        // made into position.
+        if (!firstIsSplitPane && tabIndex != -1)
+        {
+            // Move the currently active tab to the requested index Use the
+            // currently focused tab index, because we don't know if the new tab
+            // opened at the end of the list, or adjacent to the previously
+            // active tab. This is affected by the user's "newTabPosition"
+            // setting.
+            if (const auto focusedTabIndex = _GetFocusedTabIndex())
+            {
+                const auto source = *focusedTabIndex;
+                _TryMoveTab(source, tabIndex);
+            }
+            // else: This shouldn't really be possible, because the tab we _just_ opened should be active.
+        }
     }
 
     // Method Description:
@@ -4572,4 +4599,160 @@ namespace winrt::TerminalApp::implementation
         }
     }
 
+    void TerminalPage::_onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView&,
+                                          const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e)
+    {
+        // Get the tab impl from this event.
+        const auto eventTab = e.Tab();
+        const auto tabBase = _GetTabByTabViewItem(eventTab);
+        winrt::com_ptr<TabBase> tabImpl;
+        tabImpl.copy_from(winrt::get_self<TabBase>(tabBase));
+        if (tabImpl)
+        {
+            // First: stash the tab we started dragging.
+            // We're going to be asked for this.
+            _stashedDraggedTab = tabImpl;
+
+            // Into the DataPackage, let's stash our own window ID.
+            const auto id{ _WindowProperties.WindowId() };
+
+            // Get our PID
+            const auto pid{ GetCurrentProcessId() };
+
+            e.Data().Properties().Insert(L"windowId", winrt::box_value(id));
+            e.Data().Properties().Insert(L"pid", winrt::box_value<uint32_t>(pid));
+            e.Data().RequestedOperation(DataPackageOperation::Move);
+
+            // The next thing that will happen:
+            //  * Another TerminalPage will get a TabStripDragOver, then get a
+            //    TabStripDrop
+            //    * This will be handled by the _other_ page asking the monarch
+            //      to ask us to send our content to them.
+            //  * We'll get a TabDroppedOutside to indicate that this tab was
+            //    dropped _not_ on a TabView.
+            //    * This we can't handle yet, and is the last point of TODO GH#5000
+        }
+    }
+
+    void TerminalPage::_onTabStripDragOver(const winrt::Windows::Foundation::IInspectable& /*sender*/,
+                                           const winrt::Windows::UI::Xaml::DragEventArgs& e)
+    {
+        // We must mark that we can accept the drag/drop. The system will never
+        // call TabStripDrop on us if we don't indicate that we're willing.
+        const auto& props{ e.DataView().Properties() };
+        if (props.HasKey(L"windowId") &&
+            props.HasKey(L"pid") &&
+            (winrt::unbox_value_or<uint32_t>(props.TryLookup(L"pid"), 0u) == GetCurrentProcessId()))
+        {
+            e.AcceptedOperation(DataPackageOperation::Move);
+        }
+
+        // You may think to yourself, this is a great place to increase the
+        // width of the TabView artificially, to make room for the new tab item.
+        // However, we'll never get a message that the tab left the tab view
+        // (without being dropped). So there's no good way to resize back down.
+    }
+
+    // Method Description:
+    // - Called on the TARGET of a tab drag/drop. We'll unpack the DataPackage
+    //   to find who the tab came from. We'll then ask the Monarch to ask the
+    //   sender to move that tab to us.
+    winrt::fire_and_forget TerminalPage::_onTabStripDrop(winrt::Windows::Foundation::IInspectable /*sender*/,
+                                                         winrt::Windows::UI::Xaml::DragEventArgs e)
+    {
+        // Get the PID and make sure it is the same as ours.
+        if (const auto& pidObj{ e.DataView().Properties().TryLookup(L"pid") })
+        {
+            const auto pid{ winrt::unbox_value_or<uint32_t>(pidObj, 0u) };
+            if (pid != GetCurrentProcessId())
+            {
+                // The PID doesn't match ours. We can't handle this drop.
+                co_return;
+            }
+        }
+        else
+        {
+            // No PID? We can't handle this drop. Bail.
+            co_return;
+        }
+
+        const auto& windowIdObj{ e.DataView().Properties().TryLookup(L"windowId") };
+        if (windowIdObj == nullptr)
+        {
+            // No windowId? Bail.
+            co_return;
+        }
+        const uint64_t src{ winrt::unbox_value<uint64_t>(windowIdObj) };
+
+        // Figure out where in the tab strip we're dropping this tab. Add that
+        // index to the request. This is largely taken from the WinUI sample
+        // app.
+
+        // We need to be on OUR UI thread to figure out where we dropped
+        auto weakThis{ get_weak() };
+        co_await wil::resume_foreground(Dispatcher());
+        if (const auto& page{ weakThis.get() })
+        {
+            // First we need to get the position in the List to drop to
+            auto index = -1;
+
+            // Determine which items in the list our pointer is between.
+            for (auto i = 0u; i < _tabView.TabItems().Size(); i++)
+            {
+                if (const auto& item{ _tabView.ContainerFromIndex(i).try_as<winrt::MUX::Controls::TabViewItem>() })
+                {
+                    const auto posX{ e.GetPosition(item).X }; // The point of the drop, relative to the tab
+                    const auto itemWidth{ item.ActualWidth() }; // The right of the tab
+                    // If the drag point is on the left half of the tab, then insert here.
+                    if (posX < itemWidth / 2)
+                    {
+                        index = i;
+                        break;
+                    }
+                }
+            }
+
+            // `this` is safe to use
+            const auto request = winrt::make_self<RequestReceiveContentArgs>(src, _WindowProperties.WindowId(), index);
+
+            // This will go up to the monarch, who will then dispatch the request
+            // back down to the source TerminalPage, who will then perform a
+            // RequestMoveContent to move their tab to us.
+            _RequestReceiveContentHandlers(*this, *request);
+        }
+    }
+
+    // Method Description:
+    // - This is called on the drag/drop SOURCE TerminalPage, when the monarch has
+    //   requested that we send our tab to another window. We'll need to
+    //   serialize the tab, and send it to the monarch, who will then send it to
+    //   the destination window.
+    // - Fortunately, sending the tab is basically just a MoveTab action, so we
+    //   can largely reuse that.
+    winrt::fire_and_forget TerminalPage::SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args)
+    {
+        // validate that we're the source window of the tab in this request
+        if (args.SourceWindow() != _WindowProperties.WindowId())
+        {
+            co_return;
+        }
+        if (!_stashedDraggedTab)
+        {
+            co_return;
+        }
+
+        // must do the work of adding/removing tabs on the UI thread.
+        auto weakThis{ get_weak() };
+        co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::Normal);
+        if (const auto& page{ weakThis.get() })
+        {
+            // `this` is safe to use in here.
+            auto startupActions = _stashedDraggedTab->BuildStartupActions(true);
+            _DetachTabFromWindow(_stashedDraggedTab);
+            _MoveContent(std::move(startupActions), winrt::hstring{ fmt::format(L"{}", args.TargetWindow()) }, args.TabIndex());
+            // _RemoveTab will make sure to null out the _stashedDraggedTab
+            _RemoveTab(*_stashedDraggedTab);
+        }
+    }
+
 }
diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h
index 98bc0c79806..46cf9c11c8c 100644
--- a/src/cascadia/TerminalApp/TerminalPage.h
+++ b/src/cascadia/TerminalApp/TerminalPage.h
@@ -10,6 +10,7 @@
 #include "LastTabClosedEventArgs.g.h"
 #include "RenameWindowRequestedArgs.g.h"
 #include "RequestMoveContentArgs.g.h"
+#include "RequestReceiveContentArgs.g.h"
 #include "Toast.h"
 
 #define DECLARE_ACTION_HANDLER(action) void _Handle##action(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args);
@@ -74,6 +75,19 @@ namespace winrt::TerminalApp::implementation
             _TabIndex{ tabIndex } {};
     };
 
+    struct RequestReceiveContentArgs : RequestReceiveContentArgsT<RequestReceiveContentArgs>
+    {
+        WINRT_PROPERTY(uint64_t, SourceWindow);
+        WINRT_PROPERTY(uint64_t, TargetWindow);
+        WINRT_PROPERTY(uint32_t, TabIndex);
+
+    public:
+        RequestReceiveContentArgs(const uint64_t src, const uint64_t tgt, const uint32_t tabIndex) :
+            _SourceWindow{ src },
+            _TargetWindow{ tgt },
+            _TabIndex{ tabIndex } {};
+    };
+
     struct TerminalPage : TerminalPageT<TerminalPage>
     {
     public:
@@ -149,6 +163,7 @@ namespace winrt::TerminalApp::implementation
         bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down);
 
         winrt::fire_and_forget AttachContent(winrt::hstring content, uint32_t tabIndex);
+        winrt::fire_and_forget SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args);
 
         WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
 
@@ -173,6 +188,7 @@ namespace winrt::TerminalApp::implementation
         TYPED_EVENT(ShowWindowChanged, IInspectable, winrt::Microsoft::Terminal::Control::ShowWindowArgs)
 
         TYPED_EVENT(RequestMoveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestMoveContentArgs);
+        TYPED_EVENT(RequestReceiveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs);
 
         WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, _PropertyChangedHandlers, nullptr);
 
@@ -247,6 +263,8 @@ namespace winrt::TerminalApp::implementation
 
         TerminalApp::ContentManager _manager{ nullptr };
 
+        winrt::com_ptr<winrt::TerminalApp::implementation::TabBase> _stashedDraggedTab{ nullptr };
+
         winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection::NewConnection_revoker _newConnectionRevoker;
 
         winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::UI::Xaml::Controls::ContentDialogResult> _ShowDialogHelper(const std::wstring_view& name);
@@ -482,8 +500,12 @@ namespace winrt::TerminalApp::implementation
         winrt::fire_and_forget _ShowWindowChangedHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args);
         winrt::fire_and_forget _windowPropertyChanged(const IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args);
 
+        void _onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e);
+        void _onTabStripDragOver(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::DragEventArgs& e);
+        winrt::fire_and_forget _onTabStripDrop(winrt::Windows::Foundation::IInspectable sender, winrt::Windows::UI::Xaml::DragEventArgs e);
+
         void _DetachPaneFromWindow(std::shared_ptr<Pane> pane);
-        void _DetachTabFromWindow(const winrt::com_ptr<TerminalTab>& terminalTab);
+        void _DetachTabFromWindow(const winrt::com_ptr<TabBase>& terminalTab);
         void _MoveContent(std::vector<winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs>&& actions, const winrt::hstring& windowName, const uint32_t tabIndex);
 
         void _ContextMenuOpened(const IInspectable& sender, const IInspectable& args);
@@ -505,4 +527,5 @@ namespace winrt::TerminalApp::implementation
 namespace winrt::TerminalApp::factory_implementation
 {
     BASIC_FACTORY(TerminalPage);
+    BASIC_FACTORY(RequestReceiveContentArgs);
 }
diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl
index 6832fc6e12b..a23203046dd 100644
--- a/src/cascadia/TerminalApp/TerminalPage.idl
+++ b/src/cascadia/TerminalApp/TerminalPage.idl
@@ -30,6 +30,13 @@ namespace TerminalApp
         String Content { get; };
         UInt32 TabIndex { get; };
     };
+    [default_interface] runtimeclass RequestReceiveContentArgs {
+        RequestReceiveContentArgs(UInt64 src, UInt64 tgt, UInt32 tabIndex);
+
+        UInt64 SourceWindow { get; };
+        UInt64 TargetWindow { get; };
+        UInt32 TabIndex { get; };
+    };
 
     interface IDialogPresenter
     {
@@ -74,6 +81,7 @@ namespace TerminalApp
 
         Windows.UI.Xaml.Media.Brush TitlebarBrush { get; };
         void WindowActivated(Boolean activated);
+        void SendContentToOther(RequestReceiveContentArgs args);
 
         event Windows.Foundation.TypedEventHandler<Object, String> TitleChanged;
         event Windows.Foundation.TypedEventHandler<Object, LastTabClosedEventArgs> LastTabClosed;
@@ -92,6 +100,6 @@ namespace TerminalApp
         event Windows.Foundation.TypedEventHandler<Object, Microsoft.Terminal.Control.ShowWindowArgs> ShowWindowChanged;
 
         event Windows.Foundation.TypedEventHandler<Object, RequestMoveContentArgs> RequestMoveContent;
-
+        event Windows.Foundation.TypedEventHandler<Object, RequestReceiveContentArgs> RequestReceiveContent;
     }
 }
diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp
index 2d7d59bc604..3db175daec8 100644
--- a/src/cascadia/TerminalApp/TerminalWindow.cpp
+++ b/src/cascadia/TerminalApp/TerminalWindow.cpp
@@ -1182,6 +1182,13 @@ namespace winrt::TerminalApp::implementation
             _root->AttachContent(content, tabIndex);
         }
     }
+    void TerminalWindow::SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args)
+    {
+        if (_root)
+        {
+            _root->SendContentToOther(args);
+        }
+    }
 
     bool TerminalWindow::ShouldImmediatelyHandoffToElevated()
     {
diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h
index 5412da866a9..eda5c9e84ab 100644
--- a/src/cascadia/TerminalApp/TerminalWindow.h
+++ b/src/cascadia/TerminalApp/TerminalWindow.h
@@ -143,6 +143,7 @@ namespace winrt::TerminalApp::implementation
         TerminalApp::WindowProperties WindowProperties() { return *_WindowProperties; }
 
         void AttachContent(winrt::hstring content, uint32_t tabIndex);
+        void SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args);
 
         // -------------------------------- WinRT Events ---------------------------------
         // PropertyChanged is surprisingly not a typed event, so we'll define that one manually.
@@ -219,6 +220,7 @@ namespace winrt::TerminalApp::implementation
         TYPED_EVENT(SettingsChanged, winrt::Windows::Foundation::IInspectable, winrt::TerminalApp::SettingsLoadEventArgs);
 
         FORWARDED_TYPED_EVENT(RequestMoveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestMoveContentArgs, _root, RequestMoveContent);
+        FORWARDED_TYPED_EVENT(RequestReceiveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs, _root, RequestReceiveContent);
 
 #ifdef UNIT_TESTING
         friend class TerminalAppLocalTests::CommandlineTest;
diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl
index 2307336c39b..770289895e2 100644
--- a/src/cascadia/TerminalApp/TerminalWindow.idl
+++ b/src/cascadia/TerminalApp/TerminalWindow.idl
@@ -137,7 +137,9 @@ namespace TerminalApp
         event Windows.Foundation.TypedEventHandler<Object, SettingsLoadEventArgs> SettingsChanged;
 
         event Windows.Foundation.TypedEventHandler<Object, RequestMoveContentArgs> RequestMoveContent;
-        void AttachContent(String content, UInt32 tabIndex);
+        event Windows.Foundation.TypedEventHandler<Object, RequestReceiveContentArgs> RequestReceiveContent;
 
+        void AttachContent(String content, UInt32 tabIndex);
+        void SendContentToOther(RequestReceiveContentArgs args);
     }
 }
diff --git a/src/cascadia/TerminalControl/ControlInteractivity.idl b/src/cascadia/TerminalControl/ControlInteractivity.idl
index 47a2920f0eb..086b009ba09 100644
--- a/src/cascadia/TerminalControl/ControlInteractivity.idl
+++ b/src/cascadia/TerminalControl/ControlInteractivity.idl
@@ -72,10 +72,10 @@ namespace Microsoft.Terminal.Control
         event Windows.Foundation.TypedEventHandler<Object, ScrollPositionChangedArgs> ScrollPositionChanged;
         event Windows.Foundation.TypedEventHandler<Object, PasteFromClipboardEventArgs> PasteFromClipboard;
 
-        event Windows.Foundation.TypedEventHandler<Object, Object> Attached;
-
         event Windows.Foundation.TypedEventHandler<Object, Object> Closed;
 
+        event Windows.Foundation.TypedEventHandler<Object, Object> Attached;
+
         // Used to communicate to the TermControl, but not necessarily higher up in the stack
         event Windows.Foundation.TypedEventHandler<Object, ContextMenuRequestedEventArgs> ContextMenuRequested;
 
diff --git a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp
index 111dfb5877a..058a6c0793c 100644
--- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp
+++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp
@@ -86,6 +86,7 @@ namespace RemotingUnitTests
         void RequestQuitAll() DIE;
         void Quit() DIE;
         void AttachContentToWindow(Remoting::AttachRequest) DIE;
+        void SendContent(winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs) DIE;
         TYPED_EVENT(WindowActivated, winrt::Windows::Foundation::IInspectable, Remoting::WindowActivatedArgs);
         TYPED_EVENT(ExecuteCommandlineRequested, winrt::Windows::Foundation::IInspectable, Remoting::CommandlineArgs);
         TYPED_EVENT(IdentifyWindowsRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
@@ -98,6 +99,7 @@ namespace RemotingUnitTests
         TYPED_EVENT(QuitRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
         TYPED_EVENT(GetWindowLayoutRequested, winrt::Windows::Foundation::IInspectable, Remoting::GetWindowLayoutArgs);
         TYPED_EVENT(AttachRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::AttachRequest);
+        TYPED_EVENT(SendContentRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs);
     };
 
     // Same idea.
@@ -117,6 +119,7 @@ namespace RemotingUnitTests
         winrt::Windows::Foundation::Collections::IVectorView<Remoting::PeasantInfo> GetPeasantInfos() DIE;
         winrt::Windows::Foundation::Collections::IVector<winrt::hstring> GetAllWindowLayouts() DIE;
         void RequestMoveContent(winrt::hstring, winrt::hstring, uint32_t) DIE;
+        void RequestSendContent(Remoting::RequestReceiveContentArgs) DIE;
 
         TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, Remoting::FindTargetWindowArgs);
         TYPED_EVENT(ShowNotificationIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable);
diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp
index f190940e837..b4958ece432 100644
--- a/src/cascadia/WindowsTerminal/AppHost.cpp
+++ b/src/cascadia/WindowsTerminal/AppHost.cpp
@@ -384,7 +384,8 @@ void AppHost::Initialize()
     _revokers.QuitRequested = _windowLogic.QuitRequested(winrt::auto_revoke, { this, &AppHost::_RequestQuitAll });
     _revokers.ShowWindowChanged = _windowLogic.ShowWindowChanged(winrt::auto_revoke, { this, &AppHost::_ShowWindowChanged });
     _revokers.RequestMoveContent = _windowLogic.RequestMoveContent(winrt::auto_revoke, { this, &AppHost::_handleMoveContent });
-
+    _revokers.RequestReceiveContent = _windowLogic.RequestReceiveContent(winrt::auto_revoke, { this, &AppHost::_handleReceiveContent });
+    _revokers.SendContentRequested = _peasant.SendContentRequested(winrt::auto_revoke, { this, &AppHost::_handleSendContent });
     // BODGY
     // On certain builds of Windows, when Terminal is set as the default
     // it will accumulate an unbounded amount of queued animations while
@@ -1232,6 +1233,23 @@ void AppHost::_handleAttach(const winrt::Windows::Foundation::IInspectable& /*se
     _windowLogic.AttachContent(args.Content(), args.TabIndex());
 }
 
+// Page -> us -> manager -> monarch
+// The page wants to tell the monarch that it was the drop target for a drag drop.
+// The manager will tell the monarch to tell the _other_ window to send its content to us.
+void AppHost::_handleReceiveContent(const winrt::Windows::Foundation::IInspectable& /* sender */,
+                                    winrt::TerminalApp::RequestReceiveContentArgs args)
+{
+    _windowManager.RequestSendContent(winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs{ args.SourceWindow(), args.TargetWindow(), args.TabIndex() });
+}
+
+// monarch -> Peasant -> us -> Page
+// The Monarch was told to tell us to send our dragged content to someone else.
+void AppHost::_handleSendContent(const winrt::Windows::Foundation::IInspectable& /* sender */,
+                                 winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs args)
+{
+    _windowLogic.SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs{ args.SourceWindow(), args.TargetWindow(), args.TabIndex() });
+}
+
 // Bubble the update settings request up to the emperor. We're being called on
 // the Window thread, but the Emperor needs to update the settings on the _main_
 // thread.
diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h
index a9b8543ffbe..3521d984b6a 100644
--- a/src/cascadia/WindowsTerminal/AppHost.h
+++ b/src/cascadia/WindowsTerminal/AppHost.h
@@ -127,6 +127,14 @@ class AppHost
 
     void _requestUpdateSettings();
 
+    // Page -> us -> monarch
+    void _handleReceiveContent(const winrt::Windows::Foundation::IInspectable& sender,
+                               winrt::TerminalApp::RequestReceiveContentArgs args);
+
+    // monarch -> us -> Page
+    void _handleSendContent(const winrt::Windows::Foundation::IInspectable& sender,
+                            winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs args);
+
     winrt::event_token _GetWindowLayoutRequestedToken;
 
     // Helper struct. By putting these all into one struct, we can revoke them
@@ -161,9 +169,11 @@ class AppHost
         winrt::TerminalApp::TerminalWindow::QuitRequested_revoker QuitRequested;
         winrt::TerminalApp::TerminalWindow::ShowWindowChanged_revoker ShowWindowChanged;
         winrt::TerminalApp::TerminalWindow::RequestMoveContent_revoker RequestMoveContent;
+        winrt::TerminalApp::TerminalWindow::RequestReceiveContent_revoker RequestReceiveContent;
         winrt::TerminalApp::TerminalWindow::PropertyChanged_revoker PropertyChanged;
         winrt::TerminalApp::TerminalWindow::SettingsChanged_revoker SettingsChanged;
 
         winrt::Microsoft::Terminal::Remoting::WindowManager::QuitAllRequested_revoker QuitAllRequested;
+        winrt::Microsoft::Terminal::Remoting::Peasant::SendContentRequested_revoker SendContentRequested;
     } _revokers{};
 };


Test patch: